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.
- package/dist/analysis/accessibility-empathy.d.ts.map +1 -1
- package/dist/analysis/accessibility-empathy.js +395 -66
- package/dist/analysis/accessibility-empathy.js.map +1 -1
- package/dist/analysis/agent-ready-audit.d.ts.map +1 -1
- package/dist/analysis/agent-ready-audit.js +78 -0
- package/dist/analysis/agent-ready-audit.js.map +1 -1
- package/dist/analysis/page-understanding.d.ts +139 -1
- package/dist/analysis/page-understanding.d.ts.map +1 -1
- package/dist/analysis/page-understanding.js +179 -12
- package/dist/analysis/page-understanding.js.map +1 -1
- package/dist/artifact-store.d.ts +19 -5
- package/dist/artifact-store.d.ts.map +1 -1
- package/dist/artifact-store.js +41 -11
- package/dist/artifact-store.js.map +1 -1
- package/dist/browser.d.ts +9 -0
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +81 -5
- package/dist/browser.js.map +1 -1
- package/dist/cli.js +55 -10
- package/dist/cli.js.map +1 -1
- package/dist/cognitive/index.d.ts.map +1 -1
- package/dist/cognitive/index.js +3 -2
- package/dist/cognitive/index.js.map +1 -1
- package/dist/config.d.ts +57 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +234 -7
- package/dist/config.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-server-remote.d.ts.map +1 -1
- package/dist/mcp-server-remote.js +108 -4
- package/dist/mcp-server-remote.js.map +1 -1
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +62 -938
- package/dist/mcp-server.js.map +1 -1
- package/dist/mcp-tools/account-personas.d.ts +41 -0
- package/dist/mcp-tools/account-personas.d.ts.map +1 -0
- package/dist/mcp-tools/account-personas.js +98 -0
- package/dist/mcp-tools/account-personas.js.map +1 -0
- package/dist/mcp-tools/base/analysis-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/analysis-tools.js +12 -2
- package/dist/mcp-tools/base/analysis-tools.js.map +1 -1
- package/dist/mcp-tools/base/artifact-tools.d.ts +3 -0
- package/dist/mcp-tools/base/artifact-tools.d.ts.map +1 -0
- package/dist/mcp-tools/base/artifact-tools.js +78 -0
- package/dist/mcp-tools/base/artifact-tools.js.map +1 -0
- package/dist/mcp-tools/base/audit-tools.d.ts +11 -0
- package/dist/mcp-tools/base/audit-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/audit-tools.js +595 -340
- package/dist/mcp-tools/base/audit-tools.js.map +1 -1
- package/dist/mcp-tools/base/browser-management-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/browser-management-tools.js +18 -0
- package/dist/mcp-tools/base/browser-management-tools.js.map +1 -1
- package/dist/mcp-tools/base/bug-analysis-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/bug-analysis-tools.js +59 -3
- package/dist/mcp-tools/base/bug-analysis-tools.js.map +1 -1
- package/dist/mcp-tools/base/capture-tools.d.ts +37 -0
- package/dist/mcp-tools/base/capture-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/capture-tools.js +267 -6
- package/dist/mcp-tools/base/capture-tools.js.map +1 -1
- package/dist/mcp-tools/base/cognitive-tools.d.ts +12 -0
- package/dist/mcp-tools/base/cognitive-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/cognitive-tools.js +63 -15
- package/dist/mcp-tools/base/cognitive-tools.js.map +1 -1
- package/dist/mcp-tools/base/extraction-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/extraction-tools.js +33 -2
- package/dist/mcp-tools/base/extraction-tools.js.map +1 -1
- package/dist/mcp-tools/base/index.d.ts +2 -0
- package/dist/mcp-tools/base/index.d.ts.map +1 -1
- package/dist/mcp-tools/base/index.js +9 -0
- package/dist/mcp-tools/base/index.js.map +1 -1
- package/dist/mcp-tools/base/marketing-tools.js +3 -3
- package/dist/mcp-tools/base/marketing-tools.js.map +1 -1
- package/dist/mcp-tools/base/persona-comparison-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/persona-comparison-tools.js +529 -40
- package/dist/mcp-tools/base/persona-comparison-tools.js.map +1 -1
- package/dist/mcp-tools/base/site-knowledge-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/site-knowledge-tools.js +36 -3
- package/dist/mcp-tools/base/site-knowledge-tools.js.map +1 -1
- package/dist/mcp-tools/base/values-tools.d.ts +113 -0
- package/dist/mcp-tools/base/values-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/values-tools.js +868 -78
- package/dist/mcp-tools/base/values-tools.js.map +1 -1
- package/dist/mcp-tools/base/visual-testing-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/visual-testing-tools.js +130 -10
- package/dist/mcp-tools/base/visual-testing-tools.js.map +1 -1
- package/dist/mcp-tools/index.d.ts +3 -1
- package/dist/mcp-tools/index.d.ts.map +1 -1
- package/dist/mcp-tools/index.js +3 -28
- package/dist/mcp-tools/index.js.map +1 -1
- package/dist/mcp-tools/persona-creation-tools.d.ts.map +1 -1
- package/dist/mcp-tools/persona-creation-tools.js +363 -20
- package/dist/mcp-tools/persona-creation-tools.js.map +1 -1
- package/dist/mcp-tools/persona-lifecycle-tools.d.ts +3 -0
- package/dist/mcp-tools/persona-lifecycle-tools.d.ts.map +1 -0
- package/dist/mcp-tools/persona-lifecycle-tools.js +406 -0
- package/dist/mcp-tools/persona-lifecycle-tools.js.map +1 -0
- package/dist/mcp-tools/screenshot-utils.d.ts.map +1 -1
- package/dist/mcp-tools/screenshot-utils.js +56 -10
- package/dist/mcp-tools/screenshot-utils.js.map +1 -1
- package/dist/mcp-tools/tier-gate.js +2 -2
- package/dist/mcp-tools/tier-gate.js.map +1 -1
- package/dist/mcp-tools/tool-categories.d.ts.map +1 -1
- package/dist/mcp-tools/tool-categories.js +4 -1
- package/dist/mcp-tools/tool-categories.js.map +1 -1
- package/dist/mcp-tools/ui-resources.d.ts +167 -0
- package/dist/mcp-tools/ui-resources.d.ts.map +1 -0
- package/dist/mcp-tools/ui-resources.js +707 -0
- package/dist/mcp-tools/ui-resources.js.map +1 -0
- package/dist/mcp-tools/widget-kit.d.ts +299 -0
- package/dist/mcp-tools/widget-kit.d.ts.map +1 -0
- package/dist/mcp-tools/widget-kit.js +2034 -0
- package/dist/mcp-tools/widget-kit.js.map +1 -0
- package/dist/mcp-ui-resources.d.ts.map +1 -1
- package/dist/mcp-ui-resources.js +14 -4
- package/dist/mcp-ui-resources.js.map +1 -1
- package/dist/persona-questionnaire.d.ts +69 -1
- package/dist/persona-questionnaire.d.ts.map +1 -1
- package/dist/persona-questionnaire.js +177 -76
- package/dist/persona-questionnaire.js.map +1 -1
- package/dist/persona-scope.d.ts +20 -0
- package/dist/persona-scope.d.ts.map +1 -0
- package/dist/persona-scope.js +59 -0
- package/dist/persona-scope.js.map +1 -0
- package/dist/personas.d.ts +51 -17
- package/dist/personas.d.ts.map +1 -1
- package/dist/personas.js +947 -286
- package/dist/personas.js.map +1 -1
- package/dist/recording/capture-player.d.ts +68 -0
- package/dist/recording/capture-player.d.ts.map +1 -0
- package/dist/recording/capture-player.js +298 -0
- package/dist/recording/capture-player.js.map +1 -0
- package/dist/recording/engine.d.ts +52 -0
- package/dist/recording/engine.d.ts.map +1 -1
- package/dist/recording/engine.js +241 -3
- package/dist/recording/engine.js.map +1 -1
- package/dist/recording/inline-player.d.ts +39 -0
- package/dist/recording/inline-player.d.ts.map +1 -0
- package/dist/recording/inline-player.js +272 -0
- package/dist/recording/inline-player.js.map +1 -0
- package/dist/recording/saliency-overlay.d.ts +133 -0
- package/dist/recording/saliency-overlay.d.ts.map +1 -0
- package/dist/recording/saliency-overlay.js +360 -0
- package/dist/recording/saliency-overlay.js.map +1 -0
- package/dist/recording/types.d.ts +32 -0
- package/dist/recording/types.d.ts.map +1 -1
- package/dist/recording/types.js +46 -0
- package/dist/recording/types.js.map +1 -1
- package/dist/site-model/manager.d.ts +20 -0
- package/dist/site-model/manager.d.ts.map +1 -1
- package/dist/site-model/manager.js +52 -0
- package/dist/site-model/manager.js.map +1 -1
- package/dist/trait-reference.d.ts +26 -2
- package/dist/trait-reference.d.ts.map +1 -1
- package/dist/trait-reference.js +129 -19
- package/dist/trait-reference.js.map +1 -1
- package/dist/types.d.ts +105 -18
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/url-resolution.d.ts +58 -0
- package/dist/url-resolution.d.ts.map +1 -0
- package/dist/url-resolution.js +143 -0
- package/dist/url-resolution.js.map +1 -0
- package/dist/values/big-five-inference.d.ts +121 -0
- package/dist/values/big-five-inference.d.ts.map +1 -0
- package/dist/values/big-five-inference.js +241 -0
- package/dist/values/big-five-inference.js.map +1 -0
- package/dist/values/big-five-values.d.ts +76 -0
- package/dist/values/big-five-values.d.ts.map +1 -0
- package/dist/values/big-five-values.js +152 -0
- package/dist/values/big-five-values.js.map +1 -0
- package/dist/values/index.d.ts +1 -1
- package/dist/values/index.d.ts.map +1 -1
- package/dist/values/index.js +1 -1
- package/dist/values/index.js.map +1 -1
- package/dist/values/persona-completeness.d.ts +91 -0
- package/dist/values/persona-completeness.d.ts.map +1 -0
- package/dist/values/persona-completeness.js +114 -0
- package/dist/values/persona-completeness.js.map +1 -0
- package/dist/values/persona-diff.d.ts +50 -0
- package/dist/values/persona-diff.d.ts.map +1 -0
- package/dist/values/persona-diff.js +126 -0
- package/dist/values/persona-diff.js.map +1 -0
- package/dist/values/persona-values.d.ts +16 -0
- package/dist/values/persona-values.d.ts.map +1 -1
- package/dist/values/persona-values.js +33 -0
- package/dist/values/persona-values.js.map +1 -1
- package/dist/values/schwartz-values.d.ts +9 -1
- package/dist/values/schwartz-values.d.ts.map +1 -1
- package/dist/values/schwartz-values.js +10 -2
- package/dist/values/schwartz-values.js.map +1 -1
- package/dist/values/value-mappings.d.ts +41 -2
- package/dist/values/value-mappings.d.ts.map +1 -1
- package/dist/values/value-mappings.js +65 -8
- package/dist/values/value-mappings.js.map +1 -1
- package/dist/visual/attention-quality.d.ts +27 -0
- package/dist/visual/attention-quality.d.ts.map +1 -1
- package/dist/visual/attention-quality.js +164 -2
- package/dist/visual/attention-quality.js.map +1 -1
- package/dist/visual/attention-transport.d.ts +41 -2
- package/dist/visual/attention-transport.d.ts.map +1 -1
- package/dist/visual/attention-transport.js +98 -26
- package/dist/visual/attention-transport.js.map +1 -1
- package/dist/visual/barrier-overlay-html.d.ts +10 -0
- package/dist/visual/barrier-overlay-html.d.ts.map +1 -1
- package/dist/visual/barrier-overlay-html.js.map +1 -1
- package/dist/visual/cognitive-models.d.ts +109 -13
- package/dist/visual/cognitive-models.d.ts.map +1 -1
- package/dist/visual/cognitive-models.js +166 -3
- package/dist/visual/cognitive-models.js.map +1 -1
- package/dist/visual/cognitive-transport-chain.d.ts +149 -5
- package/dist/visual/cognitive-transport-chain.d.ts.map +1 -1
- package/dist/visual/cognitive-transport-chain.js +558 -30
- package/dist/visual/cognitive-transport-chain.js.map +1 -1
- package/dist/visual/cognitive-transport.d.ts +45 -3
- package/dist/visual/cognitive-transport.d.ts.map +1 -1
- package/dist/visual/cognitive-transport.js +126 -12
- package/dist/visual/cognitive-transport.js.map +1 -1
- package/dist/visual/fixation-corpus.d.ts +52 -0
- package/dist/visual/fixation-corpus.d.ts.map +1 -1
- package/dist/visual/fixation-corpus.js +95 -0
- package/dist/visual/fixation-corpus.js.map +1 -1
- package/dist/visual/llm-relevance.d.ts +193 -0
- package/dist/visual/llm-relevance.d.ts.map +1 -0
- package/dist/visual/llm-relevance.js +480 -0
- package/dist/visual/llm-relevance.js.map +1 -0
- package/dist/visual/perceptual-transport.d.ts +60 -0
- package/dist/visual/perceptual-transport.d.ts.map +1 -1
- package/dist/visual/perceptual-transport.js +228 -19
- package/dist/visual/perceptual-transport.js.map +1 -1
- package/dist/visual/persona-context.d.ts +23 -0
- package/dist/visual/persona-context.d.ts.map +1 -0
- package/dist/visual/persona-context.js +44 -0
- package/dist/visual/persona-context.js.map +1 -0
- 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
|
-
*
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
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
|
|
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", "
|
|
1128
|
+
"fearOfMissingOut", "socialProofSensitivity", "mentalModelFlexibility"
|
|
339
1129
|
],
|
|
340
1130
|
}, null, 2),
|
|
341
1131
|
},
|