@rvry/mcp 0.12.1 → 0.13.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/README.md +29 -40
- package/dist/index.d.ts +1 -1
- package/dist/index.js +316 -23
- package/dist/local-writer.d.ts +102 -16
- package/dist/local-writer.js +351 -56
- package/dist/setup.js +152 -234
- package/package.json +1 -1
package/dist/local-writer.d.ts
CHANGED
|
@@ -1,30 +1,116 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Local session writer -- writes a
|
|
3
|
-
* when a session completes.
|
|
2
|
+
* Local session writer -- writes a client-owned markdown harvest to
|
|
3
|
+
* .rvry/<op>/<timestamp>-<slug>/harvest.md when a session completes.
|
|
4
|
+
*
|
|
5
|
+
* Optionally emits per-round files (round-N-<mode>.md) when the slash-command
|
|
6
|
+
* shim passes `rounds[]` (triggered by `--record` flag on the user side).
|
|
4
7
|
*
|
|
5
8
|
* PROTOCOL SURFACE: This file produces user-facing output. It must NEVER contain
|
|
6
|
-
* constraint IDs, detection scores, mode names
|
|
7
|
-
* Only question + harvest
|
|
9
|
+
* constraint IDs, detection scores, mode names used as gate verdicts, or any
|
|
10
|
+
* engine internals. Only question + client-supplied harvest + recorded bodies.
|
|
8
11
|
*/
|
|
9
|
-
|
|
12
|
+
export type ChallengeVerdict = 'GO' | 'CONDITIONAL_GO' | 'NO_GO';
|
|
13
|
+
export type ChallengeBias = 'anchoring' | 'framing' | 'authority' | 'sunk_cost' | 'availability' | 'confirmation' | 'none';
|
|
14
|
+
export type ChallengeBlastRadius = 'contained' | 'cascading';
|
|
15
|
+
export type ChallengeAssumptionConfidence = 'high' | 'medium' | 'low';
|
|
16
|
+
export interface ChallengeAssumption {
|
|
17
|
+
assumption: string;
|
|
18
|
+
confidence: ChallengeAssumptionConfidence;
|
|
19
|
+
ifWrong: string;
|
|
20
|
+
bias: ChallengeBias;
|
|
21
|
+
biasEvidence: string;
|
|
22
|
+
}
|
|
23
|
+
export interface ChallengeFailureMode {
|
|
24
|
+
component: string;
|
|
25
|
+
howItFails: string;
|
|
26
|
+
blastRadius: ChallengeBlastRadius;
|
|
27
|
+
detection: string;
|
|
28
|
+
}
|
|
29
|
+
export interface ChallengeCounterArgument {
|
|
30
|
+
argument: string;
|
|
31
|
+
rebuttal: string;
|
|
32
|
+
}
|
|
33
|
+
export type ProblemSolveAdversarialVerdict = 'PASSED' | 'CAVEATS' | 'NEEDS_REVISION';
|
|
34
|
+
export type ProblemSolveBias = 'anchoring' | 'framing' | 'authority' | 'sunk_cost' | 'availability' | 'confirmation';
|
|
35
|
+
export interface ProblemSolveBiasOperating {
|
|
36
|
+
bias: ProblemSolveBias;
|
|
37
|
+
evidence: string;
|
|
38
|
+
sourceReflex?: string;
|
|
39
|
+
correction?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface ProblemSolveFrameReversal {
|
|
42
|
+
originalFraming?: string;
|
|
43
|
+
invertedFraming?: string;
|
|
44
|
+
wouldReachSameConclusion?: boolean;
|
|
45
|
+
}
|
|
46
|
+
export interface ProblemSolveBiasAudit {
|
|
47
|
+
biasesOperating?: ProblemSolveBiasOperating[];
|
|
48
|
+
decisionStable?: boolean;
|
|
49
|
+
frameReversal?: ProblemSolveFrameReversal;
|
|
50
|
+
}
|
|
51
|
+
export interface ClientHarvest {
|
|
52
|
+
title?: string | null;
|
|
53
|
+
summary?: string;
|
|
54
|
+
keyFindings?: string[];
|
|
55
|
+
shimmer?: string;
|
|
56
|
+
openQuestions?: string[];
|
|
57
|
+
followUps?: Array<{
|
|
58
|
+
question: string;
|
|
59
|
+
rationale: string;
|
|
60
|
+
}>;
|
|
61
|
+
verdict?: ChallengeVerdict;
|
|
62
|
+
confidence?: number;
|
|
63
|
+
mitigations?: string[];
|
|
64
|
+
assumptions?: ChallengeAssumption[];
|
|
65
|
+
edgeCases?: string[];
|
|
66
|
+
failureModes?: ChallengeFailureMode[];
|
|
67
|
+
counterArguments?: ChallengeCounterArgument[];
|
|
68
|
+
biasAudit?: ProblemSolveBiasAudit;
|
|
69
|
+
adversarialVerdict?: ProblemSolveAdversarialVerdict;
|
|
70
|
+
reversalConditions?: string[];
|
|
71
|
+
}
|
|
72
|
+
export interface ClientRound {
|
|
73
|
+
round: number;
|
|
74
|
+
mode: string;
|
|
75
|
+
body: string;
|
|
76
|
+
}
|
|
77
|
+
export interface LocalSessionData {
|
|
10
78
|
sessionId: string;
|
|
11
79
|
operationType: string;
|
|
12
80
|
question: string;
|
|
13
81
|
rounds: number;
|
|
14
|
-
harvest
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
82
|
+
/** Engine-side harvest (telemetry). Kept for potential future use; not written. */
|
|
83
|
+
harvest?: unknown;
|
|
84
|
+
/** Client-authored harvest (the synthesis Claude produced). Preferred source. */
|
|
85
|
+
clientHarvest?: ClientHarvest | null;
|
|
86
|
+
/** Per-round bodies for `--record` mode. */
|
|
87
|
+
clientRounds?: ClientRound[] | null;
|
|
88
|
+
/** Override "now" for deterministic tests. */
|
|
89
|
+
now?: Date;
|
|
90
|
+
/** Override base directory (default: process.cwd()). */
|
|
91
|
+
baseDir?: string;
|
|
23
92
|
}
|
|
24
93
|
/**
|
|
25
|
-
*
|
|
94
|
+
* Build the main harvest.md content. If `clientHarvest` is missing, emits the
|
|
95
|
+
* graceful-degradation fallback (question + upgrade notice).
|
|
96
|
+
*
|
|
97
|
+
* Per-op rendering (FR-16):
|
|
98
|
+
* - Challenge: verdict line below H1; Required Mitigations (when CONDITIONAL_GO);
|
|
99
|
+
* Assumptions table; Edge Cases bullets; Failure Modes table; Counter-Arguments.
|
|
100
|
+
* - Problem-solve: Adversarial Verdict line below H1; Bias Audit; Reversal Conditions.
|
|
101
|
+
* - Other ops: render unchanged.
|
|
102
|
+
*
|
|
103
|
+
* All op-specific sections are conditional on the corresponding field being
|
|
104
|
+
* present and non-empty. Absent field = absent section, never an empty section.
|
|
105
|
+
*/
|
|
106
|
+
export declare function buildMarkdown(data: LocalSessionData, now: Date): string;
|
|
107
|
+
/**
|
|
108
|
+
* Build a per-round markdown file body.
|
|
109
|
+
*/
|
|
110
|
+
export declare function buildRoundMarkdown(round: ClientRound, now: Date): string;
|
|
111
|
+
/**
|
|
112
|
+
* Write session folder: harvest.md (always) + optional per-round files.
|
|
26
113
|
*
|
|
27
114
|
* Disabled via RVRY_LOCAL_SESSIONS=0. Fails silently on any error.
|
|
28
115
|
*/
|
|
29
116
|
export declare function writeLocalSession(data: LocalSessionData): Promise<void>;
|
|
30
|
-
export {};
|
package/dist/local-writer.js
CHANGED
|
@@ -1,16 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Local session writer -- writes a
|
|
3
|
-
* when a session completes.
|
|
2
|
+
* Local session writer -- writes a client-owned markdown harvest to
|
|
3
|
+
* .rvry/<op>/<timestamp>-<slug>/harvest.md when a session completes.
|
|
4
|
+
*
|
|
5
|
+
* Optionally emits per-round files (round-N-<mode>.md) when the slash-command
|
|
6
|
+
* shim passes `rounds[]` (triggered by `--record` flag on the user side).
|
|
4
7
|
*
|
|
5
8
|
* PROTOCOL SURFACE: This file produces user-facing output. It must NEVER contain
|
|
6
|
-
* constraint IDs, detection scores, mode names
|
|
7
|
-
* Only question + harvest
|
|
9
|
+
* constraint IDs, detection scores, mode names used as gate verdicts, or any
|
|
10
|
+
* engine internals. Only question + client-supplied harvest + recorded bodies.
|
|
8
11
|
*/
|
|
9
12
|
import { mkdirSync, writeFileSync } from 'fs';
|
|
10
13
|
import { join } from 'path';
|
|
14
|
+
/** Op label for H1 headings. */
|
|
15
|
+
const OP_LABELS = {
|
|
16
|
+
deepthink: 'RVRY Deepthink',
|
|
17
|
+
problem_solve: 'RVRY Problem-Solve',
|
|
18
|
+
think: 'RVRY Think',
|
|
19
|
+
challenge: 'RVRY Challenge',
|
|
20
|
+
meta: 'RVRY Meta',
|
|
21
|
+
};
|
|
22
|
+
function opLabel(operationType) {
|
|
23
|
+
return OP_LABELS[operationType] ?? `RVRY ${operationType}`;
|
|
24
|
+
}
|
|
25
|
+
function opFolder(operationType) {
|
|
26
|
+
return operationType.replace(/_/g, '-');
|
|
27
|
+
}
|
|
11
28
|
/**
|
|
12
|
-
* Slugify
|
|
13
|
-
* Lowercases, replaces non-alphanumeric runs with hyphens, trims, and caps at maxLen chars.
|
|
29
|
+
* Slugify text for filenames/folders.
|
|
14
30
|
*/
|
|
15
31
|
function slugify(text, maxLen = 50) {
|
|
16
32
|
return text
|
|
@@ -20,9 +36,7 @@ function slugify(text, maxLen = 50) {
|
|
|
20
36
|
.slice(0, maxLen)
|
|
21
37
|
.replace(/-+$/, '');
|
|
22
38
|
}
|
|
23
|
-
/**
|
|
24
|
-
* Format a Date as YYYYMMDD-HHMM for filenames.
|
|
25
|
-
*/
|
|
39
|
+
/** YYYYMMDD-HHMM for folder names. */
|
|
26
40
|
function formatTimestamp(date) {
|
|
27
41
|
const y = date.getFullYear().toString();
|
|
28
42
|
const mo = (date.getMonth() + 1).toString().padStart(2, '0');
|
|
@@ -31,9 +45,7 @@ function formatTimestamp(date) {
|
|
|
31
45
|
const mi = date.getMinutes().toString().padStart(2, '0');
|
|
32
46
|
return `${y}${mo}${d}-${h}${mi}`;
|
|
33
47
|
}
|
|
34
|
-
/**
|
|
35
|
-
* Format a Date as YYYY-MM-DD HH:MM for YAML frontmatter.
|
|
36
|
-
*/
|
|
48
|
+
/** YYYY-MM-DD HH:MM for italic date line. */
|
|
37
49
|
function formatDate(date) {
|
|
38
50
|
const y = date.getFullYear().toString();
|
|
39
51
|
const mo = (date.getMonth() + 1).toString().padStart(2, '0');
|
|
@@ -43,72 +55,355 @@ function formatDate(date) {
|
|
|
43
55
|
return `${y}-${mo}-${d} ${h}:${mi}`;
|
|
44
56
|
}
|
|
45
57
|
/**
|
|
46
|
-
*
|
|
58
|
+
* Title-case the first 4 words of a question as an H1 fallback.
|
|
59
|
+
*/
|
|
60
|
+
function fallbackTitleFromQuestion(question) {
|
|
61
|
+
const words = question
|
|
62
|
+
.trim()
|
|
63
|
+
.split(/\s+/)
|
|
64
|
+
.slice(0, 4)
|
|
65
|
+
.map((w) => w.replace(/[^\p{L}\p{N}'-]/gu, ''))
|
|
66
|
+
.filter((w) => w.length > 0);
|
|
67
|
+
if (words.length === 0)
|
|
68
|
+
return 'Session';
|
|
69
|
+
return words
|
|
70
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
|
|
71
|
+
.join(' ');
|
|
72
|
+
}
|
|
73
|
+
function isNonEmptyString(v) {
|
|
74
|
+
return typeof v === 'string' && v.trim().length > 0;
|
|
75
|
+
}
|
|
76
|
+
function isNonEmptyArray(v) {
|
|
77
|
+
return Array.isArray(v) && v.length > 0;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Build the main harvest.md content. If `clientHarvest` is missing, emits the
|
|
81
|
+
* graceful-degradation fallback (question + upgrade notice).
|
|
82
|
+
*
|
|
83
|
+
* Per-op rendering (FR-16):
|
|
84
|
+
* - Challenge: verdict line below H1; Required Mitigations (when CONDITIONAL_GO);
|
|
85
|
+
* Assumptions table; Edge Cases bullets; Failure Modes table; Counter-Arguments.
|
|
86
|
+
* - Problem-solve: Adversarial Verdict line below H1; Bias Audit; Reversal Conditions.
|
|
87
|
+
* - Other ops: render unchanged.
|
|
88
|
+
*
|
|
89
|
+
* All op-specific sections are conditional on the corresponding field being
|
|
90
|
+
* present and non-empty. Absent field = absent section, never an empty section.
|
|
47
91
|
*/
|
|
48
|
-
function buildMarkdown(data, now) {
|
|
92
|
+
export function buildMarkdown(data, now) {
|
|
93
|
+
const label = opLabel(data.operationType);
|
|
49
94
|
const lines = [];
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
// Summary
|
|
62
|
-
lines.push('## Summary');
|
|
63
|
-
lines.push(data.harvest.summary);
|
|
64
|
-
lines.push('');
|
|
65
|
-
// Key Findings
|
|
66
|
-
if (data.harvest.keyFindings.length > 0) {
|
|
67
|
-
lines.push('## Key Findings');
|
|
68
|
-
for (const finding of data.harvest.keyFindings) {
|
|
69
|
-
lines.push(`- ${finding}`);
|
|
70
|
-
}
|
|
95
|
+
const harvest = data.clientHarvest;
|
|
96
|
+
if (!harvest) {
|
|
97
|
+
// Fallback: no client synthesis provided.
|
|
98
|
+
const title = fallbackTitleFromQuestion(data.question);
|
|
99
|
+
lines.push(`# ${label} — ${title}`);
|
|
100
|
+
lines.push('');
|
|
101
|
+
lines.push(`*${formatDate(now)}*`);
|
|
102
|
+
lines.push('');
|
|
103
|
+
lines.push('### Prompt');
|
|
104
|
+
lines.push('');
|
|
105
|
+
lines.push(data.question);
|
|
71
106
|
lines.push('');
|
|
107
|
+
lines.push('## Summary');
|
|
108
|
+
lines.push('');
|
|
109
|
+
lines.push('*(Synthesis not provided — upgrade your slash commands by re-running `rvry-mcp setup`.)*');
|
|
110
|
+
lines.push('');
|
|
111
|
+
return lines.join('\n');
|
|
112
|
+
}
|
|
113
|
+
const title = isNonEmptyString(harvest.title)
|
|
114
|
+
? harvest.title.trim()
|
|
115
|
+
: fallbackTitleFromQuestion(data.question);
|
|
116
|
+
lines.push(`# ${label} — ${title}`);
|
|
117
|
+
// Op-specific verdict line directly below H1.
|
|
118
|
+
if (data.operationType === 'challenge') {
|
|
119
|
+
const verdictLine = renderChallengeVerdictLine(harvest);
|
|
120
|
+
if (verdictLine)
|
|
121
|
+
lines.push(verdictLine);
|
|
122
|
+
}
|
|
123
|
+
else if (data.operationType === 'problem_solve') {
|
|
124
|
+
const verdictLine = renderProblemSolveVerdictLine(harvest);
|
|
125
|
+
if (verdictLine)
|
|
126
|
+
lines.push(verdictLine);
|
|
127
|
+
}
|
|
128
|
+
lines.push(`*${formatDate(now)}*`);
|
|
129
|
+
lines.push('');
|
|
130
|
+
lines.push('### Prompt');
|
|
131
|
+
lines.push('');
|
|
132
|
+
lines.push(data.question);
|
|
133
|
+
lines.push('');
|
|
134
|
+
if (isNonEmptyString(harvest.shimmer)) {
|
|
135
|
+
lines.push('## Shimmer');
|
|
136
|
+
lines.push('');
|
|
137
|
+
lines.push(harvest.shimmer.trim());
|
|
138
|
+
lines.push('');
|
|
139
|
+
}
|
|
140
|
+
if (isNonEmptyString(harvest.summary)) {
|
|
141
|
+
lines.push('## Summary');
|
|
142
|
+
lines.push('');
|
|
143
|
+
lines.push(harvest.summary.trim());
|
|
144
|
+
lines.push('');
|
|
145
|
+
}
|
|
146
|
+
// Challenge-specific structured sections (in declared rendering order).
|
|
147
|
+
if (data.operationType === 'challenge') {
|
|
148
|
+
appendChallengeMitigations(lines, harvest);
|
|
149
|
+
appendChallengeAssumptions(lines, harvest);
|
|
150
|
+
appendChallengeEdgeCases(lines, harvest);
|
|
151
|
+
appendChallengeFailureModes(lines, harvest);
|
|
152
|
+
appendChallengeCounterArguments(lines, harvest);
|
|
153
|
+
}
|
|
154
|
+
// Problem-solve-specific structured sections.
|
|
155
|
+
if (data.operationType === 'problem_solve') {
|
|
156
|
+
appendProblemSolveBiasAudit(lines, harvest);
|
|
157
|
+
appendProblemSolveReversalConditions(lines, harvest);
|
|
158
|
+
}
|
|
159
|
+
if (isNonEmptyArray(harvest.keyFindings)) {
|
|
160
|
+
const findings = harvest.keyFindings.filter(isNonEmptyString);
|
|
161
|
+
if (findings.length > 0) {
|
|
162
|
+
lines.push('## Key Findings');
|
|
163
|
+
lines.push('');
|
|
164
|
+
for (const f of findings)
|
|
165
|
+
lines.push(`- ${f.trim()}`);
|
|
166
|
+
lines.push('');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (isNonEmptyArray(harvest.openQuestions)) {
|
|
170
|
+
const qs = harvest.openQuestions.filter(isNonEmptyString);
|
|
171
|
+
if (qs.length > 0) {
|
|
172
|
+
lines.push('## Open Questions');
|
|
173
|
+
lines.push('');
|
|
174
|
+
for (const q of qs)
|
|
175
|
+
lines.push(`- ${q.trim()}`);
|
|
176
|
+
lines.push('');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (isNonEmptyArray(harvest.followUps)) {
|
|
180
|
+
const fus = harvest.followUps.filter((f) => f && isNonEmptyString(f.question));
|
|
181
|
+
if (fus.length > 0) {
|
|
182
|
+
lines.push('## Follow-ups');
|
|
183
|
+
lines.push('');
|
|
184
|
+
for (const f of fus) {
|
|
185
|
+
if (isNonEmptyString(f.rationale)) {
|
|
186
|
+
lines.push(`- ${f.question.trim()} — ${f.rationale.trim()}`);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
lines.push(`- ${f.question.trim()}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
lines.push('');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return lines.join('\n');
|
|
196
|
+
}
|
|
197
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
198
|
+
// Challenge renderers
|
|
199
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
200
|
+
const CHALLENGE_VERDICT_LABELS = {
|
|
201
|
+
GO: 'GO',
|
|
202
|
+
CONDITIONAL_GO: 'CONDITIONAL_GO',
|
|
203
|
+
NO_GO: 'NO_GO',
|
|
204
|
+
};
|
|
205
|
+
function renderChallengeVerdictLine(harvest) {
|
|
206
|
+
const verdict = harvest.verdict;
|
|
207
|
+
if (!verdict || !CHALLENGE_VERDICT_LABELS[verdict])
|
|
208
|
+
return null;
|
|
209
|
+
const confidenceText = typeof harvest.confidence === 'number'
|
|
210
|
+
? ` (confidence: ${harvest.confidence.toFixed(1)})`
|
|
211
|
+
: '';
|
|
212
|
+
return `**Verdict: ${CHALLENGE_VERDICT_LABELS[verdict]}${confidenceText}**`;
|
|
213
|
+
}
|
|
214
|
+
function appendChallengeMitigations(lines, harvest) {
|
|
215
|
+
// Conditional: only render when verdict is CONDITIONAL_GO and mitigations are non-empty.
|
|
216
|
+
if (harvest.verdict !== 'CONDITIONAL_GO')
|
|
217
|
+
return;
|
|
218
|
+
const mitigations = (harvest.mitigations ?? []).filter(isNonEmptyString);
|
|
219
|
+
if (mitigations.length === 0)
|
|
220
|
+
return;
|
|
221
|
+
lines.push('## Required Mitigations');
|
|
222
|
+
lines.push('');
|
|
223
|
+
for (const m of mitigations)
|
|
224
|
+
lines.push(`- ${m.trim()}`);
|
|
225
|
+
lines.push('');
|
|
226
|
+
}
|
|
227
|
+
function appendChallengeAssumptions(lines, harvest) {
|
|
228
|
+
const assumptions = (harvest.assumptions ?? []).filter((a) => a && isNonEmptyString(a.assumption));
|
|
229
|
+
if (assumptions.length === 0)
|
|
230
|
+
return;
|
|
231
|
+
lines.push('## Assumptions');
|
|
232
|
+
lines.push('');
|
|
233
|
+
lines.push('| Assumption | Confidence | If Wrong | Bias | Bias Evidence |');
|
|
234
|
+
lines.push('| --- | --- | --- | --- | --- |');
|
|
235
|
+
for (const a of assumptions) {
|
|
236
|
+
lines.push(`| ${escapeTableCell(a.assumption)} | ${escapeTableCell(a.confidence ?? '')} | ${escapeTableCell(a.ifWrong ?? '')} | ${escapeTableCell(a.bias ?? '')} | ${escapeTableCell(a.biasEvidence ?? '')} |`);
|
|
237
|
+
}
|
|
238
|
+
lines.push('');
|
|
239
|
+
}
|
|
240
|
+
function appendChallengeEdgeCases(lines, harvest) {
|
|
241
|
+
const cases = (harvest.edgeCases ?? []).filter(isNonEmptyString);
|
|
242
|
+
if (cases.length === 0)
|
|
243
|
+
return;
|
|
244
|
+
lines.push('## Edge Cases');
|
|
245
|
+
lines.push('');
|
|
246
|
+
for (const c of cases)
|
|
247
|
+
lines.push(`- ${c.trim()}`);
|
|
248
|
+
lines.push('');
|
|
249
|
+
}
|
|
250
|
+
function appendChallengeFailureModes(lines, harvest) {
|
|
251
|
+
const modes = (harvest.failureModes ?? []).filter((m) => m && isNonEmptyString(m.component));
|
|
252
|
+
if (modes.length === 0)
|
|
253
|
+
return;
|
|
254
|
+
lines.push('## Failure Modes');
|
|
255
|
+
lines.push('');
|
|
256
|
+
lines.push('| Component | How It Fails | Blast Radius | Detection |');
|
|
257
|
+
lines.push('| --- | --- | --- | --- |');
|
|
258
|
+
for (const m of modes) {
|
|
259
|
+
lines.push(`| ${escapeTableCell(m.component)} | ${escapeTableCell(m.howItFails ?? '')} | ${escapeTableCell(m.blastRadius ?? '')} | ${escapeTableCell(m.detection ?? '')} |`);
|
|
72
260
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
261
|
+
lines.push('');
|
|
262
|
+
}
|
|
263
|
+
function appendChallengeCounterArguments(lines, harvest) {
|
|
264
|
+
const args = (harvest.counterArguments ?? []).filter((c) => c && isNonEmptyString(c.argument));
|
|
265
|
+
if (args.length === 0)
|
|
266
|
+
return;
|
|
267
|
+
lines.push('## Counter-Arguments');
|
|
268
|
+
lines.push('');
|
|
269
|
+
for (const c of args) {
|
|
270
|
+
lines.push(`- **For:** ${c.argument.trim()}`);
|
|
271
|
+
if (isNonEmptyString(c.rebuttal)) {
|
|
272
|
+
lines.push(` **Against:** ${c.rebuttal.trim()}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
lines.push('');
|
|
276
|
+
}
|
|
277
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
278
|
+
// Problem-solve renderers
|
|
279
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
280
|
+
const PROBLEM_SOLVE_VERDICT_LABELS = {
|
|
281
|
+
PASSED: 'PASSED',
|
|
282
|
+
CAVEATS: 'CAVEATS',
|
|
283
|
+
NEEDS_REVISION: 'NEEDS_REVISION',
|
|
284
|
+
};
|
|
285
|
+
function renderProblemSolveVerdictLine(harvest) {
|
|
286
|
+
const verdict = harvest.adversarialVerdict;
|
|
287
|
+
if (!verdict || !PROBLEM_SOLVE_VERDICT_LABELS[verdict])
|
|
288
|
+
return null;
|
|
289
|
+
return `**Adversarial Verdict: ${PROBLEM_SOLVE_VERDICT_LABELS[verdict]}**`;
|
|
290
|
+
}
|
|
291
|
+
function appendProblemSolveBiasAudit(lines, harvest) {
|
|
292
|
+
const audit = harvest.biasAudit;
|
|
293
|
+
if (!audit)
|
|
294
|
+
return;
|
|
295
|
+
const operating = (audit.biasesOperating ?? []).filter((b) => b && isNonEmptyString(b.bias) && isNonEmptyString(b.evidence));
|
|
296
|
+
const hasReversal = audit.frameReversal && (isNonEmptyString(audit.frameReversal.originalFraming) ||
|
|
297
|
+
isNonEmptyString(audit.frameReversal.invertedFraming) ||
|
|
298
|
+
typeof audit.frameReversal.wouldReachSameConclusion === 'boolean');
|
|
299
|
+
const hasStability = typeof audit.decisionStable === 'boolean';
|
|
300
|
+
if (operating.length === 0 && !hasReversal && !hasStability)
|
|
301
|
+
return;
|
|
302
|
+
lines.push('## Bias Audit');
|
|
303
|
+
lines.push('');
|
|
304
|
+
if (operating.length > 0) {
|
|
305
|
+
for (const b of operating) {
|
|
306
|
+
lines.push(`- **${b.bias.trim()}**: ${b.evidence.trim()}`);
|
|
307
|
+
if (isNonEmptyString(b.sourceReflex)) {
|
|
308
|
+
lines.push(` - Source reflex: ${b.sourceReflex.trim()}`);
|
|
309
|
+
}
|
|
310
|
+
if (isNonEmptyString(b.correction)) {
|
|
311
|
+
lines.push(` - Correction: ${b.correction.trim()}`);
|
|
312
|
+
}
|
|
78
313
|
}
|
|
79
314
|
lines.push('');
|
|
80
315
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
lines.push('
|
|
84
|
-
|
|
85
|
-
|
|
316
|
+
if (hasStability) {
|
|
317
|
+
lines.push(`Decision stable under bias check: ${audit.decisionStable ? 'yes' : 'no'}`);
|
|
318
|
+
lines.push('');
|
|
319
|
+
}
|
|
320
|
+
if (hasReversal && audit.frameReversal) {
|
|
321
|
+
lines.push('### Frame Reversal');
|
|
322
|
+
lines.push('');
|
|
323
|
+
if (isNonEmptyString(audit.frameReversal.originalFraming)) {
|
|
324
|
+
lines.push(`- Original: ${audit.frameReversal.originalFraming.trim()}`);
|
|
325
|
+
}
|
|
326
|
+
if (isNonEmptyString(audit.frameReversal.invertedFraming)) {
|
|
327
|
+
lines.push(`- Inverted: ${audit.frameReversal.invertedFraming.trim()}`);
|
|
328
|
+
}
|
|
329
|
+
if (typeof audit.frameReversal.wouldReachSameConclusion === 'boolean') {
|
|
330
|
+
lines.push(`- Same conclusion under reversal: ${audit.frameReversal.wouldReachSameConclusion ? 'yes' : 'no'}`);
|
|
86
331
|
}
|
|
87
332
|
lines.push('');
|
|
88
333
|
}
|
|
334
|
+
}
|
|
335
|
+
function appendProblemSolveReversalConditions(lines, harvest) {
|
|
336
|
+
const conds = (harvest.reversalConditions ?? []).filter(isNonEmptyString);
|
|
337
|
+
if (conds.length === 0)
|
|
338
|
+
return;
|
|
339
|
+
lines.push('## Reversal Conditions');
|
|
340
|
+
lines.push('');
|
|
341
|
+
for (const c of conds)
|
|
342
|
+
lines.push(`- ${c.trim()}`);
|
|
343
|
+
lines.push('');
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Escape pipe and newline characters in markdown table cells.
|
|
347
|
+
* Keeps cell content on a single line and prevents pipe characters from
|
|
348
|
+
* breaking column alignment.
|
|
349
|
+
*/
|
|
350
|
+
function escapeTableCell(value) {
|
|
351
|
+
return value
|
|
352
|
+
.replace(/\r?\n/g, ' ')
|
|
353
|
+
.replace(/\|/g, '\\|')
|
|
354
|
+
.trim();
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Build a per-round markdown file body.
|
|
358
|
+
*/
|
|
359
|
+
export function buildRoundMarkdown(round, now) {
|
|
360
|
+
const modeLabel = round.mode
|
|
361
|
+
? round.mode.charAt(0).toUpperCase() + round.mode.slice(1)
|
|
362
|
+
: 'Round';
|
|
363
|
+
const lines = [];
|
|
364
|
+
lines.push(`# Round ${round.round} — ${modeLabel}`);
|
|
365
|
+
lines.push('');
|
|
366
|
+
lines.push(`*${formatDate(now)}*`);
|
|
367
|
+
lines.push('');
|
|
368
|
+
lines.push(round.body ?? '');
|
|
369
|
+
lines.push('');
|
|
89
370
|
return lines.join('\n');
|
|
90
371
|
}
|
|
372
|
+
function roundFilename(round) {
|
|
373
|
+
const modeSafe = (round.mode || 'round')
|
|
374
|
+
.toLowerCase()
|
|
375
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
376
|
+
.replace(/^-+|-+$/g, '') || 'round';
|
|
377
|
+
return `round-${round.round}-${modeSafe}.md`;
|
|
378
|
+
}
|
|
91
379
|
/**
|
|
92
|
-
* Write
|
|
380
|
+
* Write session folder: harvest.md (always) + optional per-round files.
|
|
93
381
|
*
|
|
94
382
|
* Disabled via RVRY_LOCAL_SESSIONS=0. Fails silently on any error.
|
|
95
383
|
*/
|
|
96
384
|
export async function writeLocalSession(data) {
|
|
97
|
-
// Check opt-out
|
|
98
385
|
if (process.env.RVRY_LOCAL_SESSIONS === '0')
|
|
99
386
|
return;
|
|
100
387
|
try {
|
|
101
|
-
const now = new Date();
|
|
388
|
+
const now = data.now ?? new Date();
|
|
102
389
|
const timestamp = formatTimestamp(now);
|
|
103
|
-
const slug = slugify(data.question);
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
writeFileSync(
|
|
390
|
+
const slug = slugify(data.question) || 'session';
|
|
391
|
+
const folderName = `${timestamp}-${slug}`;
|
|
392
|
+
const baseDir = data.baseDir ?? process.cwd();
|
|
393
|
+
const sessionDir = join(baseDir, '.rvry', opFolder(data.operationType), folderName);
|
|
394
|
+
mkdirSync(sessionDir, { recursive: true });
|
|
395
|
+
const harvestPath = join(sessionDir, 'harvest.md');
|
|
396
|
+
writeFileSync(harvestPath, buildMarkdown(data, now), 'utf-8');
|
|
397
|
+
if (isNonEmptyArray(data.clientRounds)) {
|
|
398
|
+
for (const r of data.clientRounds) {
|
|
399
|
+
if (!r || typeof r.round !== 'number' || typeof r.mode !== 'string')
|
|
400
|
+
continue;
|
|
401
|
+
const fp = join(sessionDir, roundFilename(r));
|
|
402
|
+
writeFileSync(fp, buildRoundMarkdown(r, now), 'utf-8');
|
|
403
|
+
}
|
|
404
|
+
}
|
|
110
405
|
}
|
|
111
406
|
catch {
|
|
112
|
-
// Fail silently --
|
|
407
|
+
// Fail silently -- convenience feature, not critical path.
|
|
113
408
|
}
|
|
114
409
|
}
|