@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.
Files changed (72) hide show
  1. package/bin/ijfw-dashboard +20 -1
  2. package/package.json +4 -3
  3. package/src/audit-roster.js +89 -12
  4. package/src/brain/tiered-llm.js +57 -7
  5. package/src/cross-orchestrator-cli.js +344 -4
  6. package/src/cross-project-search.js +39 -1
  7. package/src/dashboard-server.js +7 -1
  8. package/src/dream/runner.mjs +560 -8
  9. package/src/handlers/brain-handler.js +101 -1
  10. package/src/importers/discover.js +1 -1
  11. package/src/memory/bench-metrics.js +289 -0
  12. package/src/memory/benchmark.js +1 -1
  13. package/src/memory/search.js +53 -1
  14. package/src/orchestrator/plan-checker.js +1 -1
  15. package/src/profile/audit.js +671 -0
  16. package/src/profile/capture.js +871 -0
  17. package/src/profile/derive-dialectic.js +242 -0
  18. package/src/profile/derive-heuristic.js +733 -0
  19. package/src/profile/derive.js +156 -0
  20. package/src/profile/egress.js +306 -0
  21. package/src/profile/eval/build-real-probes.mjs +197 -0
  22. package/src/profile/eval/corpus-from-reddit.mjs +166 -0
  23. package/src/profile/eval/corpus-from-reddit.test.mjs +121 -0
  24. package/src/profile/eval/corpus-from-transcripts.mjs +264 -0
  25. package/src/profile/eval/gate-b-behavior.mjs +420 -0
  26. package/src/profile/eval/gate-b-decision-run.mjs +171 -0
  27. package/src/profile/eval/gate-b-decision-run.test.mjs +141 -0
  28. package/src/profile/eval/gate-b-run.mjs +417 -0
  29. package/src/profile/eval/gate-b-run.test.mjs +204 -0
  30. package/src/profile/eval/gate-c-capture.mjs +323 -0
  31. package/src/profile/eval/harness.mjs +551 -0
  32. package/src/profile/eval/instrument-validation.mjs +248 -0
  33. package/src/profile/eval/instrument-validation.test.mjs +125 -0
  34. package/src/profile/eval/multi-subject-harness.mjs +106 -0
  35. package/src/profile/eval/multi-subject-harness.test.mjs +99 -0
  36. package/src/profile/eval/personas.test.mjs +83 -0
  37. package/src/profile/eval/plumbing.test.mjs +69 -0
  38. package/src/profile/eval/prereg.mjs +130 -0
  39. package/src/profile/eval/prereg.test.mjs +78 -0
  40. package/src/profile/eval/real-corpus.test.mjs +103 -0
  41. package/src/profile/eval/real-personas.mjs +109 -0
  42. package/src/profile/eval/run-real-corpus-concurrent.mjs +407 -0
  43. package/src/profile/eval/run-real-corpus.mjs +358 -0
  44. package/src/profile/eval/slug-quality.mjs +464 -0
  45. package/src/profile/eval/stylometry-features.js +85 -0
  46. package/src/profile/eval/stylometry-reference.js +16 -0
  47. package/src/profile/eval/stylometry.js +224 -0
  48. package/src/profile/eval/stylometry.test.mjs +103 -0
  49. package/src/profile/eval/synthetic-personas.js +91 -0
  50. package/src/profile/eval/verifier-features.mjs +170 -0
  51. package/src/profile/eval/verifier-logreg.mjs +74 -0
  52. package/src/profile/eval/verifier-pair.mjs +122 -0
  53. package/src/profile/eval/verifier-reference.mjs +68 -0
  54. package/src/profile/eval/verifier-scorer.mjs +30 -0
  55. package/src/profile/eval/wrong-target-control.mjs +168 -0
  56. package/src/profile/eval/wrong-target-control.test.mjs +124 -0
  57. package/src/profile/exemplar-capture.js +232 -0
  58. package/src/profile/exemplar-retrieve.js +138 -0
  59. package/src/profile/exemplar-store.js +314 -0
  60. package/src/profile/lock.js +64 -0
  61. package/src/profile/merge.js +624 -0
  62. package/src/profile/path-policy.js +213 -0
  63. package/src/profile/precision-stamp.mjs +151 -0
  64. package/src/profile/render-brief.js +717 -0
  65. package/src/profile/schema.js +244 -0
  66. package/src/profile/sensitivity.js +249 -0
  67. package/src/profile/serve.js +345 -0
  68. package/src/profile/store.js +261 -0
  69. package/src/profile/telemetry.js +289 -0
  70. package/src/recovery/checkpoint.js +7 -1
  71. package/src/server.js +185 -14
  72. 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 };