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.
@@ -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
+ };