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
@@ -7,10 +7,726 @@
7
7
  */
8
8
  import { z } from "zod";
9
9
  import { PERSONA_CATEGORIES } from "../../persona-questionnaire.js";
10
+ import { getAnyPersona, getCognitiveProfile } from "../../personas.js";
11
+ import { valueAxisCorrelationCounts, TRAIT_VALUE_CORRELATIONS } from "../../persona-questionnaire.js";
12
+ import { deriveValuesFromBigFive, bigFiveReachableAxes, BIG_FIVE_VALUE_LINKS } from "../../values/big-five-values.js";
13
+ import { widgetUri } from "../widget-kit.js";
10
14
  import { getPersonaValues, PERSONA_VALUE_PROFILES, rankInfluencePatternsForProfile, INFLUENCE_PATTERNS, } from "../../values/index.js";
15
+ export function resolvePersonaValues(name, override) {
16
+ const KEYS = ["selfDirection", "stimulation", "hedonism", "achievement", "power",
17
+ "security", "conformity", "tradition", "benevolence", "universalism"];
18
+ const registry = getPersonaValues(name);
19
+ const looked = getAnyPersona(name);
20
+ // Supplied data wins over the lookup, and stands in for it entirely when the
21
+ // lookup finds nothing.
22
+ const persona = (override
23
+ ? { ...(looked ?? {}),
24
+ ...(override.valuesDerivation ? { valuesDerivation: override.valuesDerivation } : {}),
25
+ ...(override.values ? { schwartzValues: override.values } : {}),
26
+ ...(override.traits ? { cognitiveTraits: override.traits } : {}) }
27
+ : looked);
28
+ const own = (override?.values ?? persona?.schwartzValues);
29
+ // Big Five, when the persona carries it and has no explicit values.
30
+ //
31
+ // Preferred over the cognitive-trait derivation because the published value
32
+ // correlations are with the Big Five: this path crosses no gap the research
33
+ // does not cover, and it reaches all thirteen axes rather than nine. Explicit
34
+ // values still win — someone who wrote the numbers down meant them.
35
+ const bigFive = persona?.bigFive;
36
+ if (!registry && !own && bigFive && Object.keys(bigFive).length) {
37
+ const d = deriveValuesFromBigFive(bigFive);
38
+ const bfMaslow = scoreMaslow(d.values, d.values); // Big Five reaches all 13, nothing frozen
39
+ const sdt = {};
40
+ ["autonomyNeed", "competenceNeed", "relatednessNeed"].forEach((k) => {
41
+ if (typeof d.values[k] === "number")
42
+ sdt[k] = d.values[k];
43
+ });
44
+ const KEYS10 = ["selfDirection", "stimulation", "hedonism", "achievement", "power",
45
+ "security", "conformity", "tradition", "benevolence", "universalism"];
46
+ const ten = {};
47
+ KEYS10.forEach((k) => { ten[k] = d.values[k]; });
48
+ return {
49
+ values: ten,
50
+ // Which Big Five route this is, when the persona says. A bigFive block
51
+ // with no derivation label is treated as stated, because that is what an
52
+ // unlabelled one is: numbers someone put there.
53
+ source: (persona?.valuesDerivation?.method === "bigfive_inferred"
54
+ ? "bigfive_inferred" : "big_five"),
55
+ sdt,
56
+ sdtSource: "derived",
57
+ hypothesisAxes: d.hypothesisAxes,
58
+ higherOrder: {
59
+ openness: round3((ten.selfDirection + ten.stimulation) / 2),
60
+ selfEnhancement: round3((ten.achievement + ten.power) / 2),
61
+ conservation: round3((ten.security + ten.conformity + ten.tradition) / 3),
62
+ selfTranscendence: round3((ten.benevolence + ten.universalism) / 2),
63
+ },
64
+ ...(persona?.valuesDerivation?.method === "bigfive_inferred"
65
+ ? { routeCaveat: "These values come from a Big Five profile a model estimated by reading this persona's description, then the published trait-value correlations. The bridge is well supported; the five inputs are not measured and have no published calibration. Every axis is reachable on this route, which is why it is preferred over the cognitive-trait derivation — but a persona whose Big Five was answered rather than inferred is stronger evidence, and one whose values were stated directly is stronger still." }
66
+ : {}),
67
+ maslowLevel: bfMaslow[0].level,
68
+ maslowSource: "derived",
69
+ maslowBasis: bfMaslow[0].basis,
70
+ maslowMargin: round3(bfMaslow[0].score - bfMaslow[1].score),
71
+ ...(round3(bfMaslow[0].score - bfMaslow[1].score) < 0.05
72
+ ? { maslowCaveat: `Margin over ${bfMaslow[1].level} is ${round3(bfMaslow[0].score - bfMaslow[1].score)}. Too close to read as a finding; treat the top two as tied.` }
73
+ : {}),
74
+ };
75
+ }
76
+ const src = registry ?? own;
77
+ if (!src)
78
+ return { values: null, source: "none" };
79
+ const values = {};
80
+ for (const k of KEYS)
81
+ if (typeof src[k] === "number")
82
+ values[k] = src[k];
83
+ const sdt = {};
84
+ ["autonomyNeed", "competenceNeed", "relatednessNeed"].forEach((k) => {
85
+ const v = src[k];
86
+ if (typeof v === "number")
87
+ sdt[k] = v;
88
+ });
89
+ // Questionnaire-derived personas get SDT computed from answers; AI-generated
90
+ // ones get a flat 0.5/0.5/0.5 that is a placeholder, not a measurement. They
91
+ // were indistinguishable in the payload, so a caller could not tell a
92
+ // derivation from a default -- and 0.5 across all three is the tell.
93
+ const flat = Object.keys(sdt).length === 3 && Object.values(sdt).every((v) => v === 0.5);
94
+ // ONE definition of "this route cannot reach that axis", shared by every
95
+ // rollup, the Maslow scorer and the persuasion layer. Three sites computing
96
+ // it separately is how they ended up with three conventions.
97
+ const routeIsTraits = persona?.valuesDerivation?.method === "traits";
98
+ const hoFrozen = new Set(routeIsTraits
99
+ ? ["selfDirection", "stimulation", "hedonism", "achievement", "power", "security",
100
+ "conformity", "tradition", "benevolence", "universalism",
101
+ "autonomyNeed", "competenceNeed", "relatednessNeed"]
102
+ .filter((k) => !valueAxisCorrelationCounts()[k])
103
+ : []);
104
+ // The four rollups. calculateHigherOrderValues has always existed and was
105
+ // never called: both tools read values.openness and friends straight off the
106
+ // base object, where those keys do not live, so every entry rendered its
107
+ // formula string with an undefined value beside it. (2026-07-31)
108
+ //
109
+ // Three decimals, matching the primaries. At two, selfTranscendence for a
110
+ // persona with benevolence 0.493 and universalism 0.500 is (0.4965) -> 0.5:
111
+ // visually identical to an axis nothing ever touched, which is the exact
112
+ // confusion the primaries went to three decimals to remove. The collision
113
+ // was fixed one layer down and survived one layer up. (2026-08-01)
114
+ //
115
+ // Same exclusion convention as maslowLevel. These imputed until 2026-08-01,
116
+ // which meant the Maslow caveat was arguing against a convention still
117
+ // running three fields away in the same payload: selfEnhancement read
118
+ // (0.698 + 0.5)/2 = 0.599 where the live-only value is 0.698, a 0.1 swing in
119
+ // exactly the direction the caveat warns about, and enough to reorder the
120
+ // four rollups for some personas. One convention, or the disclosure is
121
+ // advice the payload does not take.
122
+ const hoInputs = {
123
+ openness: ["selfDirection", "stimulation"],
124
+ selfEnhancement: ["achievement", "power"],
125
+ conservation: ["security", "conformity", "tradition"],
126
+ selfTranscendence: ["benevolence", "universalism"],
127
+ };
128
+ const higherOrder = {};
129
+ const higherOrderWithImputed = {};
130
+ for (const [roll, ins] of Object.entries(hoInputs)) {
131
+ const live = ins.filter((k) => !hoFrozen.has(k));
132
+ const use = live.length ? live : ins;
133
+ higherOrder[roll] = round3(use.reduce((a, k) => a + values[k], 0) / use.length);
134
+ higherOrderWithImputed[roll] = round3(ins.reduce((a, k) => a + values[k], 0) / ins.length);
135
+ }
136
+ const derivation = persona?.valuesDerivation;
137
+ //
138
+ // ONE vocabulary. What is STORED and what is REPORTED were different
139
+ // spellings of the same concept -- "bigfive"/"traits" on disk against
140
+ // "big_five"/"cognitive_traits" in the payload -- so the same persona
141
+ // answered differently depending on which surface you asked, and telling a
142
+ // genuine route difference from a spelling difference required reading this
143
+ // function. New writes use the ValuesRoute names; the old spellings still
144
+ // read, because rewriting stored personas to fix a label is the overreach
145
+ // this codebase keeps having to undo. (2026-08-01)
146
+ const LEGACY_METHOD = {
147
+ traits: "cognitive_traits",
148
+ bigfive: "big_five",
149
+ };
150
+ const rawMethod = derivation?.method;
151
+ const personaSource = rawMethod
152
+ ? (LEGACY_METHOD[rawMethod] ?? (["stated", "big_five", "bigfive_inferred", "cognitive_traits"].includes(rawMethod)
153
+ ? rawMethod
154
+ : "stated"))
155
+ : "stated";
156
+ // Only the trait route is bounded by the trait correlation table. The Big
157
+ // Five table reaches all thirteen axes, so applying the trait-derived
158
+ // "unpopulated" logic to a big_five persona would report four axes as
159
+ // unreachable that its own route reaches.
160
+ const fromTraits = personaSource === "cognitive_traits";
161
+ const fromDerivation = fromTraits || personaSource === "big_five";
162
+ // Stored on registry profiles; absent from a persona-authored block, where
163
+ // the closest honest answer is the level its own values point at. Which of
164
+ // the two happened is reported rather than left to be guessed.
165
+ const stored = registry?.maslowLevel;
166
+ //
167
+ // This reported a bare category and nothing else, and the category was not
168
+ // stable. For alexa-eden esteem scored 0.5990 and self-actualization 0.6015
169
+ // -- a 0.0025 margin -- so moving the primaries to three decimals flipped
170
+ // the answer with no other input changing. Worse, both of those scores are
171
+ // half-composed of power 0.5 and universalism 0.5, which for a trait-derived
172
+ // persona are UNPOPULATED BASELINES rather than measurements: the level was
173
+ // being decided by axes the route cannot reach.
174
+ //
175
+ // It stays, because a rollup is useful, but it now carries its own arithmetic
176
+ // the way the influence patterns do, and it says out loud when the winner is
177
+ // inside the noise or is resting on axes with no signal. (2026-08-01)
178
+ // Frozen = the route structurally cannot reach the axis (no correlation
179
+ // entry), which is a property of the route rather than of this persona's
180
+ // numbers. Deliberately NOT the `unpopulated` list below: that one also
181
+ // requires the value to sit at baseline, so an axis with correlations whose
182
+ // contributions happened to cancel would be misread as unreachable.
183
+ const maslowScores = scoreMaslow(values, sdt, hoFrozen);
184
+ //
185
+ // A tie is carried BY the value, not beside it.
186
+ //
187
+ // The caveat said "treat the top two as tied" while `level` went on
188
+ // returning one string, so a consumer reading `.level` and not `.caveat` got
189
+ // a coin flip rendered as a determination -- the same defect as the two
190
+ // kinds of 0.50, moved from a number to a category. The numeric case was
191
+ // fixed by adding a decimal so the distinction lives in the value; the
192
+ // categorical equivalent is a value that cannot be mistaken for a single
193
+ // answer. `topScoring` keeps a bare argmax for callers that genuinely need
194
+ // one token. (2026-08-01)
195
+ const MASLOW_TIE = 0.05;
196
+ // ONE rounded margin, used by the label, the tie test and the caveat.
197
+ //
198
+ // These were three separate quantities and they disagreed. Membership
199
+ // filtered on the raw gap to the leader while the label printed the gap to
200
+ // the RUNNER-UP, so a four-level set could be announced as "tied, margin
201
+ // 0.008" with members 0.044 away. And the caveat tested the rounded margin
202
+ // against the same 0.05 the raw filter used, so a persona whose gap rounded
203
+ // up to exactly 0.05 was labelled tied by the string and denied by the prose.
204
+ // A payload that contradicts itself is worse than either answer alone.
205
+ // (2026-08-01)
206
+ const maslowMargin = round3(maslowScores[0].score - maslowScores[1].score);
207
+ const tiedWith = maslowScores.filter((c) => round3(maslowScores[0].score - c.score) <= MASLOW_TIE);
208
+ const maslowTied = tiedWith.map((c) => c.level);
209
+ // The SPAN of the tie set, which is the number that describes the set. The
210
+ // top-two margin describes only its first two members.
211
+ const maslowTieSpan = round3(maslowScores[0].score - tiedWith[tiedWith.length - 1].score);
212
+ const derivedMaslow = maslowTied.length > 1
213
+ ? `${maslowTied.join(" | ")} (tied within ${maslowTieSpan}; next-best margin ${maslowMargin})`
214
+ : maslowScores[0].level;
215
+ // "Unreliable" is the union of two independent problems: too close to call,
216
+ // and decided by an axis carrying no signal. Either one alone is enough.
217
+ //
218
+ // Contamination is reported the same way higherOrderValues reports it, and
219
+ // for the same reason: a rollup over an unpopulated input is not a weaker
220
+ // measurement, it is partly not a measurement. The first version of this
221
+ // caveat blamed the MARGIN, which is the wrong diagnosis -- for alexa-eden
222
+ // both candidates are half composed of a 0.5 placeholder, so a decisive gap
223
+ // between them would not have made the winner a reading either. Composition
224
+ // is reported independently of margin, and it is what the caveat leads with.
225
+ // (2026-08-01)
226
+ //
227
+ // EVERY level reports its input count, including the clean ones. Reporting
228
+ // only the contaminated levels encodes "clean" as an absent key, which is the
229
+ // same shape as the defect this whole field exists to fix: a reader has to
230
+ // know the field could have been there and infer meaning from its absence.
231
+ // Two extra lines make both states explicit. (2026-08-01)
232
+ const maslowContamination = {};
233
+ for (const c of maslowScores) {
234
+ maslowContamination[c.level] = c.frozenInputs.length
235
+ ? `${c.frozenInputs.length} of ${c.inputsTotal} inputs unpopulated (${c.frozenInputs.join(", ")}); scored on the remaining ${c.inputsUsed}`
236
+ : `0 of ${c.inputsTotal} inputs unpopulated`;
237
+ }
238
+ const winnerBad = maslowScores[0].frozenInputs;
239
+ const tooClose = maslowMargin <= MASLOW_TIE;
240
+ const anyFrozen = maslowScores.some((c) => c.frozenInputs.length > 0);
241
+ const maslowCaveat = [
242
+ winnerBad.length
243
+ ? `${maslowScores[0].level} is scored on ${maslowScores[0].inputsUsed} of ${maslowScores[0].inputsTotal} inputs; ${winnerBad.join(", ")} is unpopulated on this route and was excluded rather than imputed.`
244
+ : undefined,
245
+ anyFrozen
246
+ ? `Levels here rest on different numbers of inputs, so the ranking is thinner than it looks. Imputing 0.5 for a missing input would be worse than excluding it: imputation shrinks a contaminated level halfway toward the midpoint, downward when its live input is above 0.5 and upward when below, while leaving fully-populated levels untouched. That reorders the ranking rather than only compressing it.`
247
+ : undefined,
248
+ tooClose
249
+ ? `Margin over ${maslowScores[1].level} is ${maslowMargin}, and ${maslowTied.length} level(s) sit within ${maslowTieSpan}. Too close to read as a finding; treat ${maslowTied.length > 2 ? "all " + maslowTied.length + " as tied" : "the top two as tied"}.`
250
+ : undefined,
251
+ ].filter(Boolean).join(" ") || undefined;
252
+ // A persona's values can be authored by hand or written by the derivation.
253
+ // Both live in the same field, and a value of exactly 0.5 means different
254
+ // things in each case: a deliberate midpoint, or the untouched baseline the
255
+ // derivation starts from. Anything treating values as evidence needs to be
256
+ // able to tell them apart. (2026-07-31)
257
+ //
258
+ // Named for the ROUTE that determined the numbers, because that is the
259
+ // question a caller has. "derived" answered a different one: it covered both
260
+ // the Big Five derivation and the cognitive-trait derivation, which are the
261
+ // two routes that differ most in how far you should trust them -- one is
262
+ // published correlation, the other is our own bridge across a gap the
263
+ // literature does not cover. A field that collapses them cannot express the
264
+ // ordering the whole three-route account exists to make. (2026-08-01)
265
+ // Axes the trait derivation cannot move.
266
+ //
267
+ // Four of the thirteen -- hedonism, power, universalism, relatednessNeed --
268
+ // have no entry in the trait-value correlation table at all, so a derived
269
+ // profile leaves them at the 0.5 baseline no matter what the traits say.
270
+ // That is not a measurement of a midpoint, and anything running with
271
+ // useValues:true will differentiate on the other nine only. selfTranscendence
272
+ // is a rollup of benevolence and universalism, so it is pulled toward 0.5 by
273
+ // an input that was never populated. Stated rather than left to be inferred
274
+ // from a suspicious row of 0.5s. (2026-07-31)
275
+ // Which table has links to an axis depends on WHICH ROUTE produced the
276
+ // numbers. Using the trait counts on a Big Five route asks the wrong table:
277
+ // power has no trait link, so a genuinely cancelled power reads as having no
278
+ // inputs at all, and the cancellation goes unreported on the one route where
279
+ // it is the only thing a 0.5 can mean.
280
+ const traitCounts = valueAxisCorrelationCounts();
281
+ const bigFiveAxes = new Set(bigFiveReachableAxes());
282
+ const counts = fromTraits
283
+ ? traitCounts
284
+ : Object.fromEntries([...bigFiveAxes].map((a) => [a, 1]));
285
+ const atBaseline = (k) => values[k] === 0.5 || sdt[k] === 0.5;
286
+ const unpopulated = fromTraits
287
+ ? Object.keys({ ...values, ...sdt }).filter((k) => atBaseline(k) && !counts[k])
288
+ : [];
289
+ // Axes that ARE derived but whose contributions cancelled. A different fact
290
+ // from an axis with no correlations, and the two are indistinguishable by
291
+ // value alone -- both read 0.5.
292
+ // Which of the two zero-net shapes each axis is: a lone neutral input, or
293
+ // opposing inputs that cancelled. Reported rather than assumed.
294
+ const bigFiveScores = persona?.bigFive;
295
+ const traitsForShape = getCognitiveProfile(persona)?.traits;
296
+ const netZeroShape = (axis) => {
297
+ // The explanation has to name the table the ROUTE actually used. Reporting
298
+ // "no contributing trait" for a Big Five persona describes a table that had
299
+ // nothing to do with its numbers, which is the same defect as a formula
300
+ // string that does not match its formula.
301
+ if (!fromTraits) {
302
+ const bfLinks = BIG_FIVE_VALUE_LINKS.filter((l) => l.value === axis);
303
+ const live = bfLinks.filter((l) => typeof bigFiveScores?.[l.factor] === "number");
304
+ if (live.length === 1) {
305
+ return `${axis} has a single contributing factor (${live[0].factor}) sitting at the midpoint — nothing opposed it, nothing pushed it`;
306
+ }
307
+ if (live.length > 1) {
308
+ // "Cancel" claims opposing non-zero forces. Two factors both sitting AT
309
+ // the midpoint contribute exactly nothing each -- nothing opposed
310
+ // anything, so calling it cancellation describes a mechanism that did
311
+ // not occur. Same distinction as the single-input case, one level up.
312
+ // (2026-08-01)
313
+ const contributions = live.map((l) => ({
314
+ l, c: ((bigFiveScores[l.factor] - 0.5) * l.weight * (l.direction === "positive" ? 1 : -1)),
315
+ }));
316
+ const label = live.map((l) => (l.direction === "positive" ? "+" : "-") + l.factor).join(", ");
317
+ return contributions.every((x) => Math.abs(x.c) < 1e-9)
318
+ ? `${axis} draws on ${live.length} factors (${label}), every one of which sits at the midpoint and so contributes nothing — not opposing forces, just no signal`
319
+ : `${axis} draws on ${live.length} factors (${label}) whose contributions cancel`;
320
+ }
321
+ return `${axis} has no contributing Big Five factor on this persona`;
322
+ }
323
+ // Traits whose correlation table entry targets this axis AND which this
324
+ // persona actually carries a number for.
325
+ const contributing = Object.entries(TRAIT_VALUE_CORRELATIONS)
326
+ .filter(([trait, c]) => c.affects.some((a) => a.value === axis)
327
+ && typeof traitsForShape?.[trait] === "number")
328
+ .map(([trait]) => trait);
329
+ if (contributing.length === 1) {
330
+ return `${axis} has a single contributing trait (${contributing[0]}) sitting at the midpoint — nothing opposed it, nothing pushed it`;
331
+ }
332
+ if (contributing.length > 1) {
333
+ return `${axis} has ${contributing.length} contributing traits (${contributing.join(", ")}) whose pulls cancel`;
334
+ }
335
+ return `${axis} has no contributing trait carrying a number on this persona`;
336
+ };
337
+ // Cancellation is reportable on EVERY derived route, not just the trait one.
338
+ //
339
+ // Gating this on fromTraits left the Big Five routes with no way to tell a
340
+ // cancelled axis from an untouched one -- the exact ambiguity netNudge exists
341
+ // to remove, reappearing on a newer route. It shows up immediately in
342
+ // practice: extraversion 0.33 and agreeableness 0.33 contribute +0.085 and
343
+ // -0.085 to power, which cancel to exactly 0, and 0 is the code for "nothing
344
+ // targets this axis". Nothing is UNPOPULATED on a Big Five route (all
345
+ // thirteen are reachable), so a 0.5 there can only be a cancellation, and
346
+ // saying so is strictly more information than silence. (2026-08-01)
347
+ const netZero = fromDerivation
348
+ ? Object.keys({ ...values, ...sdt }).filter((k) => atBaseline(k) && !!counts[k])
349
+ : [];
350
+ // The signed evidence behind each axis, recovered from the squash.
351
+ //
352
+ // Three decimals separate a derived 0.495 from an untouched 0.5, but only
353
+ // for axes that happen to land off the midpoint. The nudge separates them
354
+ // always: an axis nothing targets has exactly 0, an axis whose inputs
355
+ // cancelled has a small non-zero. Beside every value rather than in a
356
+ // separate list someone has to think to read.
357
+ const netNudge = fromDerivation
358
+ ? Object.fromEntries(Object.entries({ ...values, ...sdt }).map(([k, v]) => {
359
+ // invert 0.5 + 0.5*tanh(raw) -> raw
360
+ // Clamp MAGNITUDE, keep sign. Flooring at a positive epsilon turned
361
+ // every below-midpoint axis into 0 — security and conformity, both
362
+ // genuinely derived, reported the same "nothing targets it" as power.
363
+ // The bug this field exists to prevent, reintroduced inside the field.
364
+ const t = (v - 0.5) * 2;
365
+ const clamped = Math.sign(t) * Math.min(0.999999, Math.abs(t));
366
+ const raw = 0.5 * Math.log((1 + clamped) / (1 - clamped));
367
+ return [k, Math.round(raw * 1000) / 1000];
368
+ }))
369
+ : undefined;
370
+ return {
371
+ values,
372
+ source: personaSource,
373
+ storedIn: registry ? "registry" : "persona",
374
+ ...(unpopulated.length
375
+ ? {
376
+ unpopulatedAxes: unpopulated,
377
+ unpopulatedNote: `These axes have no trait correlation defined, so the derivation leaves them at the 0.5 baseline regardless of the persona's traits. They carry no signal; a values-weighted run differentiates on the others only.`,
378
+ }
379
+ : {}),
380
+ ...(netNudge ? { netNudge, netNudgeNote: "Signed evidence behind each axis before the squash. Exactly 0 means no trait targets it at all; a small non-zero means the contributions cancelled. Both can round to 0.5.",
381
+ // Named, not left to be recovered by fitting. A reader checked the
382
+ // disclosure by inferring this transform from thirteen (nudge, value)
383
+ // pairs, which worked -- and is exactly the work the disclosure exists
384
+ // to remove. (2026-08-01)
385
+ valueTransform: "value = 0.5 + tanh(netNudge) / 2, applied per axis, then rounded to 3dp. Invertible: netNudge = atanh((value - 0.5) * 2)." } : {}),
386
+ ...(netZero.length
387
+ ? {
388
+ netZeroAxes: netZero,
389
+ // The note asserted OPPOSING FORCES ("pull them both ways"), which is
390
+ // only one of the two ways an axis lands at zero net. The other, and
391
+ // the more common one, is a single input sitting at exactly 0.5 —
392
+ // chen-wei's benevolence has one link (trustCalibration 0.5) and its
393
+ // autonomyNeed has one (selfEfficacy 0.5). Nothing cancelled; nothing
394
+ // pushed. Stating a mechanism the numbers do not show is the same
395
+ // defect as a value that overstates its own precision, so the note
396
+ // now reports WHICH case each axis is. (2026-08-01)
397
+ netZeroNote: `These axes ARE derived — the correlations for this route exist and were applied — but the net evidence lands at zero, so they sit at the baseline anyway. ${netZero.map(netZeroShape).join("; ")}. Reading 0.5 here means "measured, no net lean", which is not the same as the unpopulated axes above.`,
398
+ }
399
+ : {}),
400
+ // Which rollups inherit a baseline input. openness can be clean while
401
+ // selfTranscendence is half synthetic, and the composite hides that.
402
+ ...(unpopulated.length
403
+ ? (() => {
404
+ const inputs = {
405
+ openness: ["selfDirection", "stimulation"],
406
+ selfEnhancement: ["achievement", "power"],
407
+ conservation: ["security", "conformity", "tradition"],
408
+ selfTranscendence: ["benevolence", "universalism"],
409
+ };
410
+ // Clean rollups say so rather than being omitted. Encoding "clean"
411
+ // as a missing key makes the reader infer meaning from absence --
412
+ // the same shape as the 0.5 ambiguity this field exists to remove.
413
+ const contaminated = {};
414
+ for (const [roll, ins] of Object.entries(inputs)) {
415
+ const bad = ins.filter((i) => unpopulated.includes(i));
416
+ contaminated[roll] = bad.length
417
+ ? `${bad.length} of ${ins.length} inputs unpopulated (${bad.join(", ")})`
418
+ : `0 of ${ins.length} inputs unpopulated`;
419
+ }
420
+ return { higherOrderContamination: contaminated };
421
+ })()
422
+ : {}),
423
+ ...(Object.keys(sdt).length ? { sdt, sdtSource: flat ? "default" : "derived" } : {}),
424
+ higherOrder,
425
+ higherOrderWithImputed,
426
+ imputationPolicy: hoFrozen.size
427
+ ? `Axes this route cannot reach (${[...hoFrozen].join(", ")}) are EXCLUDED from every mean rather than imputed at 0.5. Imputing the midpoint shrinks a contaminated composite toward 0.5 while leaving a fully-populated one untouched, which reorders rankings instead of only widening them. Applies identically to higherOrder, maslowLevel and influencePatterns; the pre-change imputed figures are kept alongside each.`
428
+ : "Nothing is imputed on this route: every axis is reachable.",
429
+ maslowLevel: stored ?? derivedMaslow,
430
+ maslowSource: stored ? "stored" : "derived",
431
+ // Tie facts ship even when the level is STORED. A stored maslowLevel is an
432
+ // authored answer and stays authoritative, but gating this whole block on
433
+ // it meant the 21 builtin and accessibility personas never got any of it.
434
+ // color-blind-deuteranopia is the proof: all ten Schwartz values and all
435
+ // three SDT needs are exactly 0.5, so every level scores exactly 0.5 -- a
436
+ // four-way tie at zero margin -- and it reported a bare "esteem". The one
437
+ // persona where a single winner is guaranteed meaningless was the one
438
+ // reporting it without qualification. (2026-08-01)
439
+ maslowBasis: maslowScores[0].basis,
440
+ maslowTopScoring: maslowScores[0].level,
441
+ ...(maslowTied.length > 1 ? { maslowTied, maslowTieSpan } : {}),
442
+ ...(stored && maslowTied.length > 1
443
+ ? { maslowStoredNote: `maslowLevel here is an authored value. Computed from this persona's own numbers, ${maslowTied.length} levels tie within ${maslowTieSpan} (${maslowTied.join(", ")}), so the stored answer is a choice among them rather than a result.` }
444
+ : {}),
445
+ maslowRunnerUp: `${maslowScores[1].level} ${maslowScores[1].score} = ${maslowScores[1].basis}`,
446
+ maslowMargin: maslowMargin,
447
+ ...(Object.keys(maslowContamination).length ? { maslowContamination } : {}),
448
+ ...(maslowCaveat ? { maslowCaveat } : {}),
449
+ };
450
+ }
451
+ const round2 = (n) => Math.round(n * 100) / 100;
452
+ const round3 = (n) => Math.round(n * 1000) / 1000;
11
453
  /**
12
- * Register values system tools (7 tools)
454
+ * Score the four Maslow levels from the value axes, worst-first sorted.
455
+ *
456
+ * Module-level because the Big Five branch used to return `maslowLevel:
457
+ * "esteem"` as a literal -- the same answer for every persona that took that
458
+ * route, derived from nothing. A shared scorer is the only way one path cannot
459
+ * quietly disagree with the other. (2026-08-01)
13
460
  */
