@ijfw/memory-server 1.5.6 → 1.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.
- package/bin/ijfw-dashboard +20 -1
- package/package.json +4 -3
- package/src/audit-roster.js +89 -12
- package/src/brain/tiered-llm.js +57 -7
- package/src/cross-orchestrator-cli.js +344 -4
- package/src/cross-project-search.js +39 -1
- package/src/dashboard-server.js +7 -1
- package/src/dream/runner.mjs +560 -8
- package/src/handlers/brain-handler.js +101 -1
- package/src/importers/discover.js +1 -1
- package/src/memory/bench-metrics.js +289 -0
- package/src/memory/benchmark.js +1 -1
- package/src/memory/search.js +53 -1
- package/src/orchestrator/plan-checker.js +1 -1
- package/src/profile/audit.js +671 -0
- package/src/profile/capture.js +871 -0
- package/src/profile/derive-dialectic.js +242 -0
- package/src/profile/derive-heuristic.js +733 -0
- package/src/profile/derive.js +156 -0
- package/src/profile/egress.js +306 -0
- package/src/profile/eval/build-real-probes.mjs +197 -0
- package/src/profile/eval/corpus-from-reddit.mjs +166 -0
- package/src/profile/eval/corpus-from-reddit.test.mjs +121 -0
- package/src/profile/eval/corpus-from-transcripts.mjs +264 -0
- package/src/profile/eval/gate-b-behavior.mjs +420 -0
- package/src/profile/eval/gate-b-decision-run.mjs +171 -0
- package/src/profile/eval/gate-b-decision-run.test.mjs +141 -0
- package/src/profile/eval/gate-b-run.mjs +417 -0
- package/src/profile/eval/gate-b-run.test.mjs +204 -0
- package/src/profile/eval/gate-c-capture.mjs +323 -0
- package/src/profile/eval/harness.mjs +551 -0
- package/src/profile/eval/instrument-validation.mjs +248 -0
- package/src/profile/eval/instrument-validation.test.mjs +125 -0
- package/src/profile/eval/multi-subject-harness.mjs +106 -0
- package/src/profile/eval/multi-subject-harness.test.mjs +99 -0
- package/src/profile/eval/personas.test.mjs +83 -0
- package/src/profile/eval/plumbing.test.mjs +69 -0
- package/src/profile/eval/prereg.mjs +130 -0
- package/src/profile/eval/prereg.test.mjs +78 -0
- package/src/profile/eval/real-corpus.test.mjs +103 -0
- package/src/profile/eval/real-personas.mjs +109 -0
- package/src/profile/eval/run-real-corpus-concurrent.mjs +407 -0
- package/src/profile/eval/run-real-corpus.mjs +358 -0
- package/src/profile/eval/slug-quality.mjs +464 -0
- package/src/profile/eval/stylometry-features.js +85 -0
- package/src/profile/eval/stylometry-reference.js +16 -0
- package/src/profile/eval/stylometry.js +224 -0
- package/src/profile/eval/stylometry.test.mjs +103 -0
- package/src/profile/eval/synthetic-personas.js +91 -0
- package/src/profile/eval/verifier-features.mjs +170 -0
- package/src/profile/eval/verifier-logreg.mjs +74 -0
- package/src/profile/eval/verifier-pair.mjs +122 -0
- package/src/profile/eval/verifier-reference.mjs +68 -0
- package/src/profile/eval/verifier-scorer.mjs +30 -0
- package/src/profile/eval/wrong-target-control.mjs +168 -0
- package/src/profile/eval/wrong-target-control.test.mjs +124 -0
- package/src/profile/exemplar-capture.js +232 -0
- package/src/profile/exemplar-retrieve.js +138 -0
- package/src/profile/exemplar-store.js +314 -0
- package/src/profile/lock.js +64 -0
- package/src/profile/merge.js +624 -0
- package/src/profile/path-policy.js +213 -0
- package/src/profile/precision-stamp.mjs +151 -0
- package/src/profile/render-brief.js +717 -0
- package/src/profile/schema.js +244 -0
- package/src/profile/sensitivity.js +249 -0
- package/src/profile/serve.js +345 -0
- package/src/profile/store.js +261 -0
- package/src/profile/telemetry.js +289 -0
- package/src/recovery/checkpoint.js +7 -1
- package/src/server.js +185 -14
- package/src/.registry-meta-key.pem +0 -3
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* profile/derive-dialectic.js — Cross-system profile bus, PHASE P3.3.
|
|
3
|
+
*
|
|
4
|
+
* The BOUNDED, CORROBORATED local-LLM dialectic tier. It sits ABOVE the zero-LLM
|
|
5
|
+
* heuristic floor (derive-heuristic.js) and ADDS inferences the heuristic cannot
|
|
6
|
+
* reach — but only under three HARD, code-enforced guards. The model PROPOSES;
|
|
7
|
+
* this module DISPOSES. We never trust the model's own confidence or its claim
|
|
8
|
+
* of evidence; the corroboration and the confidence cap are computed in code.
|
|
9
|
+
*
|
|
10
|
+
* GUARD 1 — bounded input (NUMERIC cap). The digest fed to the model is a
|
|
11
|
+
* sampled/windowed slice under a hard CHARACTER cap (DIALECTIC_MAX_DIGEST_CHARS)
|
|
12
|
+
* plus a bounded output token cap. Corpus size (1k vs 100k sessions) must NOT
|
|
13
|
+
* scale per-cycle spend — the digest is the MOST-RECENT window, truncated.
|
|
14
|
+
*
|
|
15
|
+
* GUARD 2 — ≥K cross-session corroboration. A dialectic inference is admitted
|
|
16
|
+
* ONLY when the digest window spans ≥ DIALECTIC_MIN_CORROBORATION (K=2)
|
|
17
|
+
* DISTINCT sessions. A single-session corpus can therefore NEVER mint a
|
|
18
|
+
* trait — the honest unit of corroboration is the session, computed here,
|
|
19
|
+
* not asserted by the model. Each admitted inference is stamped with the
|
|
20
|
+
* corroborating session ids and evidence_count = the corroboration count.
|
|
21
|
+
*
|
|
22
|
+
* GUARD 3 — LOW confidence floor. Whatever confidence the model emits, the
|
|
23
|
+
* admitted inference is capped at DIALECTIC_MAX_CONFIDENCE (well under the
|
|
24
|
+
* 0.7 heuristic-correction default). The dialectic must never mint a
|
|
25
|
+
* high-confidence trait — it is a corroboration signal, not an authority.
|
|
26
|
+
*
|
|
27
|
+
* SELF-CONTAINED MOAT CONTRACT: this module imports ONLY ./schema.js. It takes
|
|
28
|
+
* its LLM access as an injected `transport` (a function), never importing the
|
|
29
|
+
* cloud path (tiered-llm's Anthropic caller) and never reaching the serving
|
|
30
|
+
* path. The serve/render modules must never import this file — the import-graph
|
|
31
|
+
* moat guard (P4.5) depends on it. On ANY transport error or unparseable output
|
|
32
|
+
* the module degrades to an EMPTY additive delta ({ inferences: [] }).
|
|
33
|
+
*
|
|
34
|
+
* Zero deps. ESM. Node built-ins only (none needed).
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { makeInference } from './schema.js';
|
|
38
|
+
|
|
39
|
+
/** Hard character cap on the digest handed to the model (GUARD 1). */
|
|
40
|
+
export const DIALECTIC_MAX_DIGEST_CHARS = 4000;
|
|
41
|
+
|
|
42
|
+
/** Bounded output token cap for the model call (GUARD 1). */
|
|
43
|
+
export const DIALECTIC_MAX_TOKENS = 512;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Maximum number of MOST-RECENT sessions sampled into the digest. A small,
|
|
47
|
+
* fixed window means corpus size cannot scale spend (GUARD 1). 40 rows of
|
|
48
|
+
* compact metadata sit comfortably under DIALECTIC_MAX_DIGEST_CHARS; the char
|
|
49
|
+
* cap is the belt, this is the suspenders.
|
|
50
|
+
*/
|
|
51
|
+
export const DIALECTIC_DIGEST_WINDOW = 40;
|
|
52
|
+
|
|
53
|
+
/** ≥K cross-session corroboration before ANY trait is minted (GUARD 2). */
|
|
54
|
+
export const DIALECTIC_MIN_CORROBORATION = 2;
|
|
55
|
+
|
|
56
|
+
/** LOW confidence cap — strictly under the 0.7 heuristic default (GUARD 3). */
|
|
57
|
+
export const DIALECTIC_MAX_CONFIDENCE = 0.5;
|
|
58
|
+
|
|
59
|
+
/** Coerce a signals bundle into the per-session style row array. */
|
|
60
|
+
function styleRows(signals) {
|
|
61
|
+
if (!signals || typeof signals !== 'object') return [];
|
|
62
|
+
if (Array.isArray(signals.style)) return signals.style;
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Distinct, ordered session ids present in a row list. */
|
|
67
|
+
function distinctSessions(rows) {
|
|
68
|
+
const seen = [];
|
|
69
|
+
const set = new Set();
|
|
70
|
+
for (const r of rows) {
|
|
71
|
+
const id = r && (r.session_id || r.sessionId);
|
|
72
|
+
if (typeof id === 'string' && id && !set.has(id)) {
|
|
73
|
+
set.add(id);
|
|
74
|
+
seen.push(id);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return seen;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* buildDigest(signals) -> string. A compact, MOST-RECENT-window, char-capped
|
|
82
|
+
* textual digest of the per-session metadata. Deterministic. The window keeps
|
|
83
|
+
* spend constant across corpus sizes; the slice() is a hard final truncation so
|
|
84
|
+
* the cap holds even if a single row is pathologically large.
|
|
85
|
+
*/
|
|
86
|
+
export function buildDigest(signals) {
|
|
87
|
+
const rows = styleRows(signals);
|
|
88
|
+
if (rows.length === 0) return '';
|
|
89
|
+
// Take the MOST-RECENT window (the tail of the chronological list). We do not
|
|
90
|
+
// sort (rows arrive chronological by capture); a tail-slice is the recency
|
|
91
|
+
// window and is O(window), not O(corpus).
|
|
92
|
+
const window = rows.slice(-DIALECTIC_DIGEST_WINDOW);
|
|
93
|
+
const lines = [];
|
|
94
|
+
for (const r of window) {
|
|
95
|
+
if (!r || typeof r !== 'object') continue;
|
|
96
|
+
// Compact, fixed-field rendering — never the raw object (keeps each line
|
|
97
|
+
// bounded and the digest predictable).
|
|
98
|
+
const f = (v) => (v === undefined || v === null ? '' : v);
|
|
99
|
+
lines.push(
|
|
100
|
+
`sid=${f(r.session_id || r.sessionId)} host=${f(r.host)} `
|
|
101
|
+
+ `len=${f(r.avg_msg_chars)} emoji=${f(r.emoji_per_msg)} `
|
|
102
|
+
+ `code=${f(r.code_block_ratio)} formal=${f(r.formality_markers)} `
|
|
103
|
+
+ `cadence=${f(r.turn_cadence_per_min)} msgs=${f(r.msg_count)}`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
const text = lines.join('\n');
|
|
107
|
+
// Hard final cap (GUARD 1) — truncate, never exceed.
|
|
108
|
+
return text.length > DIALECTIC_MAX_DIGEST_CHARS
|
|
109
|
+
? text.slice(0, DIALECTIC_MAX_DIGEST_CHARS)
|
|
110
|
+
: text;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The bounded instruction template wrapped around the digest. Fixed-size. */
|
|
114
|
+
function buildPrompt(digest) {
|
|
115
|
+
return (
|
|
116
|
+
'You are profiling a developer from per-session interaction METADATA '
|
|
117
|
+
+ '(statistics only, never transcripts). Propose DURABLE cross-session '
|
|
118
|
+
+ 'traits as JSON: {"inferences":[{"kind":"trait","subject":"<short '
|
|
119
|
+
+ 'phrase>","value":"<short>","confidence":<0..1>}]}. Only propose a trait '
|
|
120
|
+
+ 'visible across MULTIPLE sessions. Return ONLY JSON.\n\n'
|
|
121
|
+
+ 'SESSION METADATA DIGEST:\n'
|
|
122
|
+
+ digest
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Tolerant JSON extraction: parse the whole string, else the first {...} block. */
|
|
127
|
+
function parseInferences(text) {
|
|
128
|
+
if (typeof text !== 'string' || !text.trim()) return [];
|
|
129
|
+
const tryParse = (s) => {
|
|
130
|
+
try {
|
|
131
|
+
const obj = JSON.parse(s);
|
|
132
|
+
if (obj && Array.isArray(obj.inferences)) return obj.inferences;
|
|
133
|
+
} catch { /* fall through */ }
|
|
134
|
+
return null;
|
|
135
|
+
};
|
|
136
|
+
let arr = tryParse(text.trim());
|
|
137
|
+
if (!arr) {
|
|
138
|
+
const m = /\{[\s\S]*\}/.exec(text);
|
|
139
|
+
if (m) arr = tryParse(m[0]);
|
|
140
|
+
}
|
|
141
|
+
return Array.isArray(arr) ? arr : [];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* deriveDialectic(signals, opts) -> { inferences: Inference[] }.
|
|
146
|
+
*
|
|
147
|
+
* @param {object} signals { style: Row[], ... } — the same bundle deriveHeuristic reads
|
|
148
|
+
* @param {object} opts
|
|
149
|
+
* @param {(args:{prompt,maxTokens,model?})=>Promise<{text:string}>} opts.transport
|
|
150
|
+
* the injected LLM transport (local Ollama caller, or — only on explicit
|
|
151
|
+
* cloud opt-in handled by the orchestrator — a cloud caller). REQUIRED;
|
|
152
|
+
* without it nothing networks.
|
|
153
|
+
* @param {string} [opts.model]
|
|
154
|
+
* @param {string} [opts.sessionId] provenance for the derived inferences
|
|
155
|
+
* @param {string} [opts.host]
|
|
156
|
+
* @param {Function} [opts.log] degrade reasons are LOGGED, not swallowed
|
|
157
|
+
*
|
|
158
|
+
* ALWAYS returns an additive delta. On no-signal, no-transport, transport error,
|
|
159
|
+
* unparseable output, or sub-K corroboration -> { inferences: [] }. A degrade
|
|
160
|
+
* caused by a transport error or unparseable output is LOGGED via opts.log (the
|
|
161
|
+
* audit flagged a swallowed-error class; we surface it explicitly) while still
|
|
162
|
+
* degrading gracefully to the heuristic floor.
|
|
163
|
+
*/
|
|
164
|
+
export async function deriveDialectic(signals, opts = {}) {
|
|
165
|
+
const empty = { inferences: [] };
|
|
166
|
+
const log = typeof opts.log === 'function' ? opts.log : () => {};
|
|
167
|
+
const rows = styleRows(signals);
|
|
168
|
+
if (rows.length === 0) return empty; // GUARD 2 (degenerate): no corpus
|
|
169
|
+
if (typeof opts.transport !== 'function') return empty; // no transport => no network, ever
|
|
170
|
+
|
|
171
|
+
// GUARD 2 — corroboration is computed in CODE from the digest WINDOW (the
|
|
172
|
+
// slice the model actually saw), so a model can never claim evidence the
|
|
173
|
+
// corpus does not contain.
|
|
174
|
+
const window = rows.slice(-DIALECTIC_DIGEST_WINDOW);
|
|
175
|
+
const sessions = distinctSessions(window);
|
|
176
|
+
if (sessions.length < DIALECTIC_MIN_CORROBORATION) return empty;
|
|
177
|
+
|
|
178
|
+
const digest = buildDigest(signals);
|
|
179
|
+
if (!digest) return empty;
|
|
180
|
+
const prompt = buildPrompt(digest);
|
|
181
|
+
|
|
182
|
+
let text;
|
|
183
|
+
try {
|
|
184
|
+
const res = await opts.transport({
|
|
185
|
+
prompt,
|
|
186
|
+
maxTokens: DIALECTIC_MAX_TOKENS,
|
|
187
|
+
model: opts.model,
|
|
188
|
+
});
|
|
189
|
+
text = res && typeof res.text === 'string' ? res.text : '';
|
|
190
|
+
} catch (err) {
|
|
191
|
+
// GUARD: transport error -> degrade to the heuristic floor (empty additive).
|
|
192
|
+
// LOGGED, not swallowed.
|
|
193
|
+
log(`profile dialectic transport error -> heuristic-only: ${err && err.message ? err.message : err}`);
|
|
194
|
+
return empty;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const proposed = parseInferences(text);
|
|
198
|
+
if (proposed.length === 0) {
|
|
199
|
+
if (text && text.trim()) {
|
|
200
|
+
log('profile dialectic: model output unparseable/empty -> heuristic-only');
|
|
201
|
+
}
|
|
202
|
+
return empty;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const host = typeof opts.host === 'string' ? opts.host : null;
|
|
206
|
+
const corroboratingSessions = sessions.slice(0, Math.max(DIALECTIC_MIN_CORROBORATION, sessions.length));
|
|
207
|
+
|
|
208
|
+
const out = [];
|
|
209
|
+
for (const p of proposed) {
|
|
210
|
+
if (!p || typeof p !== 'object') continue;
|
|
211
|
+
const kind = typeof p.kind === 'string' && p.kind.trim() ? p.kind.trim() : 'trait';
|
|
212
|
+
const subject = typeof p.subject === 'string' ? p.subject.trim() : '';
|
|
213
|
+
if (!subject) continue;
|
|
214
|
+
// GUARD 3 — confidence is CAPPED in code, never taken from the model.
|
|
215
|
+
const rawConf = Number(p.confidence);
|
|
216
|
+
const confidence = Math.min(
|
|
217
|
+
DIALECTIC_MAX_CONFIDENCE,
|
|
218
|
+
Number.isFinite(rawConf) && rawConf > 0 ? rawConf : DIALECTIC_MAX_CONFIDENCE,
|
|
219
|
+
);
|
|
220
|
+
try {
|
|
221
|
+
out.push(makeInference({
|
|
222
|
+
kind,
|
|
223
|
+
subject,
|
|
224
|
+
value: p.value === undefined ? null : p.value,
|
|
225
|
+
confidence,
|
|
226
|
+
// GUARD 2 — evidence + sources reflect the CODE-COMPUTED corroboration,
|
|
227
|
+
// not the model's assertion.
|
|
228
|
+
evidence_count: corroboratingSessions.length,
|
|
229
|
+
source_sessions: corroboratingSessions,
|
|
230
|
+
source_hosts: host ? [host] : [],
|
|
231
|
+
// Dialectic traits are inferred (more revealing than a style stat) —
|
|
232
|
+
// tag medium so the sensitivity gate (P4) can govern egress.
|
|
233
|
+
sensitivity: 'med',
|
|
234
|
+
}));
|
|
235
|
+
} catch {
|
|
236
|
+
// makeInference rejects malformed atoms — skip, never throw out of derive.
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return { inferences: out };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export default { deriveDialectic, buildDigest };
|