role-os 2.3.1 → 2.6.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,251 @@
1
+ /**
2
+ * Local-panel seat — a SECOND, family-different verifier seat for the citation gate, layered on
3
+ * prism. Where prism RETRIEVES (the deterministic existence floor + the source title/abstract) and
4
+ * runs its own groundedness lens, this seat re-judges each citation prism vouched for with an
5
+ * independent grounded-entailment PANEL running entirely on local models (Qwen + Mistral via
6
+ * llama-swap, the `offload` CLI). It is decorrelated from the Claude generator by construction
7
+ * (no Anthropic model in the panel) and from prism's single groundedness model (3 seats, ≥2
8
+ * families, conservative majority).
9
+ *
10
+ * Why it can only TIGHTEN: the panel's measured property (tensor-engine-knowledge wave-5 #156) is
11
+ * ZERO false-confirms — a 3-seat conservative-majority panel never stamps a false claim
12
+ * "supported". So a panel DISAGREEMENT on a citation prism marked `supported` is a real
13
+ * false-confirm signal: we downgrade that citation's gate accept -> escalate (a human checkpoint
14
+ * with a contrastive message). The panel NEVER turns a non-accept into an accept, and the
15
+ * deterministic existence floor (`fabricated` -> blocking) always dominates. EXTERNAL_VERIFIER
16
+ * (workflow-standard #6), now runnable locally for free.
17
+ *
18
+ * Read-only (shells the read-only `offload verify`); no compensator. Mirrors the inject-`exec`,
19
+ * closed-gate-on-unreachable discipline of verify-citations.mjs. See
20
+ * design/citation-verification-runner.md (Local-panel seat).
21
+ */
22
+
23
+ import { execFileSync } from "node:child_process";
24
+
25
+ /** The offload command, overridable for non-default rigs (defaults match offload.py's README). */
26
+ export const DEFAULT_OFFLOAD_PYTHON = process.env.OFFLOAD_PYTHON || "python";
27
+ export const DEFAULT_OFFLOAD_SCRIPT =
28
+ process.env.OFFLOAD_SCRIPT || "E:/AI-Models/studio-local/offload.py";
29
+
30
+ /**
31
+ * Build the evidence string the panel judges the claim against: prism's retrieved source title +
32
+ * its FULL retrieved abstract (prism v1.0+ surfaces `source_abstract`), falling back to the single
33
+ * supporting span on older prism builds. Judging against the whole abstract — not one sentence —
34
+ * stops the panel from escalating a faithful claim that the abstract entails but prism's single
35
+ * span does not (the wave-6 e2e Kambhampati false-escalation). A strict panel that STILL cannot
36
+ * entail the claim from the full abstract is exactly the false-confirm worth catching.
37
+ * @returns {string} evidence, or "" when prism surfaced nothing to judge against.
38
+ */
39
+ export function buildEvidence({ source_title, source_abstract, span } = {}) {
40
+ const title = (source_title || "").trim();
41
+ const body = (source_abstract || "").trim() || (span || "").trim();
42
+ if (!title && !body) return "";
43
+ return [title ? `Title: ${title}` : "", body].filter(Boolean).join("\n\n");
44
+ }
45
+
46
+ /** Default exec — execFileSync, capturing stdout even on a non-zero exit, no shell (args verbatim). */
47
+ function defaultOffloadExec(cmd, args, { timeout, cwd, env }) {
48
+ try {
49
+ const stdout = execFileSync(cmd, args, {
50
+ cwd,
51
+ timeout,
52
+ env,
53
+ encoding: "utf8",
54
+ stdio: ["ignore", "pipe", "pipe"],
55
+ maxBuffer: 16 * 1024 * 1024,
56
+ });
57
+ return { status: 0, stdout, stderr: "" };
58
+ } catch (err) {
59
+ if (err.code === "ENOENT") throw err; // missing python/script -> caller escalates
60
+ return {
61
+ status: err.status ?? 1,
62
+ stdout: (err.stdout || "").toString(),
63
+ stderr: (err.stderr || "").toString(),
64
+ };
65
+ }
66
+ }
67
+
68
+ function tryParseJson(text) {
69
+ const s = (text || "").trim();
70
+ if (!s) return null;
71
+ try {
72
+ return JSON.parse(s);
73
+ } catch {
74
+ const a = s.indexOf("{");
75
+ const b = s.lastIndexOf("}");
76
+ if (a !== -1 && b > a) {
77
+ try {
78
+ return JSON.parse(s.slice(a, b + 1));
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+ return null;
84
+ }
85
+ }
86
+
87
+ /**
88
+ * @typedef {object} PanelCitation
89
+ * @property {string|null} id
90
+ * @property {string|null} identifier
91
+ * @property {string} claim
92
+ * @property {string} evidence
93
+ */
94
+
95
+ /**
96
+ * @typedef {object} PanelResult
97
+ * @property {boolean} requested always true when this ran
98
+ * @property {boolean} reachable false iff offload/llama-swap could not be reached at all
99
+ * @property {string[]} seats the actual model tags the panel used (PIN_PER_STEP)
100
+ * @property {number} checked citations the panel actually adjudicated
101
+ * @property {object[]} perCitation { id, identifier, panel_verdict, seats }
102
+ * @property {object[]} disagreements { id, identifier, prism, panel } where prism=supported, panel≠supported
103
+ * @property {string} [detail]
104
+ */
105
+
106
+ /**
107
+ * Run the offload entailment panel over the citations prism marked `supported` (the only ones whose
108
+ * acceptance the panel can challenge). Each call is `offload verify --panel --json`; the actual seat
109
+ * models come back in the panel JSON and are recorded for the receipt (PIN_PER_STEP).
110
+ *
111
+ * @param {PanelCitation[]} supported citations prism vouched for, with evidence already built
112
+ * @param {object} [options]
113
+ * @param {Function} [options.exec] injectable (cmd,args,{timeout,cwd,env}) -> {status,stdout,stderr}
114
+ * @param {string} [options.python] default DEFAULT_OFFLOAD_PYTHON
115
+ * @param {string} [options.script] default DEFAULT_OFFLOAD_SCRIPT
116
+ * @param {string} [options.base] LLAMASWAP_BASE passed to the child (default offload's own)
117
+ * @param {number} [options.timeout] per-call ms (default 300000 — first call may swap 3 models)
118
+ * @param {string} [options.cwd]
119
+ * @returns {PanelResult}
120
+ */
121
+ export function runOffloadPanel(supported, options = {}) {
122
+ const {
123
+ exec = defaultOffloadExec,
124
+ python = DEFAULT_OFFLOAD_PYTHON,
125
+ script = DEFAULT_OFFLOAD_SCRIPT,
126
+ base = process.env.LLAMASWAP_BASE || "",
127
+ timeout = 300_000,
128
+ cwd = process.cwd(),
129
+ } = options;
130
+
131
+ const env = {
132
+ ...process.env,
133
+ PYTHONIOENCODING: "utf-8",
134
+ PYTHONUTF8: "1",
135
+ ...(base ? { LLAMASWAP_BASE: base } : {}),
136
+ };
137
+
138
+ const perCitation = [];
139
+ const disagreements = [];
140
+ const seatModels = new Set();
141
+ let reachable = false;
142
+ let anyError = false;
143
+ let detail = "";
144
+
145
+ for (const c of supported) {
146
+ if (!c.evidence) {
147
+ // prism marked it supported but surfaced no span/title to re-judge — note, do not downgrade
148
+ // (absence of evidence is not a contradiction); surfaced in the report.
149
+ perCitation.push({ id: c.id, identifier: c.identifier, panel_verdict: "no_evidence", seats: [] });
150
+ continue;
151
+ }
152
+ const args = [
153
+ script, "verify", "--panel", "--json",
154
+ "--claim", c.claim,
155
+ "--evidence", c.evidence,
156
+ ];
157
+ let res;
158
+ try {
159
+ res = exec(python, args, { timeout, cwd, env });
160
+ } catch (err) {
161
+ // ENOENT (no python / no script) or spawn failure -> the panel is unreachable as a whole.
162
+ detail = `offload not runnable: ${err.code || err.message}`;
163
+ anyError = true;
164
+ perCitation.push({ id: c.id, identifier: c.identifier, panel_verdict: "error", seats: [] });
165
+ if (err.code === "ENOENT") break; // no point retrying the rest with a missing binary
166
+ continue;
167
+ }
168
+ const parsed = tryParseJson((res.stdout || "").toString());
169
+ if (!parsed || typeof parsed.verdict !== "string") {
170
+ anyError = true;
171
+ detail = detail || `offload produced no parseable panel JSON (exit ${res.status}): ${(res.stderr || res.stdout || "").toString().slice(0, 200)}`;
172
+ perCitation.push({ id: c.id, identifier: c.identifier, panel_verdict: "error", seats: [] });
173
+ continue;
174
+ }
175
+ reachable = true;
176
+ const seats = Array.isArray(parsed.seats) ? parsed.seats.map((s) => s.model).filter(Boolean) : [];
177
+ seats.forEach((m) => seatModels.add(m));
178
+ const verdict = String(parsed.verdict).toLowerCase();
179
+ perCitation.push({ id: c.id, identifier: c.identifier, panel_verdict: verdict, seats });
180
+ if (verdict !== "supported") {
181
+ disagreements.push({ id: c.id, identifier: c.identifier, prism: "supported", panel: verdict });
182
+ }
183
+ }
184
+
185
+ const checked = perCitation.filter((p) => p.panel_verdict === "supported" || disagreements.some((d) => d.id === p.id)).length;
186
+ return {
187
+ requested: true,
188
+ reachable,
189
+ seats: [...seatModels],
190
+ checked,
191
+ perCitation,
192
+ disagreements,
193
+ ...(detail ? { detail } : {}),
194
+ // unreachable iff we never got a single parseable verdict AND something errored
195
+ ...(anyError && !reachable ? { unreachable: true } : {}),
196
+ };
197
+ }
198
+
199
+ /**
200
+ * Contrastive escalation message (workflow-standard #5 / Buçinca 2024): name what the dispatch
201
+ * assumed, then what the independent panel found — so the human reviews the disagreement, not a
202
+ * bare "uncertain".
203
+ */
204
+ function contrastiveDetail(disagreements) {
205
+ const lead = disagreements
206
+ .slice(0, 3)
207
+ .map((d) => `${d.identifier || d.id}: prism read the source as SUPPORTING the claim; the local Qwen+Mistral panel found "${d.panel}" on prism's own span`)
208
+ .join("; ");
209
+ const more = disagreements.length > 3 ? ` (+${disagreements.length - 3} more)` : "";
210
+ return `local entailment panel disagrees with prism on ${disagreements.length} citation(s) — review before accepting. ${lead}${more}`;
211
+ }
212
+
213
+ /**
214
+ * Apply the panel to a gate result — MONOTONE-TIGHTENING. Only ever downgrades accept -> escalate;
215
+ * never loosens, never overrides the existence floor (blocking).
216
+ *
217
+ * - gate passing + panel DISAGREES on ≥1 supported citation -> escalate (local_panel_disagreement)
218
+ * - gate passing + panel UNREACHABLE (and it was requested) -> escalate (local_panel_unreachable)
219
+ * ("an unreachable gate is a closed gate" — same invariant prism uses)
220
+ * - gate already blocking/advisory -> unchanged (panel adds notes only)
221
+ *
222
+ * @param {object} gate GateResult from gateCitations / runCitationGate
223
+ * @param {PanelResult} panel
224
+ * @returns {object} gate (possibly downgraded), with `local_panel` attached
225
+ */
226
+ export function applyLocalPanel(gate, panel) {
227
+ const annotated = { ...gate, local_panel: panel };
228
+ if (gate.blocking || !gate.pass) return annotated; // floor + non-pass dominate; panel only annotates
229
+
230
+ if (panel.unreachable) {
231
+ return {
232
+ ...annotated,
233
+ verdict: "escalate",
234
+ pass: false,
235
+ advisory: true,
236
+ reason: "local_panel_unreachable",
237
+ detail: panel.detail || "the local verifier panel could not be reached (offload/llama-swap down)",
238
+ };
239
+ }
240
+ if (panel.disagreements.length > 0) {
241
+ return {
242
+ ...annotated,
243
+ verdict: "escalate",
244
+ pass: false,
245
+ advisory: true,
246
+ reason: "local_panel_disagreement",
247
+ detail: contrastiveDetail(panel.disagreements),
248
+ };
249
+ }
250
+ return annotated; // panel agrees (or had nothing to challenge) -> pass stands
251
+ }