461
+ function scoreMaslow(values, sdt, frozen = new Set()) {
462
+ const spec = [
463
+ ["safety", ["security", "conformity"]],
464
+ ["belonging", ["benevolence", "relatednessNeed"]],
465
+ ["esteem", ["achievement", "power"]],
466
+ ["self-actualization", ["selfDirection", "universalism"]],
467
+ ];
468
+ const pick = (k) => values[k] ?? sdt[k] ?? 0.5;
469
+ return spec.map(([level, axes]) => {
470
+ const live = axes.filter((k) => !frozen.has(k));
471
+ const dead = axes.filter((k) => frozen.has(k));
472
+ // Live inputs only. Averaging a live axis against a frozen 0.5 does not
473
+ // merely add noise -- it shrinks that level halfway to the midpoint, DOWN
474
+ // when the live input is above 0.5 and UP when it is below, while a level
475
+ // with no frozen input is not shrunk at all. So contamination reorders the
476
+ // ranking as well as compressing it, and the direction depends on the sign
477
+ // of each level's live input. That is imputation-with-shrinkage presented
478
+ // as a complete case. Dropping the imputed inputs removes the bias; it
479
+ // cannot remove the fact that some levels rest on fewer inputs, which is
480
+ // what `inputsUsed` is for. (2026-08-01)
481
+ const denom = live.length || axes.length;
482
+ const used = live.length ? live : axes;
483
+ return {
484
+ level,
485
+ score: round3(used.reduce((a, k) => a + pick(k), 0) / denom),
486
+ scoreWithImputed: round3(axes.reduce((a, k) => a + pick(k), 0) / axes.length),
487
+ basis: `(${used.map((k) => `${k} ${pick(k)}`).join(" + ")}) / ${denom}`,
488
+ inputsUsed: live.length,
489
+ inputsTotal: axes.length,
490
+ frozenInputs: dead,
491
+ axes,
492
+ };
493
+ }).sort((a, b) => b.score - a.score);
494
+ }
495
+ /**
496
+ * The persona_values_lookup payload, extracted from the tool handler so it can
497
+ * be asserted directly.
498
+ *
499
+ * It lived inline, which meant the only way to see what the tool actually
500
+ * returns was to run an MCP server -- and that is precisely why six resolver
501
+ * fields shipped computed-but-undelivered in one day. A payload no test can
502
+ * reach is a payload nothing checks. (2026-08-01)
503
+ */
504
+ export async function buildValuesPayload(persona, includeInfluencePatterns = true) {
505
+ const resolved = resolvePersonaValues(persona);
506
+ // Keyed on "the resolver found something", not on an enumerated list of
507
+ // sources. This read `source === "persona"` and then a later change
508
+ // added "derived" for personas whose values the derivation wrote --
509
+ // which is every regenerated custom persona -- so they stopped matching
510
+ // and fell back into the not-found error the fallback exists to prevent.
511
+ // A condition that enumerates the valid cases has to be revisited every
512
+ // time a case is added; "not none" does not. (2026-07-31)
513
+ const values = getPersonaValues(persona) ?? (resolved.source !== "none"
514
+ ? resolved.values
515
+ : undefined);
516
+ if (!values) {
517
+ const availablePersonas = PERSONA_VALUE_PROFILES.map(p => p.personaName);
518
+ return {
519
+ error: `No values profile found for persona: ${persona}`,
520
+ availablePersonas,
521
+ // These are the registry's OWN keys, and several differ from
522
+ // the names every other tool advertises -- "adhd" here against
523
+ // "cognitive-adhd" in empathy_audit, "motor-tremor" against
524
+ // "motor-impairment-tremor". Both forms resolve, because
525
+ // resolvePersonaName aliases them, but a list that teaches the
526
+ // internal vocabulary sends a caller to a different name than
527
+ // the rest of the surface uses. Say so rather than let the
528
+ // list imply these are the canonical names. (2026-07-31)
529
+ availablePersonasNote: "Registry keys. The longer names used by empathy_audit and the persona tools (cognitive-adhd, motor-impairment-tremor, low-vision-magnified) alias onto these and resolve fine. list_cognitive_personas is the authoritative roster.",
530
+ note: "Values come from this registry, or from a schwartzValues block on a custom persona's own file — persona_lookup reads both. Custom personas can also have values added via the questionnaire.",
531
+ };
532
+ }
533
+ const profile = PERSONA_VALUE_PROFILES.find(p => p.personaName.toLowerCase() === persona.toLowerCase());
534
+ let influencePatterns;
535
+ let influencePatternsTotal;
536
+ let influencePatternsOmitted;
537
+ let influencePatternsOmittedNote;
538
+ if (includeInfluencePatterns) {
539
+ // Traits passed through, so patterns sharing a value target set can
540
+ // differ and a trait named for its pattern actually reaches it.
541
+ const rankTraits = getCognitiveProfile(getAnyPersona(persona))?.traits;
542
+ const ranked = rankInfluencePatternsForProfile(values, rankTraits, new Set(resolved.unpopulatedAxes ?? []));
543
+ // A silent top-7 cut made a mapped pattern look unmapped: social_proof
544
+ // ranks 10th for some personas and simply vanished, with nothing in the
545
+ // response distinguishing "scored low and truncated" from "this trait
546
+ // never reaches the pattern list". The count and the omitted names are
547
+ // reported now. (2026-07-31)
548
+ influencePatternsTotal = ranked.length;
549
+ influencePatternsOmitted = ranked.slice(7).map(r => r.pattern.name);
550
+ // The cut is applied AFTER scoring, so the patterns the imputation
551
+ // policy moves most are systematically the ones most likely to be
552
+ // truncated away: scarcity and liking both target an axis this route
553
+ // cannot reach, which drags them down the ranking, which pushes them
554
+ // past the cut. On a low-achievement persona the two patterns whose
555
+ // scores changed are precisely the two the caller never sees. Naming
556
+ // them in the omission note is the difference between "these ranked
557
+ // low" and "these ranked low partly because of a policy stated
558
+ // elsewhere in this same payload". (2026-08-01)
559
+ const omittedContaminated = ranked.slice(7)
560
+ .filter(r => r.pattern.targetValues.some(v => (resolved.unpopulatedAxes ?? []).includes(v)))
561
+ .map(r => r.pattern.name);
562
+ if (omittedContaminated.length) {
563
+ influencePatternsOmittedNote = `${omittedContaminated.join(", ")} ${omittedContaminated.length === 1 ? "was" : "were"} cut AND ${omittedContaminated.length === 1 ? "is" : "are"} scored on axes this route cannot reach. The ranking is computed before the cut, so a pattern held down by an unpopulated input is more likely to fall past it — the patterns the imputation policy affects most are the ones you are least likely to see.`;
564
+ }
565
+ // Each score's inputs, so a rank can be traced instead of guessed at.
566
+ // commitment topping the list was untraceable from the output: the
567
+ // reader could see 0.71 and had to reverse-engineer which values and
568
+ // which traits produced it.
569
+ // Unpopulated inputs marked INSIDE the basis, not only counted elsewhere.
570
+ // The blog widget already badged these ("scarcity - 1 of 3 frozen") while
571
+ // the API handed back a bare `power=0.5` that reads as a measured
572
+ // midpoint. The web UI was more honest than the tool output, which is
573
+ // backwards: the tool output is the one consumed programmatically, by a
574
+ // reader with no page to look at. (2026-08-01)
575
+ const frozen = new Set(resolved.unpopulatedAxes ?? []);
576
+ const vAll = values;
577
+ influencePatterns = ranked.slice(0, 7).map(r => {
578
+ const badValues = r.pattern.targetValues.filter((v) => frozen.has(v));
579
+ // The value mean is taken over DEFINED targets only, and the trait
580
+ // half is dropped entirely when a pattern has no related traits or
581
+ // none of them are on this persona (value-mappings.ts:609,618). So
582
+ // the weighting is 60/40 or 100/0 depending on the pattern, and the
583
+ // string said 60/40 unconditionally.
584
+ const definedValues = r.pattern.targetValues.filter((v) => typeof vAll[v] === "number");
585
+ const liveTraits = (r.pattern.relatedTraits ?? []).filter((t) => typeof rankTraits?.[t.trait] === "number");
586
+ const valueWeight = liveTraits.length ? 0.6 : 1;
587
+ // The fraction of the FINAL score that is placeholder, not the
588
+ // fraction of the value inputs. One frozen value out of three is a
589
+ // third of the value mean, which is 0.6 x 1/3 = 20% of the score --
590
+ // not 33%. A post about not overstating precision should not
591
+ // overstate its own contamination. (2026-08-01)
592
+ const syntheticShare = definedValues.length
593
+ ? valueWeight * (badValues.length / definedValues.length)
594
+ : 0;
595
+ return {
596
+ basis: {
597
+ values: r.pattern.targetValues.map((v) => `${v}=${vAll[v] ?? "n/a"}${frozen.has(v) ? " (unpopulated baseline, not a measurement)" : ""}`),
598
+ traits: (r.pattern.relatedTraits ?? []).map((t) => `${t.trait}=${rankTraits?.[t.trait] ?? "n/a"}${t.direction === "negative" ? " (inverted)" : ""}`),
599
+ // "defined" was accurate about the CHECK and wrong about the
600
+ // result: an axis this route cannot reach is defined -- it is
601
+ // sitting at 0.5 -- so the mean silently included placeholders
602
+ // while the string implied it did not. Says what it does now.
603
+ formula: liveTraits.length
604
+ ? "susceptibility = 0.6 * mean(target values that are populated on this route) + 0.4 * mean(related traits, inverted where direction is negative), rounded to 3dp"
605
+ : "susceptibility = mean(target values that are populated on this route)",
606
+ weighting: liveTraits.length
607
+ ? "60% value mean, 40% trait mean"
608
+ : "100% value mean (this pattern contributed no traits, so the 40% trait term is not applied)",
609
+ unpopulatedInputs: badValues.length
610
+ ? `${badValues.length} of ${definedValues.length} value inputs unpopulated (${badValues.join(", ")}) and EXCLUDED from the mean, so the score rests on ${definedValues.length - badValues.length}. Previously they were averaged in at 0.5, which at ${Math.round(valueWeight * 100)}% value weighting made ${Math.round(syntheticShare * 1000) / 10}% of the score placeholder.`
611
+ : `0 of ${definedValues.length} value inputs unpopulated`,
612
+ ...(r.susceptibilityWithImputed !== r.susceptibility
613
+ ? { susceptibilityWithImputed: r.susceptibilityWithImputed }
614
+ : {}),
615
+ },
616
+ pattern: r.pattern.name,
617
+ susceptibility: r.susceptibility,
618
+ description: r.pattern.description,
619
+ };
620
+ });
621
+ }
622
+ return {
623
+ persona,
624
+ rationale: profile?.rationale,
625
+ schwartzValues: {
626
+ selfDirection: { value: values.selfDirection, meaning: "Independent thought, creativity, freedom" },
627
+ stimulation: { value: values.stimulation, meaning: "Excitement, novelty, challenge" },
628
+ hedonism: { value: values.hedonism, meaning: "Pleasure, sensuous gratification" },
629
+ achievement: { value: values.achievement, meaning: "Personal success through competence" },
630
+ power: { value: values.power, meaning: "Social status, prestige, control" },
631
+ security: { value: values.security, meaning: "Safety, harmony, stability" },
632
+ conformity: { value: values.conformity, meaning: "Restraint of actions that harm others" },
633
+ tradition: { value: values.tradition, meaning: "Respect for customs, heritage" },
634
+ benevolence: { value: values.benevolence, meaning: "Welfare of close others" },
635
+ universalism: { value: values.universalism, meaning: "Tolerance, social justice, environment" },
636
+ },
637
+ higherOrderValues: {
638
+ openness: { value: resolved.higherOrder?.openness, meaning: "(selfDirection + stimulation) / 2" },
639
+ selfEnhancement: { value: resolved.higherOrder?.selfEnhancement, meaning: "(achievement + power) / 2" },
640
+ conservation: { value: resolved.higherOrder?.conservation, meaning: "(security + conformity + tradition) / 3" },
641
+ selfTranscendence: { value: resolved.higherOrder?.selfTranscendence, meaning: "(benevolence + universalism) / 2" },
642
+ },
643
+ // From the resolver, which carries the SDT numbers whichever
644
+ // source they came from. Reading them off `values` returned
645
+ // undefined on the persona-file path -- the block rendered three
646
+ // "meaning" strings with no values attached.
647
+ // Values exist here but the persona does not, so nothing can
648
+ // actually be RUN as it. Eight registry keys are in this state;
649
+ // finding that out previously meant diffing this tool's output
650
+ // against list_cognitive_personas by hand.
651
+ ...(getAnyPersona(persona)
652
+ ? {}
653
+ : { runnable: false,
654
+ runnableNote: `A values profile exists for "${persona}" but no persona does, so it cannot be used by empathy_audit, cognitive_journey or any other tool that runs AS a persona. Values data without a persona behind it.` }),
655
+ ...(resolved.sdtSource ? { selfDeterminationSource: resolved.sdtSource } : {}),
656
+ selfDeterminationTheory: {
657
+ autonomyNeed: { value: resolved.sdt?.autonomyNeed ?? values.autonomyNeed, meaning: "Need for choice and control" },
658
+ competenceNeed: { value: resolved.sdt?.competenceNeed ?? values.competenceNeed, meaning: "Need to feel capable" },
659
+ relatednessNeed: { value: resolved.sdt?.relatednessNeed ?? values.relatednessNeed, meaning: "Need for connection" },
660
+ },
661
+ maslowLevel: {
662
+ level: resolved.maslowLevel ?? values.maslowLevel,
663
+ source: resolved.maslowSource,
664
+ meaning: (resolved.maslowLevel ?? values.maslowLevel) === "physiological" ? "Basic survival needs"
665
+ : (resolved.maslowLevel ?? values.maslowLevel) === "safety" ? "Security and stability"
666
+ : (resolved.maslowLevel ?? values.maslowLevel) === "belonging" ? "Social connection and love"
667
+ : (resolved.maslowLevel ?? values.maslowLevel) === "esteem" ? "Achievement and recognition"
668
+ : "Self-fulfillment and growth",
669
+ // The level used to arrive bare. It is a rollup of four axes
670
+ // that can be a coin-flip apart, so it now ships its own
671
+ // arithmetic and says when the winner is not a finding.
672
+ ...(resolved.maslowBasis ? { basis: resolved.maslowBasis } : {}),
673
+ ...(resolved.maslowTopScoring ? { topScoring: resolved.maslowTopScoring } : {}),
674
+ ...(resolved.maslowTied ? { tied: resolved.maslowTied } : {}),
675
+ ...(resolved.maslowTieSpan !== undefined ? { tieSpan: resolved.maslowTieSpan } : {}),
676
+ ...(resolved.maslowStoredNote ? { storedNote: resolved.maslowStoredNote } : {}),
677
+ ...(resolved.maslowRunnerUp ? { runnerUp: resolved.maslowRunnerUp } : {}),
678
+ ...(resolved.maslowMargin !== undefined ? { marginOverRunnerUp: resolved.maslowMargin } : {}),
679
+ // Same contract as higherOrderContamination, and surfaced in the
680
+ // same breath as the fields it qualifies -- adding it to the
681
+ // resolver and forgetting it here is the exact defect this file
682
+ // has now produced four times.
683
+ ...(resolved.maslowContamination ? { contamination: resolved.maslowContamination } : {}),
684
+ ...(resolved.maslowCaveat ? { caveat: resolved.maslowCaveat } : {}),
685
+ },
686
+ influencePatterns,
687
+ ...(influencePatternsTotal !== undefined && influencePatternsOmitted?.length
688
+ ? {
689
+ influencePatternsShown: influencePatterns?.length,
690
+ ...(influencePatternsOmittedNote ? { influencePatternsOmittedNote } : {}),
691
+ influencePatternsTotal,
692
+ influencePatternsOmitted,
693
+ influencePatternsNote: "Ranked by susceptibility and cut to the top seven. The omitted names ARE mapped and scored -- they ranked below the cut, which is different from a pattern the persona's traits never reach.",
694
+ }
695
+ : {}),
696
+ // The Schwartz block arrived unattributed here while
697
+ // persona_lookup labelled the same values. Provenance should not
698
+ // depend on which tool you asked.
699
+ valuesSource: resolved.source,
700
+ ...(resolved.routeCaveat ? { routeCaveat: resolved.routeCaveat } : {}),
701
+ ...(resolved.higherOrderWithImputed ? { higherOrderWithImputed: resolved.higherOrderWithImputed } : {}),
702
+ ...(resolved.imputationPolicy ? { imputationPolicy: resolved.imputationPolicy } : {}),
703
+ ...(resolved.storedIn ? { valuesStoredIn: resolved.storedIn } : {}),
704
+ ...(resolved.unpopulatedAxes
705
+ ? { unpopulatedAxes: resolved.unpopulatedAxes, unpopulatedNote: resolved.unpopulatedNote }
706
+ : {}),
707
+ // Computed in the resolver since it was written and dropped on the
708
+ // floor here, so the field built to disambiguate a 0.5 never
709
+ // reached a single caller. Third producer with no consumer found
710
+ // in this file. (2026-08-01)
711
+ ...(resolved.netNudge
712
+ ? { netNudge: resolved.netNudge, netNudgeNote: resolved.netNudgeNote,
713
+ ...(resolved.valueTransform ? { valueTransform: resolved.valueTransform } : {}) }
714
+ : {}),
715
+ ...(resolved.netZeroAxes
716
+ ? { netZeroAxes: resolved.netZeroAxes, netZeroNote: resolved.netZeroNote }
717
+ : {}),
718
+ ...(resolved.higherOrderContamination
719
+ ? { higherOrderContamination: resolved.higherOrderContamination }
720
+ : {}),
721
+ researchBasis: {
722
+ schwartz: "Schwartz, S. H. (1992, 2012). Theory of Basic Human Values. DOI: 10.1016/S0065-2601(08)60281-6",
723
+ sdt: "Deci, E. L., & Ryan, R. M. (1985, 2000). Self-Determination Theory. DOI: 10.1037/0003-066X.55.1.68",
724
+ maslow: "Maslow, A. H. (1943). A Theory of Human Motivation. DOI: 10.1037/h0054346",
725
+ },
726
+ };
727
+ }
728
+ /** Test-facing alias; same function, named so its purpose is obvious in specs. */
729
+ export const buildValuesPayloadForTest = buildValuesPayload;
14
730
  export function registerValuesTools(server) {
15
731
  server.registerTool("persona_values_list", {
16
732
  title: "List Persona Values",
@@ -70,6 +786,148 @@ export function registerValuesTools(server) {
70
786
  ],
71
787
  };
72
788
  });
