role-os 2.0.0 → 2.2.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/CHANGELOG.md +61 -0
- package/README.es.md +123 -54
- package/README.fr.md +90 -21
- package/README.hi.md +90 -21
- package/README.it.md +130 -61
- package/README.ja.md +91 -22
- package/README.md +72 -16
- package/README.pt-BR.md +90 -21
- package/README.zh.md +160 -88
- package/package.json +2 -2
- package/src/artifacts.mjs +569 -437
- package/src/brainstorm-render.mjs +462 -0
- package/src/brainstorm-roles.mjs +774 -0
- package/src/brainstorm.mjs +778 -0
- package/src/dispatch.mjs +339 -310
- package/src/evidence.mjs +9 -9
- package/src/mission-run.mjs +111 -13
- package/src/mission.mjs +508 -388
- package/src/packs.mjs +430 -359
- package/src/route.mjs +715 -564
- package/src/run.mjs +5 -2
- package/starter-pack/agents/engineering/audit-synthesizer.md +56 -0
- package/starter-pack/agents/engineering/component-auditor.md +46 -0
- package/starter-pack/agents/engineering/seam-auditor.md +46 -0
- package/starter-pack/agents/engineering/test-truth-auditor.md +48 -0
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brainstorm Role Contracts — v0.3
|
|
3
|
+
*
|
|
4
|
+
* Real role specialization means each analyst:
|
|
5
|
+
* - Sees a filtered slice of the brief (input partition)
|
|
6
|
+
* - Answers only permitted questions (method contract)
|
|
7
|
+
* - Cannot reason about forbidden topics (blindspot enforcement)
|
|
8
|
+
* - Emits a role-native output schema (not shared prose)
|
|
9
|
+
* - Gets validated for out-of-lens claims (boundary validator)
|
|
10
|
+
*
|
|
11
|
+
* Pipeline: Frame → Analyze (role-native) → Normalize → Cross-Examine → Rebut → Synthesize → Expand → Judge → Return
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ── Role-native output schemas ──────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Context Analyst: What is this, what is it next to, and what is it not?
|
|
18
|
+
* Method: terminology genealogy, adjacency map, category boundary map
|
|
19
|
+
*/
|
|
20
|
+
const CONTEXT_MAP_SCHEMA = {
|
|
21
|
+
role: "Context Analyst",
|
|
22
|
+
fields: {
|
|
23
|
+
terms: { type: "array", items: { term: "string", meaning: "string", adjacent_to: "string[]" }, required: true, minItems: 3 },
|
|
24
|
+
category_map: { type: "array", items: { category: "string", examples: "string[]" }, required: true, minItems: 2 },
|
|
25
|
+
lineage_claims: { type: "array", items: { claim: "string", precedent: "string[]" }, required: true, minItems: 1 },
|
|
26
|
+
boundary_claims: { type: "array", items: "string", required: true, minItems: 1 },
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* User Value Analyst: Where is the felt pull or pain?
|
|
32
|
+
* Method: jobs-to-be-done, pain/relief map, willingness/avoidance signals
|
|
33
|
+
*/
|
|
34
|
+
const USER_VALUE_MAP_SCHEMA = {
|
|
35
|
+
role: "User Value Analyst",
|
|
36
|
+
fields: {
|
|
37
|
+
jobs: { type: "array", items: { actor: "string", situation: "string", desired_outcome: "string" }, required: true, minItems: 2 },
|
|
38
|
+
frictions: { type: "array", items: { friction: "string", severity: "enum:high|medium|low" }, required: true, minItems: 2 },
|
|
39
|
+
unmet_desires: { type: "array", items: "string", required: true, minItems: 1 },
|
|
40
|
+
willingness_signals: { type: "array", items: "string", required: true, minItems: 1 },
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Mechanics Analyst: What has to be true for this to work?
|
|
46
|
+
* Method: loop decomposition, dependency chain, failure-mode analysis
|
|
47
|
+
*/
|
|
48
|
+
const MECHANICS_MAP_SCHEMA = {
|
|
49
|
+
role: "Mechanics Analyst",
|
|
50
|
+
fields: {
|
|
51
|
+
loops: { type: "array", items: { name: "string", input: "string[]", transform: "string[]", output: "string[]" }, required: true, minItems: 1 },
|
|
52
|
+
dependencies: { type: "array", items: { component: "string", depends_on: "string[]" }, required: true, minItems: 1 },
|
|
53
|
+
failure_points: { type: "array", items: "string", required: true, minItems: 1 },
|
|
54
|
+
irreducible_mechanisms: { type: "array", items: "string", required: true, minItems: 1 },
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Positioning Analyst: What claim could this own, and when is it legal to make it?
|
|
60
|
+
* Method: substitute comparison, wedge identification, claim timing analysis
|
|
61
|
+
*/
|
|
62
|
+
const POSITIONING_MAP_SCHEMA = {
|
|
63
|
+
role: "Positioning Analyst",
|
|
64
|
+
fields: {
|
|
65
|
+
substitutes: { type: "array", items: { name: "string", overlap: "string", gap: "string" }, required: true, minItems: 1 },
|
|
66
|
+
wedge_candidates: { type: "array", items: { claim: "string", timing: "string", risk: "string" }, required: true, minItems: 1 },
|
|
67
|
+
category_frame: { type: "string", required: true },
|
|
68
|
+
forbidden_claims: { type: "array", items: { claim: "string", reason: "string" }, required: false },
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Contrarian Analyst: Which specific claims are overstated, premature, or structurally false?
|
|
74
|
+
* Method: targeted challenge only, claim-by-claim attack, contradiction exposure
|
|
75
|
+
*/
|
|
76
|
+
const CHALLENGE_SET_SCHEMA = {
|
|
77
|
+
role: "Contrarian Analyst",
|
|
78
|
+
fields: {
|
|
79
|
+
challenges: { type: "array", items: {
|
|
80
|
+
target_claim_id: "string",
|
|
81
|
+
source_role: "string",
|
|
82
|
+
challenge_type: "enum:out_of_scope|unsupported|premature|mechanically_blocked|market_invisible|user_misaligned",
|
|
83
|
+
argument: "string",
|
|
84
|
+
evidence_grade: "enum:grounded|mixed|speculative",
|
|
85
|
+
confidence: "enum:high|medium|low",
|
|
86
|
+
}, required: true, minItems: 1 },
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const ROLE_NATIVE_SCHEMAS = {
|
|
91
|
+
"Context Analyst": CONTEXT_MAP_SCHEMA,
|
|
92
|
+
"User Value Analyst": USER_VALUE_MAP_SCHEMA,
|
|
93
|
+
"Mechanics Analyst": MECHANICS_MAP_SCHEMA,
|
|
94
|
+
"Positioning Analyst": POSITIONING_MAP_SCHEMA,
|
|
95
|
+
"Contrarian Analyst": CHALLENGE_SET_SCHEMA,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// ── Input partitioning ──────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Each role sees a filtered view of the brief. Not guidance — structural partition.
|
|
102
|
+
* Fields not in the permitted set are stripped before the role receives the brief.
|
|
103
|
+
*/
|
|
104
|
+
export const INPUT_PARTITIONS = {
|
|
105
|
+
"Context Analyst": {
|
|
106
|
+
permitted: ["topic", "constraints", "search_axes"],
|
|
107
|
+
forbidden: ["audience", "novelty_bias"],
|
|
108
|
+
rationale: "Context Analyst maps the space, not the people in it. Audience awareness biases toward user-centric framing.",
|
|
109
|
+
},
|
|
110
|
+
"User Value Analyst": {
|
|
111
|
+
permitted: ["topic", "objective", "audience", "constraints"],
|
|
112
|
+
forbidden: ["search_axes", "novelty_bias"],
|
|
113
|
+
rationale: "User Value Analyst needs audience and constraints but not category-shaping axes that belong to Context.",
|
|
114
|
+
},
|
|
115
|
+
"Mechanics Analyst": {
|
|
116
|
+
permitted: ["topic", "objective", "constraints"],
|
|
117
|
+
forbidden: ["audience", "novelty_bias", "search_axes"],
|
|
118
|
+
rationale: "Mechanics Analyst reasons about what has to be true structurally. Audience and novelty are irrelevant to mechanism.",
|
|
119
|
+
},
|
|
120
|
+
"Positioning Analyst": {
|
|
121
|
+
permitted: ["topic", "objective", "audience", "constraints"],
|
|
122
|
+
forbidden: ["search_axes"],
|
|
123
|
+
rationale: "Positioning needs audience and constraints but should not see search axes that pre-frame the category.",
|
|
124
|
+
},
|
|
125
|
+
"Contrarian Analyst": {
|
|
126
|
+
permitted: [], // Contrarian sees ONLY claim atoms from other roles, not the original brief
|
|
127
|
+
forbidden: ["topic", "objective", "audience", "constraints", "search_axes", "novelty_bias"],
|
|
128
|
+
rationale: "Contrarian operates on claims, not the brief. Seeing the brief creates alignment bias.",
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Partition a BrainstormRequest for a specific role.
|
|
134
|
+
* Returns only the fields the role is permitted to see.
|
|
135
|
+
*
|
|
136
|
+
* @param {object} request - Full BrainstormRequest
|
|
137
|
+
* @param {string} roleName - Role to partition for
|
|
138
|
+
* @returns {object} Filtered request with only permitted fields
|
|
139
|
+
*/
|
|
140
|
+
export function partitionBrief(request, roleName) {
|
|
141
|
+
const partition = INPUT_PARTITIONS[roleName];
|
|
142
|
+
if (!partition) return request; // Unknown role gets full brief (fallback)
|
|
143
|
+
|
|
144
|
+
const filtered = {};
|
|
145
|
+
for (const field of partition.permitted) {
|
|
146
|
+
if (request[field] !== undefined) {
|
|
147
|
+
filtered[field] = request[field];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return filtered;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── Forbidden topics / blindspot enforcement ────────────────────────────────
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Each role has forbidden vocabulary — words and phrases that indicate
|
|
157
|
+
* the role is reasoning outside its lens. These are rejection criteria,
|
|
158
|
+
* not guidance.
|
|
159
|
+
*/
|
|
160
|
+
export const ROLE_BLINDSPOTS = {
|
|
161
|
+
"Context Analyst": {
|
|
162
|
+
forbidden_phrases: [
|
|
163
|
+
"users want", "users need", "developers prefer", "audience desires",
|
|
164
|
+
"should build", "recommend building", "the product should",
|
|
165
|
+
"market opportunity", "revenue", "pricing", "go-to-market",
|
|
166
|
+
"competitive advantage", "positioning",
|
|
167
|
+
],
|
|
168
|
+
forbidden_claim_kinds: ["need", "desire", "positioning", "mechanism"],
|
|
169
|
+
permitted_claim_kinds: ["definition", "category", "lineage", "boundary", "adjacency"],
|
|
170
|
+
rejection_reason: "Context Analyst cannot infer user desire, rank commercial opportunity, or recommend product scope.",
|
|
171
|
+
},
|
|
172
|
+
"User Value Analyst": {
|
|
173
|
+
forbidden_phrases: [
|
|
174
|
+
"architecture should", "implement using", "the system requires",
|
|
175
|
+
"dependency on", "infrastructure", "database", "API design",
|
|
176
|
+
"competitor ranks", "market share", "positioning against",
|
|
177
|
+
"category definition", "adjacent to", "lineage of",
|
|
178
|
+
],
|
|
179
|
+
forbidden_claim_kinds: ["definition", "category", "lineage", "mechanism", "positioning"],
|
|
180
|
+
permitted_claim_kinds: ["need", "desire", "friction", "willingness", "avoidance"],
|
|
181
|
+
rejection_reason: "User Value Analyst cannot compare competitors, propose architecture, or infer technical feasibility.",
|
|
182
|
+
},
|
|
183
|
+
"Mechanics Analyst": {
|
|
184
|
+
forbidden_phrases: [
|
|
185
|
+
"users want", "users feel", "emotional resonance",
|
|
186
|
+
"brand", "messaging", "positioning", "go-to-market",
|
|
187
|
+
"pricing", "revenue model", "customer psychology",
|
|
188
|
+
"market appetite", "competitive advantage",
|
|
189
|
+
],
|
|
190
|
+
forbidden_claim_kinds: ["need", "desire", "positioning", "category"],
|
|
191
|
+
permitted_claim_kinds: ["mechanism", "dependency", "constraint", "failure_mode", "loop"],
|
|
192
|
+
rejection_reason: "Mechanics Analyst cannot decide messaging, estimate emotional resonance, or assert user willingness.",
|
|
193
|
+
},
|
|
194
|
+
"Positioning Analyst": {
|
|
195
|
+
forbidden_phrases: [
|
|
196
|
+
"implement using", "architecture should", "database",
|
|
197
|
+
"API design", "dependency chain", "failure mode",
|
|
198
|
+
"the system requires", "infrastructure",
|
|
199
|
+
"terminology genealogy", "adjacent space", "category boundary",
|
|
200
|
+
],
|
|
201
|
+
forbidden_claim_kinds: ["mechanism", "dependency", "definition", "lineage"],
|
|
202
|
+
permitted_claim_kinds: ["positioning", "wedge", "substitute", "timing", "category_frame"],
|
|
203
|
+
rejection_reason: "Positioning Analyst cannot design implementation or map ontology.",
|
|
204
|
+
},
|
|
205
|
+
"Contrarian Analyst": {
|
|
206
|
+
forbidden_phrases: [
|
|
207
|
+
"I suggest", "we should", "the product could", "a better approach",
|
|
208
|
+
"my recommendation", "consider building",
|
|
209
|
+
],
|
|
210
|
+
forbidden_claim_kinds: ["need", "desire", "mechanism", "positioning", "definition"],
|
|
211
|
+
permitted_claim_kinds: ["challenge", "contradiction", "overstatement", "gap"],
|
|
212
|
+
rejection_reason: "Contrarian Analyst cannot generate original ideas, only challenge existing claims by ID.",
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// ── Boundary validators ─────────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Validate a role-native output against its blindspot contract.
|
|
220
|
+
* Returns violations — any forbidden phrase or claim kind found in the output.
|
|
221
|
+
*
|
|
222
|
+
* @param {string} roleName
|
|
223
|
+
* @param {string} outputText - The full text of the role's output (stringified)
|
|
224
|
+
* @returns {{ valid: boolean, violations: Array<{type: string, detail: string}> }}
|
|
225
|
+
*/
|
|
226
|
+
export function validateRoleBoundary(roleName, outputText) {
|
|
227
|
+
const blindspot = ROLE_BLINDSPOTS[roleName];
|
|
228
|
+
if (!blindspot) return { valid: true, violations: [] };
|
|
229
|
+
|
|
230
|
+
const violations = [];
|
|
231
|
+
const lower = outputText.toLowerCase();
|
|
232
|
+
|
|
233
|
+
for (const phrase of blindspot.forbidden_phrases) {
|
|
234
|
+
if (lower.includes(phrase.toLowerCase())) {
|
|
235
|
+
violations.push({
|
|
236
|
+
type: "forbidden_phrase",
|
|
237
|
+
detail: `"${phrase}" — ${blindspot.rejection_reason}`,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return { valid: violations.length === 0, violations };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Validate that a role's output claims are within its permitted claim kinds.
|
|
247
|
+
*
|
|
248
|
+
* @param {string} roleName
|
|
249
|
+
* @param {Array<{kind: string}>} claims - Claims with kind field
|
|
250
|
+
* @returns {{ valid: boolean, violations: Array<{claim_index: number, kind: string, detail: string}> }}
|
|
251
|
+
*/
|
|
252
|
+
export function validateClaimKinds(roleName, claims) {
|
|
253
|
+
const blindspot = ROLE_BLINDSPOTS[roleName];
|
|
254
|
+
if (!blindspot) return { valid: true, violations: [] };
|
|
255
|
+
|
|
256
|
+
const violations = [];
|
|
257
|
+
for (let i = 0; i < claims.length; i++) {
|
|
258
|
+
if (blindspot.forbidden_claim_kinds.includes(claims[i].kind)) {
|
|
259
|
+
violations.push({
|
|
260
|
+
claim_index: i,
|
|
261
|
+
kind: claims[i].kind,
|
|
262
|
+
detail: `Claim kind "${claims[i].kind}" is forbidden for ${roleName}. ${blindspot.rejection_reason}`,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return { valid: violations.length === 0, violations };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ── Role-native output validators ───────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Validate a role-native output against its schema.
|
|
274
|
+
*
|
|
275
|
+
* @param {string} roleName
|
|
276
|
+
* @param {object} output - The role-native output object
|
|
277
|
+
* @returns {{ valid: boolean, issues: string[] }}
|
|
278
|
+
*/
|
|
279
|
+
export function validateRoleNativeOutput(roleName, output) {
|
|
280
|
+
const schema = ROLE_NATIVE_SCHEMAS[roleName];
|
|
281
|
+
if (!schema) return { valid: false, issues: [`No schema defined for role "${roleName}"`] };
|
|
282
|
+
|
|
283
|
+
const issues = [];
|
|
284
|
+
|
|
285
|
+
if (!output) return { valid: false, issues: ["Output is null"] };
|
|
286
|
+
|
|
287
|
+
for (const [fieldName, spec] of Object.entries(schema.fields)) {
|
|
288
|
+
const value = output[fieldName];
|
|
289
|
+
|
|
290
|
+
if (spec.required && (value === undefined || value === null)) {
|
|
291
|
+
issues.push(`${fieldName} is required`);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (value === undefined) continue;
|
|
296
|
+
|
|
297
|
+
if (spec.type === "array") {
|
|
298
|
+
if (!Array.isArray(value)) {
|
|
299
|
+
issues.push(`${fieldName} must be an array`);
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (spec.minItems && value.length < spec.minItems) {
|
|
303
|
+
issues.push(`${fieldName} must have at least ${spec.minItems} items (got ${value.length})`);
|
|
304
|
+
}
|
|
305
|
+
// Validate item shape for object items
|
|
306
|
+
if (typeof spec.items === "object" && !Array.isArray(spec.items)) {
|
|
307
|
+
for (let i = 0; i < value.length; i++) {
|
|
308
|
+
for (const [itemField, itemType] of Object.entries(spec.items)) {
|
|
309
|
+
if (value[i][itemField] === undefined) {
|
|
310
|
+
issues.push(`${fieldName}[${i}].${itemField} is required`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
} else if (spec.type === "string") {
|
|
316
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
317
|
+
issues.push(`${fieldName} must be a non-empty string`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return { valid: issues.length === 0, issues };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ── Cross-examination schemas ───────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
const VALID_CHALLENGE_TYPES = [
|
|
328
|
+
"out_of_scope",
|
|
329
|
+
"unsupported",
|
|
330
|
+
"premature",
|
|
331
|
+
"mechanically_blocked",
|
|
332
|
+
"market_invisible",
|
|
333
|
+
"user_misaligned",
|
|
334
|
+
];
|
|
335
|
+
|
|
336
|
+
const VALID_REBUTTAL_RESPONSES = ["defend", "narrow", "retract", "unresolved"];
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Validate a ChallengeEdge.
|
|
340
|
+
*/
|
|
341
|
+
export function validateChallengeEdge(edge) {
|
|
342
|
+
const issues = [];
|
|
343
|
+
if (!edge.target_claim_id) issues.push("target_claim_id is required");
|
|
344
|
+
if (!edge.challenger_role) issues.push("challenger_role is required");
|
|
345
|
+
if (!edge.reason) issues.push("reason is required");
|
|
346
|
+
if (!VALID_CHALLENGE_TYPES.includes(edge.challenge_type)) {
|
|
347
|
+
issues.push(`challenge_type must be one of: ${VALID_CHALLENGE_TYPES.join(", ")}`);
|
|
348
|
+
}
|
|
349
|
+
return { valid: issues.length === 0, issues };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Validate a RebuttalEdge.
|
|
354
|
+
*/
|
|
355
|
+
export function validateRebuttalEdge(edge) {
|
|
356
|
+
const issues = [];
|
|
357
|
+
if (!edge.target_claim_id) issues.push("target_claim_id is required");
|
|
358
|
+
if (!edge.source_role) issues.push("source_role is required");
|
|
359
|
+
if (!VALID_REBUTTAL_RESPONSES.includes(edge.response)) {
|
|
360
|
+
issues.push(`response must be one of: ${VALID_REBUTTAL_RESPONSES.join(", ")}`);
|
|
361
|
+
}
|
|
362
|
+
if (!edge.note) issues.push("note is required");
|
|
363
|
+
return { valid: issues.length === 0, issues };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Cross-examination permission matrix.
|
|
368
|
+
* Each role can only challenge claims from specific other roles.
|
|
369
|
+
* This prevents all-vs-all noise and enforces structured debate.
|
|
370
|
+
*/
|
|
371
|
+
export const CROSS_EXAM_PERMISSIONS = {
|
|
372
|
+
"Context Analyst": {
|
|
373
|
+
can_challenge: ["Positioning Analyst"], // Can challenge category framing
|
|
374
|
+
cannot_challenge: ["User Value Analyst", "Mechanics Analyst", "Contrarian Analyst"],
|
|
375
|
+
},
|
|
376
|
+
"User Value Analyst": {
|
|
377
|
+
can_challenge: ["Context Analyst"], // Can challenge category claims that overstate importance vs felt need
|
|
378
|
+
cannot_challenge: ["Mechanics Analyst", "Positioning Analyst", "Contrarian Analyst"],
|
|
379
|
+
},
|
|
380
|
+
"Mechanics Analyst": {
|
|
381
|
+
can_challenge: ["User Value Analyst", "Positioning Analyst"], // Can challenge claims assuming impossible behavior
|
|
382
|
+
cannot_challenge: ["Context Analyst", "Contrarian Analyst"],
|
|
383
|
+
},
|
|
384
|
+
"Positioning Analyst": {
|
|
385
|
+
can_challenge: ["Mechanics Analyst"], // Can challenge claims that are true but not market-visible
|
|
386
|
+
cannot_challenge: ["Context Analyst", "User Value Analyst", "Contrarian Analyst"],
|
|
387
|
+
},
|
|
388
|
+
"Contrarian Analyst": {
|
|
389
|
+
can_challenge: ["Context Analyst", "User Value Analyst", "Mechanics Analyst", "Positioning Analyst"], // Can challenge any, but only by ID with grounds
|
|
390
|
+
cannot_challenge: [],
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Check if a challenger is permitted to challenge a target role's claims.
|
|
396
|
+
*
|
|
397
|
+
* @param {string} challengerRole
|
|
398
|
+
* @param {string} targetRole
|
|
399
|
+
* @returns {boolean}
|
|
400
|
+
*/
|
|
401
|
+
export function canChallenge(challengerRole, targetRole) {
|
|
402
|
+
const perms = CROSS_EXAM_PERMISSIONS[challengerRole];
|
|
403
|
+
if (!perms) return false;
|
|
404
|
+
return perms.can_challenge.includes(targetRole);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ── Analyst role definitions (for role catalog integration) ─────────────────
|
|
408
|
+
|
|
409
|
+
export const ANALYST_ROLES = [
|
|
410
|
+
{
|
|
411
|
+
name: "Context Analyst",
|
|
412
|
+
question: "What is this, what is it next to, and what is it not?",
|
|
413
|
+
method: "terminology genealogy, adjacency map, category boundary map",
|
|
414
|
+
output_type: "ContextMap",
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
name: "User Value Analyst",
|
|
418
|
+
question: "Where is the felt pull or pain?",
|
|
419
|
+
method: "jobs-to-be-done, pain/relief map, willingness/avoidance signals",
|
|
420
|
+
output_type: "UserValueMap",
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
name: "Mechanics Analyst",
|
|
424
|
+
question: "What has to be true for this to work?",
|
|
425
|
+
method: "loop decomposition, dependency chain, failure-mode analysis",
|
|
426
|
+
output_type: "MechanicsMap",
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
name: "Positioning Analyst",
|
|
430
|
+
question: "What claim could this own, and when is it legal to make it?",
|
|
431
|
+
method: "substitute comparison, wedge identification, claim timing analysis",
|
|
432
|
+
output_type: "PositioningMap",
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
name: "Contrarian Analyst",
|
|
436
|
+
question: "Which specific claims are overstated, premature, or structurally false?",
|
|
437
|
+
method: "targeted challenge only, claim-by-claim attack, contradiction exposure",
|
|
438
|
+
output_type: "ChallengeSet",
|
|
439
|
+
},
|
|
440
|
+
];
|
|
441
|
+
|
|
442
|
+
// ── Provenance-preserving atom translation ──────────────────────────────────
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Claim kinds per role-native artifact type.
|
|
446
|
+
* When Normalize translates a role-native output into atoms,
|
|
447
|
+
* each atom carries these so cross-examine knows what's legal to challenge.
|
|
448
|
+
*/
|
|
449
|
+
const ARTIFACT_CLAIM_KINDS = {
|
|
450
|
+
ContextMap: ["definition", "category", "lineage", "boundary", "adjacency"],
|
|
451
|
+
UserValueMap: ["need", "desire", "friction", "willingness", "avoidance"],
|
|
452
|
+
MechanicsMap: ["mechanism", "dependency", "constraint", "failure_mode", "loop"],
|
|
453
|
+
PositioningMap: ["positioning", "wedge", "substitute", "timing", "category_frame"],
|
|
454
|
+
ChallengeSet: ["challenge", "contradiction", "overstatement", "gap"],
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Translate a role-native output into provenance-preserving claim atoms.
|
|
459
|
+
* Each atom carries: source_role, source_artifact_type, claim_kind, allowed_challengers.
|
|
460
|
+
*
|
|
461
|
+
* Normalize must NOT flatten these fields. They are the identity of the claim.
|
|
462
|
+
*
|
|
463
|
+
* @param {string} roleName
|
|
464
|
+
* @param {object} roleOutput - The role-native output object
|
|
465
|
+
* @returns {Array<{id: string, text: string, claim_kind: string, source_role: string, source_artifact_type: string, allowed_challengers: string[]}>}
|
|
466
|
+
*/
|
|
467
|
+
export function translateToAtoms(roleName, roleOutput) {
|
|
468
|
+
const role = ANALYST_ROLES.find(r => r.name === roleName);
|
|
469
|
+
if (!role) return [];
|
|
470
|
+
|
|
471
|
+
const artifactType = role.output_type;
|
|
472
|
+
const allowedKinds = ARTIFACT_CLAIM_KINDS[artifactType] || [];
|
|
473
|
+
|
|
474
|
+
// Determine who can challenge this role's claims
|
|
475
|
+
const allowed_challengers = [];
|
|
476
|
+
for (const [challenger, perms] of Object.entries(CROSS_EXAM_PERMISSIONS)) {
|
|
477
|
+
if (perms.can_challenge.includes(roleName)) {
|
|
478
|
+
allowed_challengers.push(challenger);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const atoms = [];
|
|
483
|
+
let seq = 0;
|
|
484
|
+
|
|
485
|
+
switch (artifactType) {
|
|
486
|
+
case "ContextMap": {
|
|
487
|
+
// Terms → definition atoms
|
|
488
|
+
if (roleOutput.terms) {
|
|
489
|
+
for (const t of roleOutput.terms) {
|
|
490
|
+
atoms.push({
|
|
491
|
+
id: `atom-${roleName.toLowerCase().replace(/\s+/g, "-")}-${++seq}`,
|
|
492
|
+
text: `${t.term}: ${t.meaning} (adjacent to: ${(t.adjacent_to || []).join(", ")})`,
|
|
493
|
+
claim_kind: "definition",
|
|
494
|
+
source_role: roleName,
|
|
495
|
+
source_artifact_type: artifactType,
|
|
496
|
+
allowed_challengers,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
// Category map → category atoms
|
|
501
|
+
if (roleOutput.category_map) {
|
|
502
|
+
for (const c of roleOutput.category_map) {
|
|
503
|
+
atoms.push({
|
|
504
|
+
id: `atom-${roleName.toLowerCase().replace(/\s+/g, "-")}-${++seq}`,
|
|
505
|
+
text: `Category "${c.category}": ${(c.examples || []).join(", ")}`,
|
|
506
|
+
claim_kind: "category",
|
|
507
|
+
source_role: roleName,
|
|
508
|
+
source_artifact_type: artifactType,
|
|
509
|
+
allowed_challengers,
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
// Lineage claims
|
|
514
|
+
if (roleOutput.lineage_claims) {
|
|
515
|
+
for (const l of roleOutput.lineage_claims) {
|
|
516
|
+
atoms.push({
|
|
517
|
+
id: `atom-${roleName.toLowerCase().replace(/\s+/g, "-")}-${++seq}`,
|
|
518
|
+
text: `${l.claim} (precedent: ${(l.precedent || []).join(", ")})`,
|
|
519
|
+
claim_kind: "lineage",
|
|
520
|
+
source_role: roleName,
|
|
521
|
+
source_artifact_type: artifactType,
|
|
522
|
+
allowed_challengers,
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
// Boundary claims
|
|
527
|
+
if (roleOutput.boundary_claims) {
|
|
528
|
+
for (const b of roleOutput.boundary_claims) {
|
|
529
|
+
atoms.push({
|
|
530
|
+
id: `atom-${roleName.toLowerCase().replace(/\s+/g, "-")}-${++seq}`,
|
|
531
|
+
text: b,
|
|
532
|
+
claim_kind: "boundary",
|
|
533
|
+
source_role: roleName,
|
|
534
|
+
source_artifact_type: artifactType,
|
|
535
|
+
allowed_challengers,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
case "UserValueMap": {
|
|
543
|
+
// Jobs → need atoms
|
|
544
|
+
if (roleOutput.jobs) {
|
|
545
|
+
for (const j of roleOutput.jobs) {
|
|
546
|
+
atoms.push({
|
|
547
|
+
id: `atom-${roleName.toLowerCase().replace(/\s+/g, "-")}-${++seq}`,
|
|
548
|
+
text: `When ${j.actor} is ${j.situation}, they want ${j.desired_outcome}`,
|
|
549
|
+
claim_kind: "need",
|
|
550
|
+
source_role: roleName,
|
|
551
|
+
source_artifact_type: artifactType,
|
|
552
|
+
allowed_challengers,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
// Frictions → friction atoms
|
|
557
|
+
if (roleOutput.frictions) {
|
|
558
|
+
for (const f of roleOutput.frictions) {
|
|
559
|
+
atoms.push({
|
|
560
|
+
id: `atom-${roleName.toLowerCase().replace(/\s+/g, "-")}-${++seq}`,
|
|
561
|
+
text: `Friction (${f.severity}): ${f.friction}`,
|
|
562
|
+
claim_kind: "friction",
|
|
563
|
+
source_role: roleName,
|
|
564
|
+
source_artifact_type: artifactType,
|
|
565
|
+
allowed_challengers,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
// Unmet desires
|
|
570
|
+
if (roleOutput.unmet_desires) {
|
|
571
|
+
for (const d of roleOutput.unmet_desires) {
|
|
572
|
+
atoms.push({
|
|
573
|
+
id: `atom-${roleName.toLowerCase().replace(/\s+/g, "-")}-${++seq}`,
|
|
574
|
+
text: d,
|
|
575
|
+
claim_kind: "desire",
|
|
576
|
+
source_role: roleName,
|
|
577
|
+
source_artifact_type: artifactType,
|
|
578
|
+
allowed_challengers,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
// Willingness signals
|
|
583
|
+
if (roleOutput.willingness_signals) {
|
|
584
|
+
for (const w of roleOutput.willingness_signals) {
|
|
585
|
+
atoms.push({
|
|
586
|
+
id: `atom-${roleName.toLowerCase().replace(/\s+/g, "-")}-${++seq}`,
|
|
587
|
+
text: w,
|
|
588
|
+
claim_kind: "willingness",
|
|
589
|
+
source_role: roleName,
|
|
590
|
+
source_artifact_type: artifactType,
|
|
591
|
+
allowed_challengers,
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
case "MechanicsMap": {
|
|
599
|
+
// Loops → loop atoms
|
|
600
|
+
if (roleOutput.loops) {
|
|
601
|
+
for (const l of roleOutput.loops) {
|
|
602
|
+
atoms.push({
|
|
603
|
+
id: `atom-${roleName.toLowerCase().replace(/\s+/g, "-")}-${++seq}`,
|
|
604
|
+
text: `Loop "${l.name}": ${(l.input || []).join(", ")} → ${(l.transform || []).join(", ")} → ${(l.output || []).join(", ")}`,
|
|
605
|
+
claim_kind: "loop",
|
|
606
|
+
source_role: roleName,
|
|
607
|
+
source_artifact_type: artifactType,
|
|
608
|
+
allowed_challengers,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
// Dependencies
|
|
613
|
+
if (roleOutput.dependencies) {
|
|
614
|
+
for (const d of roleOutput.dependencies) {
|
|
615
|
+
atoms.push({
|
|
616
|
+
id: `atom-${roleName.toLowerCase().replace(/\s+/g, "-")}-${++seq}`,
|
|
617
|
+
text: `${d.component} depends on: ${(d.depends_on || []).join(", ")}`,
|
|
618
|
+
claim_kind: "dependency",
|
|
619
|
+
source_role: roleName,
|
|
620
|
+
source_artifact_type: artifactType,
|
|
621
|
+
allowed_challengers,
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
// Failure points
|
|
626
|
+
if (roleOutput.failure_points) {
|
|
627
|
+
for (const f of roleOutput.failure_points) {
|
|
628
|
+
atoms.push({
|
|
629
|
+
id: `atom-${roleName.toLowerCase().replace(/\s+/g, "-")}-${++seq}`,
|
|
630
|
+
text: f,
|
|
631
|
+
claim_kind: "failure_mode",
|
|
632
|
+
source_role: roleName,
|
|
633
|
+
source_artifact_type: artifactType,
|
|
634
|
+
allowed_challengers,
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
// Irreducible mechanisms
|
|
639
|
+
if (roleOutput.irreducible_mechanisms) {
|
|
640
|
+
for (const m of roleOutput.irreducible_mechanisms) {
|
|
641
|
+
atoms.push({
|
|
642
|
+
id: `atom-${roleName.toLowerCase().replace(/\s+/g, "-")}-${++seq}`,
|
|
643
|
+
text: m,
|
|
644
|
+
claim_kind: "mechanism",
|
|
645
|
+
source_role: roleName,
|
|
646
|
+
source_artifact_type: artifactType,
|
|
647
|
+
allowed_challengers,
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
break;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
case "PositioningMap": {
|
|
655
|
+
// Substitutes
|
|
656
|
+
if (roleOutput.substitutes) {
|
|
657
|
+
for (const s of roleOutput.substitutes) {
|
|
658
|
+
atoms.push({
|
|
659
|
+
id: `atom-${roleName.toLowerCase().replace(/\s+/g, "-")}-${++seq}`,
|
|
660
|
+
text: `Substitute "${s.name}": overlaps on ${s.overlap}, gap at ${s.gap}`,
|
|
661
|
+
claim_kind: "substitute",
|
|
662
|
+
source_role: roleName,
|
|
663
|
+
source_artifact_type: artifactType,
|
|
664
|
+
allowed_challengers,
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
// Wedge candidates
|
|
669
|
+
if (roleOutput.wedge_candidates) {
|
|
670
|
+
for (const w of roleOutput.wedge_candidates) {
|
|
671
|
+
atoms.push({
|
|
672
|
+
id: `atom-${roleName.toLowerCase().replace(/\s+/g, "-")}-${++seq}`,
|
|
673
|
+
text: `Wedge: "${w.claim}" — timing: ${w.timing}, risk: ${w.risk}`,
|
|
674
|
+
claim_kind: "wedge",
|
|
675
|
+
source_role: roleName,
|
|
676
|
+
source_artifact_type: artifactType,
|
|
677
|
+
allowed_challengers,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
// Category frame
|
|
682
|
+
if (roleOutput.category_frame) {
|
|
683
|
+
atoms.push({
|
|
684
|
+
id: `atom-${roleName.toLowerCase().replace(/\s+/g, "-")}-${++seq}`,
|
|
685
|
+
text: roleOutput.category_frame,
|
|
686
|
+
claim_kind: "category_frame",
|
|
687
|
+
source_role: roleName,
|
|
688
|
+
source_artifact_type: artifactType,
|
|
689
|
+
allowed_challengers,
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
break;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
case "ChallengeSet": {
|
|
696
|
+
// Challenges are NOT translated into atoms — they become ChallengeEdges
|
|
697
|
+
// in the cross-examine phase. Contrarian output feeds cross-exam directly.
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
return atoms;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Validate that a translated atom preserves all required provenance fields.
|
|
707
|
+
*
|
|
708
|
+
* @param {object} atom
|
|
709
|
+
* @returns {{ valid: boolean, issues: string[] }}
|
|
710
|
+
*/
|
|
711
|
+
export function validateAtomProvenance(atom) {
|
|
712
|
+
const issues = [];
|
|
713
|
+
if (!atom.id) issues.push("id is required");
|
|
714
|
+
if (!atom.text) issues.push("text is required");
|
|
715
|
+
if (!atom.claim_kind) issues.push("claim_kind is required");
|
|
716
|
+
if (!atom.source_role) issues.push("source_role is required");
|
|
717
|
+
if (!atom.source_artifact_type) issues.push("source_artifact_type is required");
|
|
718
|
+
if (!Array.isArray(atom.allowed_challengers)) issues.push("allowed_challengers must be an array");
|
|
719
|
+
return { valid: issues.length === 0, issues };
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Filter challenge edges to only those permitted by the cross-exam matrix.
|
|
724
|
+
*
|
|
725
|
+
* @param {Array} challenges - ChallengeEdge candidates
|
|
726
|
+
* @param {Array} atoms - Translated atoms with source_role
|
|
727
|
+
* @returns {{ permitted: Array, rejected: Array<{challenge: object, reason: string}> }}
|
|
728
|
+
*/
|
|
729
|
+
export function filterChallenges(challenges, atoms) {
|
|
730
|
+
const atomMap = new Map(atoms.map(a => [a.id, a]));
|
|
731
|
+
const permitted = [];
|
|
732
|
+
const rejected = [];
|
|
733
|
+
|
|
734
|
+
for (const c of challenges) {
|
|
735
|
+
const targetAtom = atomMap.get(c.target_claim_id);
|
|
736
|
+
if (!targetAtom) {
|
|
737
|
+
rejected.push({ challenge: c, reason: `Target atom "${c.target_claim_id}" not found` });
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
if (!canChallenge(c.challenger_role, targetAtom.source_role)) {
|
|
741
|
+
rejected.push({
|
|
742
|
+
challenge: c,
|
|
743
|
+
reason: `${c.challenger_role} is not permitted to challenge ${targetAtom.source_role} claims`,
|
|
744
|
+
});
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
permitted.push(c);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
return { permitted, rejected };
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
export { ARTIFACT_CLAIM_KINDS };
|
|
754
|
+
|
|
755
|
+
// ── Updated pipeline definition ─────────────────────────────────────────────
|
|
756
|
+
|
|
757
|
+
export const BRAINSTORM_V03_PIPELINE = [
|
|
758
|
+
"frame",
|
|
759
|
+
"analyze", // Role-native outputs (replaces "scout")
|
|
760
|
+
"normalize", // Translate role-native → shared atoms
|
|
761
|
+
"cross_examine", // Targeted challenges between roles
|
|
762
|
+
"rebut", // Original roles defend/narrow/retract
|
|
763
|
+
"synthesize", // Now has real dispute graph, not parallel notes
|
|
764
|
+
"expand",
|
|
765
|
+
"judge",
|
|
766
|
+
"return",
|
|
767
|
+
];
|
|
768
|
+
|
|
769
|
+
// ── Exports ─────────────────────────────────────────────────────────────────
|
|
770
|
+
|
|
771
|
+
export {
|
|
772
|
+
VALID_CHALLENGE_TYPES,
|
|
773
|
+
VALID_REBUTTAL_RESPONSES,
|
|
774
|
+
};
|