role-os 2.0.0 → 2.1.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,462 @@
1
+ /**
2
+ * Brainstorm Renderers — v0.4
3
+ *
4
+ * Layer 2: Human-legible rendering of structural analyst artifacts.
5
+ * Each renderer converts a dry role-native output into a distinct
6
+ * human register — without contaminating the law layer.
7
+ *
8
+ * Rule: Synthesis consumes structural artifacts, NEVER rendered prose.
9
+ * Rule: Users always have access to BOTH layers. Rendered is opt-in, not a replacement.
10
+ * Dev users may prefer the raw truth artifacts. Newer users get the rendered view.
11
+ * Renderers exist for human legibility and debate presentation only.
12
+ *
13
+ * Five render formats:
14
+ * Context → Boundary Memo (taxonomist)
15
+ * User Value → Field Notes (ethnographer)
16
+ * Mechanics → System Sketch (whiteboard engineer)
17
+ * Positioning → Claim Brief (strategist)
18
+ * Contrarian → Cross-Exam Transcript (litigator)
19
+ */
20
+
21
+ // ── Lexical bans per renderer ───────────────────────────────────────────────
22
+
23
+ /**
24
+ * Words and phrases each renderer CANNOT use.
25
+ * This is what kills "same senior engineer" voice.
26
+ * Bans target the vocabulary of OTHER roles, preventing voice convergence.
27
+ */
28
+ export const RENDERER_LEXICAL_BANS = {
29
+ "Context Analyst": {
30
+ banned: [
31
+ "pain point", "adoption", "resonant", "resonance", "compelling",
32
+ "users want", "users need", "developers prefer", "emotional",
33
+ "friction", "willingness", "desire", "pull",
34
+ "wedge", "positioning", "go-to-market", "competitive advantage",
35
+ "loop", "cascade", "failure mode", "dependency chain",
36
+ ],
37
+ voice: "taxonomist — noun-heavy, classification-first, boundary-obsessed",
38
+ },
39
+ "User Value Analyst": {
40
+ banned: [
41
+ "structural", "surface area", "infrastructure", "substrate",
42
+ "schema", "protocol", "specification", "taxonomy", "ontology",
43
+ "category boundary", "lineage", "genealogy", "precedent class",
44
+ "wedge", "claim timing", "market territory", "competitor",
45
+ "attestation", "cryptographic", "sandbox", "dependency chain",
46
+ ],
47
+ voice: "ethnographer — behavior-first, situation-anchored, comfortable with ambiguity",
48
+ },
49
+ "Mechanics Analyst": {
50
+ banned: [
51
+ "brand", "story", "narrative", "messaging", "pitch",
52
+ "emotional", "resonant", "compelling", "pain point",
53
+ "users want", "users feel", "developers prefer",
54
+ "wedge", "claim", "positioning", "territory", "timing",
55
+ "category", "taxonomy", "lineage", "genealogy",
56
+ ],
57
+ voice: "systems designer at whiteboard — if/then, state transitions, failure cascades",
58
+ },
59
+ "Positioning Analyst": {
60
+ banned: [
61
+ "schema", "table", "database", "endpoint", "API design",
62
+ "dependency chain", "failure mode", "cascade", "loop decomposition",
63
+ "sandbox", "attestation record", "cryptographic binding",
64
+ "taxonomy", "ontology", "genealogy", "precedent class",
65
+ "pain point", "adoption friction", "willingness signal",
66
+ ],
67
+ voice: "strategist in war room — territorial claims, timing, what is legal to say now",
68
+ },
69
+ "Contrarian Analyst": {
70
+ banned: [
71
+ "I suggest", "we should", "a better approach", "consider building",
72
+ "the product could", "my recommendation", "alternatively",
73
+ "opportunity", "upside", "promising", "exciting",
74
+ "users want", "users need", "the market",
75
+ ],
76
+ voice: "litigator — only questions and challenges, burden of proof on the claimant",
77
+ },
78
+ };
79
+
80
+ // ── Render schema definitions ───────────────────────────────────────────────
81
+
82
+ /**
83
+ * Each renderer produces a role-native document format.
84
+ * These are NOT the structural artifacts (those feed synthesis).
85
+ * These are human-readable presentations for debate transcripts.
86
+ */
87
+ export const RENDER_SCHEMAS = {
88
+ "Context Analyst": {
89
+ format: "Boundary Memo",
90
+ sections: [
91
+ { name: "Classification", description: "What this is, stated as category membership" },
92
+ { name: "Adjacent spaces", description: "What this is next to, with boundary precision" },
93
+ { name: "Lineage", description: "Where this came from, stated as descent claims" },
94
+ { name: "Exclusions", description: "What this is NOT, stated as hard boundaries" },
95
+ ],
96
+ style_rules: [
97
+ "Open every section with a noun phrase, not a verb",
98
+ "Use 'is' and 'is not' more than 'should' or 'could'",
99
+ "Name categories before describing them",
100
+ "Never explain why something matters — only what it is",
101
+ ],
102
+ },
103
+ "User Value Analyst": {
104
+ format: "Field Notes",
105
+ sections: [
106
+ { name: "Observed situations", description: "Concrete scenes where the behavior happens" },
107
+ { name: "What they reach for", description: "Actions taken, tools grabbed, workarounds improvised" },
108
+ { name: "What they avoid", description: "Behaviors conspicuously absent, doors not opened" },
109
+ { name: "Signals of readiness", description: "Moments where they would act if something existed" },
110
+ ],
111
+ style_rules: [
112
+ "Write in present tense, observational voice",
113
+ "Anchor every claim in a concrete situation, not an abstraction",
114
+ "Quote imagined developer speech in single quotes: 'is this safe?'",
115
+ "Never name a system component — only name what a person does or feels",
116
+ "Use short sentences. Sentence fragments are fine.",
117
+ ],
118
+ },
119
+ "Mechanics Analyst": {
120
+ format: "System Sketch",
121
+ sections: [
122
+ { name: "Core loops", description: "Named input→transform→output cycles" },
123
+ { name: "Dependency map", description: "What must exist before what" },
124
+ { name: "Break points", description: "Where the system fails and what cascades" },
125
+ { name: "Irreducibles", description: "Mechanisms that cannot be deferred or simplified" },
126
+ ],
127
+ style_rules: [
128
+ "Use arrows (→) for flow",
129
+ "Name components before describing them",
130
+ "State dependencies as 'X requires Y' not 'Y enables X'",
131
+ "State failures as 'if X breaks, then Y' — always causal",
132
+ "No adjectives. No adverbs. Only nouns and verbs.",
133
+ ],
134
+ },
135
+ "Positioning Analyst": {
136
+ format: "Claim Brief",
137
+ sections: [
138
+ { name: "Territory", description: "What claim this could own" },
139
+ { name: "Current occupants", description: "Who holds adjacent territory now" },
140
+ { name: "Timing", description: "When each claim becomes legal to make" },
141
+ { name: "Forbidden claims", description: "What cannot be honestly said yet, and why" },
142
+ ],
143
+ style_rules: [
144
+ "State claims as single declarative sentences",
145
+ "Attach timing to every claim: 'legal now' / 'legal after X' / 'never legal'",
146
+ "Name the risk of each claim in one phrase",
147
+ "Never describe how to build — only what to say and when",
148
+ ],
149
+ },
150
+ "Contrarian Analyst": {
151
+ format: "Cross-Exam Transcript",
152
+ sections: [
153
+ { name: "Challenges", description: "Each challenge targets a specific claim ID" },
154
+ { name: "Exposed assumptions", description: "Unstated premises the claims rest on" },
155
+ { name: "Burden shifts", description: "Where the burden of proof should move" },
156
+ ],
157
+ style_rules: [
158
+ "Address claims by ID: 'Regarding ctx-11:'",
159
+ "State the challenge as a question when possible: 'On what basis...?'",
160
+ "Never propose alternatives — only expose weakness",
161
+ "End each challenge with what evidence would resolve it",
162
+ ],
163
+ },
164
+ };
165
+
166
+ // ── Lexical ban validator ───────────────────────────────────────────────────
167
+
168
+ /**
169
+ * Validate rendered text against lexical bans for a role.
170
+ *
171
+ * @param {string} roleName
172
+ * @param {string} renderedText
173
+ * @returns {{ valid: boolean, violations: string[] }}
174
+ */
175
+ export function validateRenderedText(roleName, renderedText) {
176
+ const bans = RENDERER_LEXICAL_BANS[roleName];
177
+ if (!bans) return { valid: true, violations: [] };
178
+
179
+ const violations = [];
180
+ const lower = renderedText.toLowerCase();
181
+
182
+ for (const phrase of bans.banned) {
183
+ if (lower.includes(phrase.toLowerCase())) {
184
+ violations.push(`"${phrase}" — banned for ${roleName} (voice: ${bans.voice})`);
185
+ }
186
+ }
187
+
188
+ return { valid: violations.length === 0, violations };
189
+ }
190
+
191
+ // ── Debate transcript generator ─────────────────────────────────────────────
192
+
193
+ /**
194
+ * Generate a structured debate transcript from challenge/rebuttal edges.
195
+ * This is the presentation layer — it makes the dispute graph readable
196
+ * as a conversation between distinct voices.
197
+ *
198
+ * @param {Array} challenges - ChallengeEdge + cross-exam challenges
199
+ * @param {Array} rebuttals - RebuttalEdge responses
200
+ * @param {Array} atoms - Provenance atoms (for claim text lookup)
201
+ * @returns {string} Formatted debate transcript
202
+ */
203
+ export function generateDebateTranscript(challenges, rebuttals, atoms) {
204
+ const atomMap = new Map(atoms.map(a => [a.id, a]));
205
+ const rebuttalMap = new Map(rebuttals.map(r => [r.target_claim_id, r]));
206
+
207
+ const lines = [];
208
+ lines.push("## Debate Transcript");
209
+ lines.push("");
210
+
211
+ // Group challenges by target atom
212
+ const byTarget = new Map();
213
+ for (const c of challenges) {
214
+ if (!byTarget.has(c.target_claim_id)) byTarget.set(c.target_claim_id, []);
215
+ byTarget.get(c.target_claim_id).push(c);
216
+ }
217
+
218
+ for (const [atomId, atomChallenges] of byTarget) {
219
+ const atom = atomMap.get(atomId);
220
+ const claimText = atom ? atom.text : atomId;
221
+ const sourceRole = atom ? atom.source_role : "unknown";
222
+
223
+ lines.push(`### ${atomId} (${sourceRole})`);
224
+ lines.push(`> ${claimText}`);
225
+ lines.push("");
226
+
227
+ for (const c of atomChallenges) {
228
+ lines.push(`**${c.challenger_role}** [${c.challenge_type}]:`);
229
+ lines.push(`${c.reason}`);
230
+ lines.push("");
231
+ }
232
+
233
+ const rebuttal = rebuttalMap.get(atomId);
234
+ if (rebuttal) {
235
+ const verb = rebuttal.response === "defend" ? "DEFENDS"
236
+ : rebuttal.response === "narrow" ? "NARROWS"
237
+ : rebuttal.response === "retract" ? "RETRACTS"
238
+ : "LEAVES UNRESOLVED";
239
+ lines.push(`**${rebuttal.source_role}** ${verb}:`);
240
+ lines.push(`${rebuttal.note}`);
241
+ lines.push("");
242
+ }
243
+
244
+ lines.push("---");
245
+ lines.push("");
246
+ }
247
+
248
+ // Summary stats
249
+ const narrowed = rebuttals.filter(r => r.response === "narrow").length;
250
+ const defended = rebuttals.filter(r => r.response === "defend").length;
251
+ const retracted = rebuttals.filter(r => r.response === "retract").length;
252
+ const unresolved = rebuttals.filter(r => r.response === "unresolved").length;
253
+
254
+ lines.push("### Dispute Summary");
255
+ lines.push(`- Challenges issued: ${challenges.length}`);
256
+ lines.push(`- Claims narrowed: ${narrowed}`);
257
+ lines.push(`- Claims defended: ${defended}`);
258
+ lines.push(`- Claims retracted: ${retracted}`);
259
+ lines.push(`- Claims unresolved: ${unresolved}`);
260
+
261
+ return lines.join("\n");
262
+ }
263
+
264
+ // ── Render format validator ─────────────────────────────────────────────────
265
+
266
+ /**
267
+ * Validate that a rendered document follows its role's format.
268
+ * Checks for required sections and style rule compliance.
269
+ *
270
+ * @param {string} roleName
271
+ * @param {string} renderedText
272
+ * @returns {{ valid: boolean, missing_sections: string[], style_issues: string[] }}
273
+ */
274
+ export function validateRenderFormat(roleName, renderedText) {
275
+ const schema = RENDER_SCHEMAS[roleName];
276
+ if (!schema) return { valid: true, missing_sections: [], style_issues: [] };
277
+
278
+ const missing_sections = [];
279
+ const lower = renderedText.toLowerCase();
280
+
281
+ for (const section of schema.sections) {
282
+ // Check for section header presence (flexible matching)
283
+ const sectionLower = section.name.toLowerCase();
284
+ if (!lower.includes(sectionLower)) {
285
+ missing_sections.push(section.name);
286
+ }
287
+ }
288
+
289
+ return {
290
+ valid: missing_sections.length === 0,
291
+ missing_sections,
292
+ style_issues: [], // Style rules are guidance, not automated checks
293
+ };
294
+ }
295
+
296
+ // ── Result formatter ─────────────────────────────────────────────────────────
297
+
298
+ /**
299
+ * Format a complete brainstorm result as a saveable markdown document.
300
+ *
301
+ * Produces a single document with both layers, dispute summary, verdict,
302
+ * and evidence trail — suitable for saving, diffing, and sharing.
303
+ *
304
+ * @param {object} opts
305
+ * @param {object} opts.request - The BrainstormRequest
306
+ * @param {object} opts.synthesisReport - SynthesisReport from synthesize phase
307
+ * @param {object} opts.judgeReport - JudgeReport from judge phase
308
+ * @param {object} opts.evidenceTrail - Evidence trail stats
309
+ * @param {Array} opts.challenges - ChallengeEdge array
310
+ * @param {Array} opts.rebuttals - RebuttalEdge array
311
+ * @param {Array} opts.atoms - Provenance atoms
312
+ * @param {object} [opts.renderedArtifacts] - Map of roleName → rendered text (optional)
313
+ * @param {string} [opts.layer] - "truth" | "render" | "both" (default: "both")
314
+ * @returns {string} Formatted markdown document
315
+ */
316
+ export function formatBrainstormResult(opts) {
317
+ const {
318
+ request,
319
+ synthesisReport,
320
+ judgeReport,
321
+ evidenceTrail,
322
+ challenges = [],
323
+ rebuttals = [],
324
+ atoms = [],
325
+ renderedArtifacts = null,
326
+ layer = "both",
327
+ } = opts;
328
+
329
+ const lines = [];
330
+
331
+ // ── Header ──
332
+ lines.push(`# Brainstorm: ${request.topic}`);
333
+ lines.push("");
334
+ lines.push(`**Objective:** ${request.objective}`);
335
+ if (request.audience) lines.push(`**Audience:** ${request.audience}`);
336
+ lines.push(`**Evidence mode:** ${request.evidence_mode} | **Breadth:** ${request.breadth} | **Depth:** ${request.depth}`);
337
+ lines.push("");
338
+
339
+ // ── Verdict ──
340
+ lines.push("## Verdict");
341
+ lines.push("");
342
+ lines.push(`**Disposition:** ${judgeReport.disposition} | **Quality:** ${judgeReport.overall_quality}`);
343
+ lines.push("");
344
+
345
+ if (judgeReport.per_direction) {
346
+ lines.push("| Direction | Verdict | Action |");
347
+ lines.push("|-----------|---------|--------|");
348
+ for (const d of judgeReport.per_direction) {
349
+ lines.push(`| ${d.direction_id} | ${d.verdict} | ${d.action || "—"} |`);
350
+ }
351
+ lines.push("");
352
+ }
353
+
354
+ if (judgeReport.reasons) {
355
+ for (const r of judgeReport.reasons) {
356
+ lines.push(`- ${r}`);
357
+ }
358
+ lines.push("");
359
+ }
360
+
361
+ // ── Synthesis ──
362
+ lines.push("## Directions");
363
+ lines.push("");
364
+ lines.push(`> ${synthesisReport.topic_model}`);
365
+ lines.push("");
366
+
367
+ if (synthesisReport.advancing_directions) {
368
+ for (const dir of synthesisReport.advancing_directions) {
369
+ lines.push(`### ${dir.name}`);
370
+ lines.push("");
371
+ lines.push(dir.thesis);
372
+ lines.push("");
373
+ if (dir.why_it_matters) lines.push(`**Why it matters:** ${dir.why_it_matters}`);
374
+ if (dir.supporting_atoms && dir.supporting_atoms.length > 0) {
375
+ lines.push(`**Evidence:** ${dir.supporting_atoms.join(", ")}`);
376
+ }
377
+ if (dir.risks && dir.risks.length > 0) {
378
+ lines.push(`**Risks:** ${dir.risks.join("; ")}`);
379
+ }
380
+ lines.push("");
381
+ }
382
+ }
383
+
384
+ if (synthesisReport.archived_directions && synthesisReport.archived_directions.length > 0) {
385
+ lines.push("### Archived");
386
+ lines.push("");
387
+ for (const d of synthesisReport.archived_directions) {
388
+ lines.push(`- **${d.name}** — ${d.reason}`);
389
+ }
390
+ lines.push("");
391
+ }
392
+
393
+ // ── Dispute graph ──
394
+ if (challenges.length > 0) {
395
+ lines.push("## Dispute");
396
+ lines.push("");
397
+
398
+ const narrowed = rebuttals.filter(r => r.response === "narrow").length;
399
+ const defended = rebuttals.filter(r => r.response === "defend").length;
400
+ const retracted = rebuttals.filter(r => r.response === "retract").length;
401
+ const unresolved = rebuttals.filter(r => r.response === "unresolved").length;
402
+
403
+ lines.push(`${challenges.length} challenges issued. ${narrowed} narrowed, ${defended} defended, ${retracted} retracted, ${unresolved} unresolved.`);
404
+ lines.push("");
405
+
406
+ const rebuttalMap = new Map(rebuttals.map(r => [r.target_claim_id, r]));
407
+ for (const c of challenges) {
408
+ const reb = rebuttalMap.get(c.target_claim_id);
409
+ const outcome = reb
410
+ ? reb.response === "narrow" ? "NARROWED"
411
+ : reb.response === "defend" ? "DEFENDED"
412
+ : reb.response === "retract" ? "RETRACTED"
413
+ : "UNRESOLVED"
414
+ : "no rebuttal";
415
+ lines.push(`- **${c.target_claim_id}** [${c.challenge_type}] — ${outcome}`);
416
+ lines.push(` ${c.reason}`);
417
+ if (reb) lines.push(` → ${reb.note}`);
418
+ }
419
+ lines.push("");
420
+ }
421
+
422
+ // ── Tensions ──
423
+ if (synthesisReport.tensions && synthesisReport.tensions.length > 0) {
424
+ lines.push("## Tensions");
425
+ lines.push("");
426
+ for (const t of synthesisReport.tensions) {
427
+ lines.push(`- ${t}`);
428
+ }
429
+ lines.push("");
430
+ }
431
+
432
+ // ── Rendered artifacts (Layer 2, opt-in) ──
433
+ if ((layer === "both" || layer === "render") && renderedArtifacts) {
434
+ lines.push("## Rendered Artifacts");
435
+ lines.push("");
436
+ for (const [role, text] of Object.entries(renderedArtifacts)) {
437
+ const schema = RENDER_SCHEMAS[role];
438
+ const format = schema ? schema.format : role;
439
+ lines.push(`### ${format} (${role})`);
440
+ lines.push("");
441
+ lines.push(text);
442
+ lines.push("");
443
+ }
444
+ }
445
+
446
+ // ── Evidence trail ──
447
+ lines.push("## Evidence Trail");
448
+ lines.push("");
449
+ if (evidenceTrail) {
450
+ lines.push(`- Analysts: ${(evidenceTrail.scouts_run || []).join(", ")}`);
451
+ lines.push(`- Atoms after normalize: ${evidenceTrail.atoms_after_normalize}`);
452
+ lines.push(`- Directions considered: ${evidenceTrail.directions_considered} → advancing: ${evidenceTrail.directions_advancing}`);
453
+ lines.push(`- Judge loops: ${evidenceTrail.judge_loops} → disposition: ${evidenceTrail.judge_disposition}`);
454
+ }
455
+ lines.push("");
456
+
457
+ // ── Footer ──
458
+ lines.push("---");
459
+ lines.push(`*Generated by Role OS brainstorm mission (v0.4)*`);
460
+
461
+ return lines.join("\n");
462
+ }