789
+ /**
790
+ * Whole-persona lookup: traits, values, accessibility and demographics in one
791
+ * call, and the payload the persona view renders.
792
+ *
793
+ * persona_values_lookup stays registered rather than being renamed into this.
794
+ * It is a published tool name that connectors already call, and renaming it
795
+ * in the same deploy as the replacement would break every existing caller --
796
+ * the expand half of expand-contract. It can be retired once nothing calls it.
797
+ */
798
+ server.registerTool("persona_lookup", {
799
+ title: "Look Up Persona",
800
+ description: "Look up a complete persona: cognitive traits, Schwartz values, accessibility traits and demographics. Renders as an interactive profile with per-trait meters. Use this instead of persona_values_lookup when you want the whole persona rather than only its values.",
801
+ inputSchema: {
802
+ persona: z.string().describe("Persona name (e.g., 'first-timer', 'power-user', 'anxious-user')"),
803
+ },
804
+ annotations: {
805
+ title: "Look Up Persona",
806
+ readOnlyHint: true,
807
+ destructiveHint: false,
808
+ idempotentHint: true,
809
+ openWorldHint: false,
810
+ },
811
+ _meta: { ui: { resourceUri: widgetUri("persona") } },
812
+ }, async ({ persona }) => {
813
+ const p = getAnyPersona(persona);
814
+ if (!p) {
815
+ return {
816
+ content: [{ type: "text", text: JSON.stringify({
817
+ error: `No persona found: ${persona}`,
818
+ hint: "Call list_cognitive_personas for the authoritative roster.",
819
+ }, null, 2) }],
820
+ isError: true,
821
+ };
822
+ }
823
+ const profile = getCognitiveProfile(p);
824
+ const values = getPersonaValues(p.name);
825
+ const rec = p;
826
+ // Flattened to plain name->number maps because that is what the meters
827
+ // read. The nested {value, meaning} shape values_lookup returns is fine
828
+ // for prose and useless for a bar.
829
+ const payload = {
830
+ name: p.name,
831
+ description: p.description ?? "",
832
+ demographics: rec.demographics ?? {},
833
+ traits: profile?.traits ?? {},
834
+ // One resolver, shared with persona_values_lookup, so the two tools
835
+ // cannot answer the same question differently again.
836
+ ...(() => {
837
+ const r = resolvePersonaValues(p.name);
838
+ if (r.source === "none") {
839
+ return {
840
+ values: null,
841
+ valuesSource: "none",
842
+ valuesNote: "No Schwartz values for this persona. They come from a hand-authored registry, or from a schwartzValues block on the persona itself; this persona has neither. Anything weighting by values is running on defaults for it.",
843
+ };
844
+ }
845
+ return {
846
+ values: r.values,
847
+ valuesSource: r.source,
848
+ ...(r.sdt ? { selfDeterminationNeeds: r.sdt, selfDeterminationSource: r.sdtSource } : {}),
849
+ ...(r.unpopulatedAxes ? { unpopulatedAxes: r.unpopulatedAxes, unpopulatedNote: r.unpopulatedNote } : {}),
850
+ ...(r.netZeroAxes ? { netZeroAxes: r.netZeroAxes, netZeroNote: r.netZeroNote } : {}),
851
+ ...(r.higherOrderContamination ? { higherOrderContamination: r.higherOrderContamination } : {}),
852
+ ...(r.higherOrder ? { higherOrderValues: r.higherOrder } : {}),
853
+ ...(r.maslowLevel ? { maslowLevel: r.maslowLevel, maslowSource: r.maslowSource } : {}),
854
+ ...(r.sdtSource === "default"
855
+ ? { selfDeterminationNote: "All three needs read 0.5, which is the placeholder AI-generated personas ship with rather than a measurement. Questionnaire-derived personas carry computed values here." }
856
+ : {}),
857
+ };
858
+ })(),
859
+ ...(rec.accessibility_traits ? { accessibility_traits: rec.accessibility_traits } : {}),
860
+ ...(rec.accessibilityTraits ? { accessibility_traits: rec.accessibilityTraits } : {}),
861
+ ...(profile?.attentionPattern ? { attentionPattern: profile.attentionPattern } : {}),
862
+ // NOT a truthiness guard. `decisionStyle` is null when the margin
863
+ // between the top two styles is below threshold, and a truthiness
864
+ // guard drops the key entirely on exactly that case -- so the payload
865
+ // shipped `decisionStyleSource: "suppressed"` describing a field it
866
+ // had omitted, and a consumer could not tell suppression from "never
867
+ // computed". Emit the null. (2026-08-03, BUG-08)
868
+ ...(profile && "decisionStyle" in profile ? { decisionStyle: profile.decisionStyle ?? null } : {}),
869
+ ...(profile?.decisionStyleSuppressed ? {
870
+ decisionStyleSuppressed: true,
871
+ decisionStyleThreshold: profile.decisionStyleThreshold,
872
+ decisionStyleNote: "The top two decision styles were separated by less than the threshold, so no label is claimed. decisionStyleMargin below is the gap that fell short.",
873
+ } : {}),
874
+ // Where those two came from. "default" means no rule matched and no
875
+ // value was declared -- it is the initial literal, not a reading of
876
+ // this persona, and it should not be acted on as one.
877
+ ...(profile?.attentionPatternSource ? { attentionPatternSource: profile.attentionPatternSource } : {}),
878
+ ...(profile?.decisionStyleSource ? { decisionStyleSource: profile.decisionStyleSource } : {}),
879
+ // Gap to the runner-up style. Near zero means the traits do not favour
880
+ // one, and the label is a weak reading rather than a finding.
881
+ ...(profile?.decisionStyleMargin !== undefined
882
+ ? { decisionStyleMargin: profile.decisionStyleMargin }
883
+ : {}),
884
+ // Author-declared summaries that disagree with the trait vector.
885
+ //
886
+ // A persona can declare attentionPattern "thorough" while carrying
887
+ // attentionSpan 0.35, or a tech_level that its own traits contradict.
888
+ // Both are legitimate authoring -- the declaration wins -- but shipping
889
+ // the two side by side with nothing acknowledging the gap is what makes
890
+ // a profile read as internally inconsistent. Named, not silently
891
+ // reconciled: only the author knows which one is wrong.
892
+ ...(() => {
893
+ const notes = [];
894
+ const t = (profile?.traits ?? {});
895
+ const acc = (rec.accessibility_traits ?? rec.accessibilityTraits ?? {});
896
+ const demo = (rec.demographics ?? {});
897
+ const span = acc.attentionSpan;
898
+ if (profile?.attentionPatternSource === "declared" &&
899
+ profile.attentionPattern === "thorough" && typeof span === "number" && span < 0.5) {
900
+ notes.push(`attentionPattern is declared "thorough" but accessibilityTraits.attentionSpan is ${span}. The declaration is used; the traits suggest skimming.`);
901
+ }
902
+ if (profile?.decisionStyleSource === "default") {
903
+ notes.push(`decisionStyle "${profile.decisionStyle}" is the fallback: no rule matched this trait combination and the persona declares none. Do not read it as derived from the traits.`);
904
+ }
905
+ const tech = String(demo.tech_level ?? "");
906
+ const transfer = t.transferLearning, fluency = t.proceduralFluency;
907
+ if (tech && tech !== "expert" && typeof transfer === "number" && transfer >= 0.85 &&
908
+ typeof fluency === "number" && fluency >= 0.65) {
909
+ notes.push(`demographics.tech_level is "${tech}" while transferLearning is ${transfer} and proceduralFluency is ${fluency}. Demographics are set at creation and are not inferred from traits.`);
910
+ }
911
+ return notes.length ? { consistencyNotes: notes } : {};
912
+ })(),
913
+ };
914
+ // JSON in the text block, no structuredContent and no outputSchema.
915
+ //
916
+ // An outputSchema of {} is not "unconstrained" -- the SDK expands it to
917
+ // {type:object, properties:{}, additionalProperties:false}, a schema that
918
+ // permits no properties at all, which every real payload then violates.
919
+ // A schema loose enough to be correct here cannot be expressed as a Zod
920
+ // raw shape anyway, since getStatusInfo-style payloads gain fields over
921
+ // time.
922
+ //
923
+ // The view reads structuredContent when a host forwards it and falls back
924
+ // to content[0].text otherwise, which is the shape Anthropic's own MCP
925
+ // Apps example uses. Dropping both removes the validation failure without
926
+ // costing the widget anything.
927
+ return {
928
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
929
+ };
930
+ });
73
931
  server.registerTool("persona_values_lookup", {
74
932
  title: "Lookup Persona Values",
75
933
  description: "Look up the values profile for a persona (Schwartz's 10 Universal Values, SDT needs, Maslow level). Values describe WHO the persona is at a deeper motivational level, informing influence susceptibility.",
@@ -85,80 +943,9 @@ export function registerValuesTools(server) {
85
943
  openWorldHint: false,
86
944
  },
87
945
  }, async ({ persona, includeInfluencePatterns }) => {
88
- const values = getPersonaValues(persona);
89
- if (!values) {
90
- const availablePersonas = PERSONA_VALUE_PROFILES.map(p => p.personaName);
91
- return {
92
- content: [
93
- {
94
- type: "text",
95
- text: JSON.stringify({
96
- error: `No values profile found for persona: ${persona}`,
97
- availablePersonas,
98
- note: "Values are defined for all built-in personas. Custom personas can have values added via the questionnaire.",
99
- }, null, 2),
100
- },
101
- ],
102
- };
103
- }
104
- const profile = PERSONA_VALUE_PROFILES.find(p => p.personaName.toLowerCase() === persona.toLowerCase());
105
- let influencePatterns;
106
- if (includeInfluencePatterns) {
107
- const ranked = rankInfluencePatternsForProfile(values);
108
- influencePatterns = ranked.slice(0, 7).map(r => ({
109
- pattern: r.pattern.name,
110
- susceptibility: r.susceptibility,
111
- description: r.pattern.description,
112
- }));
113
- }
114
- return {
115
- content: [
116
- {
117
- type: "text",
118
- text: JSON.stringify({
119
- persona,
120
- rationale: profile?.rationale,
121
- schwartzValues: {
122
- selfDirection: { value: values.selfDirection, meaning: "Independent thought, creativity, freedom" },
123
- stimulation: { value: values.stimulation, meaning: "Excitement, novelty, challenge" },
124
- hedonism: { value: values.hedonism, meaning: "Pleasure, sensuous gratification" },
125
- achievement: { value: values.achievement, meaning: "Personal success through competence" },
126
- power: { value: values.power, meaning: "Social status, prestige, control" },
127
- security: { value: values.security, meaning: "Safety, harmony, stability" },
128
- conformity: { value: values.conformity, meaning: "Restraint of actions that harm others" },
129
- tradition: { value: values.tradition, meaning: "Respect for customs, heritage" },
130
- benevolence: { value: values.benevolence, meaning: "Welfare of close others" },
131
- universalism: { value: values.universalism, meaning: "Tolerance, social justice, environment" },
132
- },
133
- higherOrderValues: {
134
- openness: { value: values.openness, meaning: "(selfDirection + stimulation) / 2" },
135
- selfEnhancement: { value: values.selfEnhancement, meaning: "(achievement + power) / 2" },
136
- conservation: { value: values.conservation, meaning: "(security + conformity + tradition) / 3" },
137
- selfTranscendence: { value: values.selfTranscendence, meaning: "(benevolence + universalism) / 2" },
138
- },
139
- selfDeterminationTheory: {
140
- autonomyNeed: { value: values.autonomyNeed, meaning: "Need for choice and control" },
141
- competenceNeed: { value: values.competenceNeed, meaning: "Need to feel capable" },
142
- relatednessNeed: { value: values.relatednessNeed, meaning: "Need for connection" },
143
- },
144
- maslowLevel: {
145
- level: values.maslowLevel,
146
- meaning: values.maslowLevel === "physiological" ? "Basic survival needs"
147
- : values.maslowLevel === "safety" ? "Security and stability"
148
- : values.maslowLevel === "belonging" ? "Social connection and love"
149
- : values.maslowLevel === "esteem" ? "Achievement and recognition"
150
- : "Self-fulfillment and growth",
151
- },
152
- influencePatterns,
153
- researchBasis: {
154
- schwartz: "Schwartz, S. H. (1992, 2012). Theory of Basic Human Values. DOI: 10.1016/S0065-2601(08)60281-6",
155
- sdt: "Deci, E. L., & Ryan, R. M. (1985, 2000). Self-Determination Theory. DOI: 10.1037/0003-066X.55.1.68",
156
- maslow: "Maslow, A. H. (1943). A Theory of Human Motivation. DOI: 10.1037/h0054346",
157
- },
158
- }, null, 2),
159
- },
160
- ],
161
- };
946
+ // Delegates so the shape is testable without an MCP server.
947
+ const payload = await buildValuesPayload(persona, includeInfluencePatterns);
948
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
162
949
  });
163
950
  server.registerTool("list_influence_patterns", {
164
951
  title: "List Influence Patterns",
@@ -196,9 +983,9 @@ export function registerValuesTools(server) {
196
983
  });
197
984
  server.registerTool("persona_questionnaire_get", {
198
985
  title: "Get Persona Questionnaire",
199
- description: "Get the persona questionnaire for building a custom persona. Returns research-backed questions that map to cognitive traits. Use comprehensive=true for all 25 traits, or leave false for 8 core traits. v16.12.0: Now includes optional category question for disability-specific value safeguards.",
986
+ description: "Get the persona questionnaire for building a custom persona. Returns research-backed questions that map to cognitive traits. Use comprehensive=true for all 26 traits, or leave false for the 8 core ones. v16.12.0: Now includes optional category question for disability-specific value safeguards.",
200
987
  inputSchema: {
201
- comprehensive: z.boolean().optional().default(false).describe("Include all 25 traits (true) or just 8 core traits (false)"),
988
+ comprehensive: z.boolean().optional().default(false).describe("Include all 26 traits (true) or just the 8 core ones (false)"),
202
989
  traits: z.array(z.string()).optional().describe("Specific trait names to include (overrides comprehensive)"),
203
990
  includeCategory: z.boolean().optional().default(true).describe("Include category question for disability-aware values (v16.12.0)"),
204
991
  },
@@ -312,6 +1099,9 @@ export function registerValuesTools(server) {
312
1099
  trait: z.string().describe("Trait name (e.g., 'patience', 'riskTolerance')"),
313
1100
  value: z.number().min(0).max(1).describe("Trait value (0-1)"),
314
1101
  },
1102
+ // Declares the trait view. The bands are the finding; the JSON is a list
1103
+ // of five level objects a reader has to hold in their head to compare.
1104
+ _meta: { ui: { resourceUri: "ui://cbrowser/trait-v2" } },
315
1105
  annotations: {
316
1106
  title: "Lookup Persona Trait",
317
1107
  readOnlyHint: true,
@@ -335,7 +1125,7 @@ export function registerValuesTools(server) {
335
1125
  "trustCalibration", "interruptRecovery", "informationForaging", "changeBlindness",
336
1126
  "anchoringBias", "timeHorizon", "attributionStyle", "metacognitivePlanning",
337
1127
  "proceduralFluency", "transferLearning", "authoritySensitivity", "emotionalContagion",
338
- "fearOfMissingOut", "socialProofSensitivity", "mentalModelRigidity", "siteFamiliarity"
1128
+ "fearOfMissingOut", "socialProofSensitivity", "mentalModelFlexibility"
339
1129
  ],
340
1130
  }, null, 2),
341
1131
  },