aegis-desktop 1.0.13 → 1.0.15
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/SOURCE-OFFER.txt +9 -0
- package/bin/aegis.js +1 -32
- package/lib/account-key.js +1 -56
- package/lib/auto.js +1 -430
- package/lib/avatar/companion.js +1 -698
- package/lib/avatar/cosmetics.js +1 -532
- package/lib/avatar/events.js +1 -344
- package/lib/avatar/holder-turn.js +1 -350
- package/lib/avatar/holders.js +1 -612
- package/lib/avatar/identity.js +1 -641
- package/lib/avatar/level.js +1 -368
- package/lib/avatar/mirror.js +1 -524
- package/lib/avatar/persona.js +1 -388
- package/lib/avatar/profile.js +1 -977
- package/lib/avatar/register.js +1 -307
- package/lib/avatar/store.js +1 -472
- package/lib/avatar/tiers.js +1 -38
- package/lib/avatar/transfer.js +1 -415
- package/lib/avatar/turn.js +1 -216
- package/lib/avatar/voice.js +1 -328
- package/lib/avatar/xp.js +1 -404
- package/lib/deep-link.js +1 -120
- package/lib/fingerprint.js +2 -347
- package/lib/google-connect.js +1 -169
- package/lib/local/agents.js +1 -105
- package/lib/local/autonomous.js +1 -1045
- package/lib/local/claude-pro.js +1 -993
- package/lib/local/context.js +1 -95
- package/lib/local/engine.js +1 -3228
- package/lib/local/entitlement.js +1 -654
- package/lib/local/git-scope.js +1 -228
- package/lib/local/local.js +1 -2135
- package/lib/local/prompt.js +1 -275
- package/lib/local/queue.js +1 -560
- package/lib/local/session-rounds.js +1 -208
- package/lib/local/shell.js +1 -680
- package/lib/local/telemetry.js +1 -470
- package/lib/local/tools.js +1 -1238
- package/lib/local/turn-guard.js +1 -300
- package/lib/local/worktree-lock.js +1 -235
- package/lib/provider-key-mirror.js +1 -252
- package/lib/quick-launcher.js +1 -84
- package/lib/settings.js +1 -890
- package/lib/sync/memory-queue.js +1 -60
- package/lib/sync/persist-gate.js +1 -148
- package/lib/sync/sessions.js +1 -106
- package/lib/window-state.js +1 -66
- package/main.js +3 -4390
- package/package.json +3 -5
- package/preload.js +3 -628
- package/renderer/app.js +1 -6560
- package/renderer/avatar/assemble.js +1 -335
- package/renderer/avatar/avatar.js +1 -380
- package/renderer/avatar/cards.js +1 -141
- package/renderer/avatar/hud.js +1 -206
- package/renderer/avatar/machine.js +1 -213
- package/renderer/avatar/pane.js +1 -439
- package/renderer/avatar/parts.js +1 -437
- package/renderer/budget.js +1 -168
- package/renderer/companion.js +1 -223
- package/renderer/diff.js +1 -359
- package/renderer/diffview.js +1 -336
- package/renderer/fingerprint.js +3 -720
- package/renderer/markdown.js +1 -168
- package/renderer/provider-toolbar.js +1 -134
- package/renderer/providers.js +1 -259
- package/renderer/quick.js +1 -191
- package/renderer/stream-policy.js +1 -306
- package/renderer/transcript-view.js +1 -323
- package/renderer/usage.js +1 -1239
- package/vendor/aegis.js +1 -2054
- package/vendor/attest.js +1 -442
- package/vendor/authorship-key.js +1 -410
- package/vendor/auto-provision.js +1 -134
- package/vendor/brain-catalog.js +1 -268
- package/vendor/byok-catalog.js +1 -230
- package/vendor/credentials.js +1 -494
- package/vendor/device-fingerprint.js +1 -141
- package/vendor/drive-connect.js +1 -85
- package/vendor/drive-upload.js +1 -174
- package/vendor/env-file.js +1 -1273
- package/vendor/fingerprint.js +1 -1064
- package/vendor/foreign-memory.js +1 -669
- package/vendor/google-signin.js +1 -125
- package/vendor/hooks.js +1 -430
- package/vendor/model-discovery.js +1 -330
- package/vendor/platform.js +1 -937
- package/vendor/prefs.js +1 -351
- package/vendor/presence-constants.js +1 -107
- package/vendor/presence.js +1 -438
- package/vendor/record-vault.js +3 -1055
- package/vendor/session-store.js +1 -548
- package/vendor/topup.js +1 -297
- package/vendor/touch.js +1 -458
- package/vendor/update.js +1 -704
- package/vendor/user-models.js +1 -381
package/lib/avatar/holders.js
CHANGED
|
@@ -1,614 +1,3 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
2
|
// Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
|
|
3
|
-
|
|
4
|
-
// Conveying a modified copy, or running one as a network service, owes the
|
|
5
|
-
// corresponding source under this licence (§5/§13) — see LICENSING.md.
|
|
6
|
-
'use strict';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* holders.js — who holds the memory.
|
|
10
|
-
*
|
|
11
|
-
* `identity.js` decides *which* holder a set of signals points at and
|
|
12
|
-
* refuses to merge two people on a fingerprint match. `profile.js`
|
|
13
|
-
* folds one holder's entries into bounded, evidence-gated facets. Neither of
|
|
14
|
-
* them owns a directory. This file is that owner — the layout, the registry, the
|
|
15
|
-
* switch, the conflict decision and the "why does it know this" surface — and it
|
|
16
|
-
* is deliberately still pure Node (fs + path only, no Electron, no clock unless
|
|
17
|
-
* one is injected) so the whole phase is testable with `node --test`.
|
|
18
|
-
*
|
|
19
|
-
* The four claims this module exists to make true:
|
|
20
|
-
*
|
|
21
|
-
* 1. **A switch swaps everything together.** `switchHolder()` returns a state
|
|
22
|
-
* read fresh off disk for the new holder — ledger, persona and profile
|
|
23
|
-
* cache — and names the holder's memory scope in the same object. There is
|
|
24
|
-
* no field carried over from the previous holder, and the only thing that
|
|
25
|
-
* survives in memory is the registry (which is an *index*, not data). The
|
|
26
|
-
* claim is asserted by comparing the two states byte-for-byte for any field
|
|
27
|
-
* that could carry holder A's text.
|
|
28
|
-
*
|
|
29
|
-
* 2. **A fingerprint is not an identity.** `applyConflict()` has one default:
|
|
30
|
-
* *keep separate*. Rebinding two holders behind one fingerprint requires
|
|
31
|
-
* `{ choice: 'rebind', confirm: true }` — the card's explicit confirm — and
|
|
32
|
-
* even then it only copies the fingerprint onto the surviving row. Nothing
|
|
33
|
-
* in this file ever merges two ledgers, two personas or two profiles.
|
|
34
|
-
*
|
|
35
|
-
* 3. **Isolation is applied at the seam, not hoped for.** `entriesForHolder()`
|
|
36
|
-
* is the client-side filter §3 calls "the assertion that the fix happened":
|
|
37
|
-
* the server-side scope is the fix, and this is what proves it ran. It is
|
|
38
|
-
* used by the fold *and* by the recall block, so a leak needs to defeat one
|
|
39
|
-
* function rather than two call sites — and the test drives two
|
|
40
|
-
* holders through the real turn assembly and greps the prompt.
|
|
41
|
-
*
|
|
42
|
-
* 4. **Every claim is traceable and deletable.** `provenance()` renders a
|
|
43
|
-
* facet's `sources` as the underlying entry text, and `forgetFacet()`
|
|
44
|
-
* removes the facet *and* the entries behind it: the ids go on the holder's
|
|
45
|
-
* own forget list, which `entriesForHolder()` honours on the next fold, so
|
|
46
|
-
* the claim cannot silently re-derive from the same row (§4).
|
|
47
|
-
*
|
|
48
|
-
* What is NOT here: any level arithmetic (that is `xp.js` + `store.js`), any
|
|
49
|
-
* prompt text (that is `holder-turn.js`), and any IPC (that is `holder-ipc.js`).
|
|
50
|
-
*/
|
|
51
|
-
|
|
52
|
-
const fs = require('node:fs');
|
|
53
|
-
const path = require('node:path');
|
|
54
|
-
|
|
55
|
-
const identity = require('./identity.js');
|
|
56
|
-
const personaModule = require('./persona.js');
|
|
57
|
-
const profileModule = require('./profile.js');
|
|
58
|
-
const xp = require('./xp.js');
|
|
59
|
-
|
|
60
|
-
/** `<userData>/avatar/` — the one directory tree this module owns. */
|
|
61
|
-
const AVATAR_DIR = 'avatar';
|
|
62
|
-
const REGISTRY_FILE = 'holders.json';
|
|
63
|
-
const HOLDERS_DIR = 'holders';
|
|
64
|
-
const LEDGER_FILE = 'ledger.jsonl';
|
|
65
|
-
const PERSONA_FILE = 'persona.json';
|
|
66
|
-
const PROFILE_FILE = 'profile.json';
|
|
67
|
-
const FORGOTTEN_FILE = 'forgotten.json';
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* The one thing a switch may leave behind. A list, not a sentence: the
|
|
71
|
-
* test iterates it, so adding a survivor to the code without adding it here
|
|
72
|
-
* makes the test fail rather than the promise quietly rot.
|
|
73
|
-
*/
|
|
74
|
-
const SURVIVES_SWITCH = Object.freeze(['registry']);
|
|
75
|
-
|
|
76
|
-
/** Reserved mirror sessions (§6) — holder id, never a fingerprint. */
|
|
77
|
-
const MIRROR_SESSIONS = Object.freeze(['avatar:ledger', 'avatar:profile']);
|
|
78
|
-
function mirrorSession(holderId, kind = 'ledger') {
|
|
79
|
-
const suffix = kind === 'profile' ? 'profile' : 'ledger';
|
|
80
|
-
return `avatar:${suffix}:${holderId}`;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// ---------------------------------------------------------------- paths ----
|
|
84
|
-
|
|
85
|
-
function registryPath(dir) {
|
|
86
|
-
return path.join(String(dir), AVATAR_DIR, REGISTRY_FILE);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** The holder's own directory. Ids are shape-checked before they touch a path. */
|
|
90
|
-
function holderDir(dir, holderId) {
|
|
91
|
-
const id = safeHolderId(holderId);
|
|
92
|
-
if (!id) throw new Error('holderDir: expected a holder id (h_ + 8-16 hex)');
|
|
93
|
-
return path.join(String(dir), AVATAR_DIR, HOLDERS_DIR, id);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function holderPaths(dir, holderId) {
|
|
97
|
-
const base = holderDir(dir, holderId);
|
|
98
|
-
return {
|
|
99
|
-
dir: base,
|
|
100
|
-
ledger: path.join(base, LEDGER_FILE),
|
|
101
|
-
persona: path.join(base, PERSONA_FILE),
|
|
102
|
-
profile: path.join(base, PROFILE_FILE),
|
|
103
|
-
forgotten: path.join(base, FORGOTTEN_FILE),
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function safeHolderId(value) {
|
|
108
|
-
const id = value == null ? '' : String(value).trim();
|
|
109
|
-
if (!id) return null;
|
|
110
|
-
return identity.HOLDER_ID_RE && identity.HOLDER_ID_RE.test(id) ? id : null;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// -------------------------------------------------------------- file IO ----
|
|
114
|
-
|
|
115
|
-
function readJsonFile(file) {
|
|
116
|
-
try {
|
|
117
|
-
const raw = fs.readFileSync(file, 'utf8');
|
|
118
|
-
const parsed = JSON.parse(raw);
|
|
119
|
-
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
120
|
-
} catch (err) {
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function writeJsonFile(file, value) {
|
|
126
|
-
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
127
|
-
const tmp = `${file}.tmp`;
|
|
128
|
-
fs.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`);
|
|
129
|
-
fs.renameSync(tmp, file);
|
|
130
|
-
return value;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/** Parse a JSONL ledger. Tolerant by construction: a torn last line is skipped. */
|
|
134
|
-
function readLedger(file) {
|
|
135
|
-
let raw = '';
|
|
136
|
-
try {
|
|
137
|
-
raw = fs.readFileSync(file, 'utf8');
|
|
138
|
-
} catch (err) {
|
|
139
|
-
return [];
|
|
140
|
-
}
|
|
141
|
-
const rows = [];
|
|
142
|
-
for (const line of raw.split('\n')) {
|
|
143
|
-
const trimmed = line.trim();
|
|
144
|
-
if (!trimmed) continue;
|
|
145
|
-
try {
|
|
146
|
-
const row = JSON.parse(trimmed);
|
|
147
|
-
if (row && typeof row === 'object') rows.push(row);
|
|
148
|
-
} catch (err) {
|
|
149
|
-
// A half-written append is lost XP, never a crash — same rule as store.js.
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
return rows;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function appendLedgerRow(file, row) {
|
|
156
|
-
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
157
|
-
fs.appendFileSync(file, `${JSON.stringify(row)}\n`);
|
|
158
|
-
return row;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// ------------------------------------------------------------ registry ----
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Read the registry. Anything unreadable or invalid degrades to a fresh empty
|
|
165
|
-
* registry (identity.load validates and migrates), because "the index is gone"
|
|
166
|
-
* must never read as "your memory is gone" — the holder directories are still
|
|
167
|
-
* on disk and a registry rebuild is not a data loss.
|
|
168
|
-
*/
|
|
169
|
-
function readRegistry(dir) {
|
|
170
|
-
const raw = readJsonFile(registryPath(dir));
|
|
171
|
-
try {
|
|
172
|
-
return identity.load(raw || identity.emptyRegistry());
|
|
173
|
-
} catch (err) {
|
|
174
|
-
return identity.load(identity.emptyRegistry());
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/** Validate + persist. Returns the validated registry, never the raw input. */
|
|
179
|
-
function writeRegistry(dir, registry) {
|
|
180
|
-
const validated = identity.load(registry);
|
|
181
|
-
writeJsonFile(registryPath(dir), validated);
|
|
182
|
-
return validated;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function holderRow(registry, holderId) {
|
|
186
|
-
const id = safeHolderId(holderId);
|
|
187
|
-
if (!id) return null;
|
|
188
|
-
return (registry.holders || []).find((row) => row && row.id === id) || null;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Add a holder (idempotent by id and by fingerprint-free identity) and return
|
|
193
|
-
* `{ registry, row, created }`. Used by a first-run mint and by the conflict
|
|
194
|
-
* card's "keep separate", which is exactly `ensureHolder` with no fingerprint.
|
|
195
|
-
*/
|
|
196
|
-
function ensureHolder(dir, input = {}) {
|
|
197
|
-
const registry = input.registry || readRegistry(dir);
|
|
198
|
-
const wanted = input.id ? safeHolderId(input.id) : null;
|
|
199
|
-
const existing = wanted ? holderRow(registry, wanted) : null;
|
|
200
|
-
if (existing) return { registry, row: existing, created: false };
|
|
201
|
-
|
|
202
|
-
const withRow = identity.addHolder(registry, {
|
|
203
|
-
id: wanted || undefined,
|
|
204
|
-
displayName: input.displayName,
|
|
205
|
-
fingerprint: input.fingerprint,
|
|
206
|
-
accountId: input.accountId,
|
|
207
|
-
now: input.now,
|
|
208
|
-
});
|
|
209
|
-
const validated = identity.load(withRow);
|
|
210
|
-
const row = validated.holders[validated.holders.length - 1] || null;
|
|
211
|
-
if (input.persist !== false) writeRegistry(dir, validated);
|
|
212
|
-
return { registry: validated, row, created: true };
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// --------------------------------------------------------------- state -----
|
|
216
|
-
|
|
217
|
-
/** What a level reads as, from this holder's ledger rows alone. */
|
|
218
|
-
function levelOf(ledger) {
|
|
219
|
-
try {
|
|
220
|
-
const evaluated = xp.evaluate(Array.isArray(ledger) ? ledger : []);
|
|
221
|
-
return evaluated && Number.isFinite(evaluated.level) ? evaluated.level : 1;
|
|
222
|
-
} catch (err) {
|
|
223
|
-
return 1;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/** The forgotten-entry list: what "forget" actually writes. */
|
|
228
|
-
function readForgotten(dir, holderId) {
|
|
229
|
-
const parsed = readJsonFile(holderPaths(dir, holderId).forgotten);
|
|
230
|
-
const ids = parsed && Array.isArray(parsed.entries) ? parsed.entries : [];
|
|
231
|
-
return new Set(ids.map(String));
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function writeForgotten(dir, holderId, ids) {
|
|
235
|
-
const list = Array.from(new Set(Array.from(ids).map(String))).sort();
|
|
236
|
-
writeJsonFile(holderPaths(dir, holderId).forgotten, {
|
|
237
|
-
schema: 1,
|
|
238
|
-
holderId,
|
|
239
|
-
entries: list,
|
|
240
|
-
});
|
|
241
|
-
return new Set(list);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function emptyProfile(holderId) {
|
|
245
|
-
return { schema: 1, holderId, facets: [], budget: { tokens: 0, used: 0 }, coldStart: true };
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Load one holder's state — every field read from that holder's own directory,
|
|
250
|
-
* nothing defaulted from a previous holder.
|
|
251
|
-
*
|
|
252
|
-
* @returns {{ holderId:string, displayName:string, ledger:object[],
|
|
253
|
-
* persona:object, profile:object, forgotten:Set<string>,
|
|
254
|
-
* level:number, scope:string[] }}
|
|
255
|
-
*/
|
|
256
|
-
function loadState(dir, holderId, opts = {}) {
|
|
257
|
-
const id = safeHolderId(holderId);
|
|
258
|
-
if (!id) throw new Error('loadState: expected a holder id (h_ + 8-16 hex)');
|
|
259
|
-
const registry = opts.registry || readRegistry(dir);
|
|
260
|
-
const row = holderRow(registry, id);
|
|
261
|
-
const paths = holderPaths(dir, id);
|
|
262
|
-
const ledger = readLedger(paths.ledger);
|
|
263
|
-
|
|
264
|
-
const rawPersona = readJsonFile(paths.persona);
|
|
265
|
-
let persona = personaModule.defaultPersona();
|
|
266
|
-
if (rawPersona) {
|
|
267
|
-
try {
|
|
268
|
-
const loaded = personaModule.load(rawPersona);
|
|
269
|
-
persona = loaded && loaded.persona ? loaded.persona : loaded;
|
|
270
|
-
} catch (err) {
|
|
271
|
-
persona = personaModule.defaultPersona();
|
|
272
|
-
}
|
|
273
|
-
} else {
|
|
274
|
-
// A holder with no persona file inherits nothing: it gets the neutral
|
|
275
|
-
// default, so a fresh holder is an unconfigured install (§4, cold start).
|
|
276
|
-
persona = personaModule.defaultPersona();
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const profile = readJsonFile(paths.profile) || emptyProfile(id);
|
|
280
|
-
const forgotten = readForgotten(dir, id);
|
|
281
|
-
|
|
282
|
-
return {
|
|
283
|
-
holderId: id,
|
|
284
|
-
displayName: (row && row.displayName) || personaDisplayName(persona) || identity.DEFAULT_DISPLAY_NAME,
|
|
285
|
-
ledger,
|
|
286
|
-
persona,
|
|
287
|
-
profile: profile && profile.holderId === id ? profile : { ...profile, holderId: id },
|
|
288
|
-
forgotten,
|
|
289
|
-
level: levelOf(ledger),
|
|
290
|
-
scope: MIRROR_SESSIONS.map((kind) => mirrorSession(id, kind.split(':')[1])),
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function personaDisplayName(persona) {
|
|
295
|
-
const name = persona && persona.identity && persona.identity.name;
|
|
296
|
-
return typeof name === 'string' && name.trim() ? name.trim() : null;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/** Persist a folded profile into the holder's own cache file. */
|
|
300
|
-
function writeProfile(dir, holderId, profile) {
|
|
301
|
-
const withHolder = { ...(profile || {}), holderId };
|
|
302
|
-
writeJsonFile(holderPaths(dir, holderId).profile, withHolder);
|
|
303
|
-
return withHolder;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function writePersona(dir, holderId, persona) {
|
|
307
|
-
writeJsonFile(holderPaths(dir, holderId).persona, { ...(persona || {}), holderId });
|
|
308
|
-
return persona;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// -------------------------------------------------------------- switching --
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Switch the active holder.
|
|
315
|
-
*
|
|
316
|
-
* The returned `state` is loaded *after* the registry write, so a caller cannot
|
|
317
|
-
* accidentally keep rendering the old holder by holding a stale object: every
|
|
318
|
-
* field that can carry holder A's text — ledger, persona, profile, forget list
|
|
319
|
-
* and memory scope — is fresh. `survived` is the honest list of what came
|
|
320
|
-
* across, and it is `['registry']` by construction.
|
|
321
|
-
*/
|
|
322
|
-
function switchHolder(dir, holderId, opts = {}) {
|
|
323
|
-
const id = safeHolderId(holderId);
|
|
324
|
-
if (!id) return { ok: false, reason: 'expected a holder id (h_ + 8-16 hex)', survived: SURVIVES_SWITCH.slice() };
|
|
325
|
-
|
|
326
|
-
const registry = opts.registry || readRegistry(dir);
|
|
327
|
-
if (!holderRow(registry, id) && !opts.create) {
|
|
328
|
-
return { ok: false, reason: `unknown holder ${id}`, survived: SURVIVES_SWITCH.slice() };
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
let next = registry;
|
|
332
|
-
if (holderRow(registry, id)) {
|
|
333
|
-
try {
|
|
334
|
-
next = identity.setActive(registry, id);
|
|
335
|
-
} catch (err) {
|
|
336
|
-
next = { ...registry, active: id };
|
|
337
|
-
}
|
|
338
|
-
next = writeRegistry(dir, next);
|
|
339
|
-
} else {
|
|
340
|
-
const created = ensureHolder(dir, { id, displayName: opts.displayName, now: opts.now });
|
|
341
|
-
next = writeRegistry(dir, identity.setActive(created.registry, id));
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const state = loadState(dir, id, { registry: next });
|
|
345
|
-
return {
|
|
346
|
-
ok: true,
|
|
347
|
-
previous: opts.previous || (registry && registry.active) || null,
|
|
348
|
-
state,
|
|
349
|
-
registry: next,
|
|
350
|
-
survived: SURVIVES_SWITCH.slice(),
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* Resolve the active holder from signals (identity.resolve — one source of
|
|
356
|
-
* truth for precedence) and, when a signal minted nothing, create the row so
|
|
357
|
-
* the next launch resolves to the same holder.
|
|
358
|
-
*/
|
|
359
|
-
function resolveActive(dir, signals = {}, opts = {}) {
|
|
360
|
-
const registry = opts.registry || readRegistry(dir);
|
|
361
|
-
const resolution = identity.resolve(signals, registry);
|
|
362
|
-
if (resolution.needsMint || !resolution.holderId) {
|
|
363
|
-
return { registry, resolution, holderId: null, created: false };
|
|
364
|
-
}
|
|
365
|
-
const existing = holderRow(registry, resolution.holderId);
|
|
366
|
-
if (existing) {
|
|
367
|
-
return { registry, resolution, holderId: existing.id, created: false };
|
|
368
|
-
}
|
|
369
|
-
const created = ensureHolder(dir, {
|
|
370
|
-
id: resolution.holderId,
|
|
371
|
-
displayName: resolution.displayName,
|
|
372
|
-
fingerprint: resolution.proposal ? resolution.proposal.fingerprint : undefined,
|
|
373
|
-
now: opts.now,
|
|
374
|
-
registry,
|
|
375
|
-
});
|
|
376
|
-
return { registry: created.registry, resolution, holderId: created.row ? created.row.id : resolution.holderId, created: created.created };
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
/**
|
|
380
|
-
* Apply the conflict card's choice (§2/§3).
|
|
381
|
-
*
|
|
382
|
-
* Default is *keep separate*: two holders stay two holders, and the only thing
|
|
383
|
-
* the fingerprint gets is a note that it is shared. `rebind` requires
|
|
384
|
-
* `confirm: true` — the explicit confirm behind the card — and still never
|
|
385
|
-
* merges data: it moves the fingerprint onto the winner and records the loser
|
|
386
|
-
* as unbound, because a fingerprint is not an identity.
|
|
387
|
-
*/
|
|
388
|
-
function applyConflict(registry, resolution, choice = {}) {
|
|
389
|
-
const reg = identity.load(registry || identity.emptyRegistry());
|
|
390
|
-
const conflict = resolution && resolution.conflict;
|
|
391
|
-
const target = (resolution && resolution.holderId) || reg.active;
|
|
392
|
-
const action = choice && choice.choice === 'rebind' ? 'rebind' : 'keep-separate';
|
|
393
|
-
|
|
394
|
-
if (!conflict || !target) {
|
|
395
|
-
return { ok: false, action, reason: 'no conflict to resolve', registry: reg, requiresConfirm: false };
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
const other = conflict.a === target ? conflict.b : conflict.a;
|
|
399
|
-
if (action === 'keep-separate') {
|
|
400
|
-
// Nothing about the rows changes. Keeping the two is not a failure state —
|
|
401
|
-
// it is the outcome the copy on the card recommends.
|
|
402
|
-
return {
|
|
403
|
-
ok: true,
|
|
404
|
-
action,
|
|
405
|
-
registry: reg,
|
|
406
|
-
requiresConfirm: false,
|
|
407
|
-
kept: [target, other].filter(Boolean),
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
if (choice.confirm !== true) {
|
|
412
|
-
return {
|
|
413
|
-
ok: false,
|
|
414
|
-
action,
|
|
415
|
-
reason: 'rebind requires an explicit confirm',
|
|
416
|
-
registry: reg,
|
|
417
|
-
requiresConfirm: true,
|
|
418
|
-
kept: [target, other].filter(Boolean),
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
const losers = new Set((reg.holders || [])
|
|
423
|
-
.filter((row) => row && row.id !== target && row.id === other)
|
|
424
|
-
.map((row) => row.id));
|
|
425
|
-
const holders = (reg.holders || []).map((row) => {
|
|
426
|
-
if (!row) return row;
|
|
427
|
-
if (row.id === target) {
|
|
428
|
-
return { ...row, fingerprint: resolution.proposal ? resolution.proposal.fingerprint : row.fingerprint };
|
|
429
|
-
}
|
|
430
|
-
if (losers.has(row.id)) return { ...row, fingerprint: undefined };
|
|
431
|
-
return row;
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
return {
|
|
435
|
-
ok: true,
|
|
436
|
-
action,
|
|
437
|
-
requiresConfirm: false,
|
|
438
|
-
registry: identity.load({ ...reg, holders }),
|
|
439
|
-
rebound: { kept: target, unbound: Array.from(losers) },
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// ---------------------------------------------------------------- scope ----
|
|
444
|
-
|
|
445
|
-
/**
|
|
446
|
-
* The client-side filter (§3). `profile.belongsTo` is the same predicate the
|
|
447
|
-
* fold uses, so there is one definition of "this row is this holder's", and a
|
|
448
|
-
* row with no holder stamp is *unattributed* — it belongs to nobody in
|
|
449
|
-
* particular and therefore to the holder asking, which is what makes a legacy
|
|
450
|
-
* unstamped ledger readable instead of invisible.
|
|
451
|
-
*
|
|
452
|
-
* `clientFilter:false` exists only so the isolation test can prove the
|
|
453
|
-
* filter is load-bearing by removing it and watching the leg go red. It is not
|
|
454
|
-
* reachable from any IPC handler (holder-ipc.js never passes it).
|
|
455
|
-
*/
|
|
456
|
-
function entriesForHolder(entries, holderId, opts = {}) {
|
|
457
|
-
const rows = Array.isArray(entries) ? entries : [];
|
|
458
|
-
const id = safeHolderId(holderId);
|
|
459
|
-
const forgotten = opts.forgotten instanceof Set ? opts.forgotten : new Set();
|
|
460
|
-
if (opts.clientFilter === false || !id) {
|
|
461
|
-
return rows.filter((row) => row && typeof row === 'object' && !forgotten.has(String(rowId(row))));
|
|
462
|
-
}
|
|
463
|
-
return rows.filter((row) => {
|
|
464
|
-
if (!row || typeof row !== 'object') return false;
|
|
465
|
-
if (forgotten.has(String(rowId(row)))) return false;
|
|
466
|
-
try {
|
|
467
|
-
return profileModule.belongsTo(row, id);
|
|
468
|
-
} catch (err) {
|
|
469
|
-
return String(row.holder || '') === id;
|
|
470
|
-
}
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
function rowId(row) {
|
|
475
|
-
if (!row || typeof row !== 'object') return '';
|
|
476
|
-
return row.id != null ? row.id : (row.entryId != null ? row.entryId : '');
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
/**
|
|
480
|
-
* Main stamps the holder on every row it writes; the renderer never supplies it
|
|
481
|
-
* (same reasoning as lib/sync/persist-gate.js). A renderer-supplied `holder`
|
|
482
|
-
* field is overwritten, not merged, so a malformed payload cannot smuggle a row
|
|
483
|
-
* into another holder's fold.
|
|
484
|
-
*/
|
|
485
|
-
function stampEntry(entry, holderId) {
|
|
486
|
-
const id = safeHolderId(holderId);
|
|
487
|
-
if (!entry || typeof entry !== 'object') return entry;
|
|
488
|
-
if (!id) return { ...entry, holder: undefined };
|
|
489
|
-
return { ...entry, holder: id };
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
// ------------------------------------------------------------ provenance ---
|
|
493
|
-
|
|
494
|
-
/**
|
|
495
|
-
* "Why does it know this" (§4): a facet's sources rendered as the underlying
|
|
496
|
-
* entry text, in the facet's own source order.
|
|
497
|
-
*/
|
|
498
|
-
function provenance(profile, entries, facetId, opts = {}) {
|
|
499
|
-
const facets = (profile && Array.isArray(profile.facets)) ? profile.facets : [];
|
|
500
|
-
const facet = facets.find((f) => f && f.id === facetId) || null;
|
|
501
|
-
if (!facet) return { ok: false, reason: `no facet ${facetId}`, facet: null, claims: [] };
|
|
502
|
-
const byId = new Map();
|
|
503
|
-
for (const row of Array.isArray(entries) ? entries : []) {
|
|
504
|
-
const id = rowId(row);
|
|
505
|
-
if (id) byId.set(String(id), row);
|
|
506
|
-
}
|
|
507
|
-
const claims = (Array.isArray(facet.sources) ? facet.sources : []).map((sourceId) => {
|
|
508
|
-
const row = byId.get(String(sourceId)) || null;
|
|
509
|
-
return {
|
|
510
|
-
entryId: String(sourceId),
|
|
511
|
-
// The text is quoted as inert data: this surface renders memory, it never
|
|
512
|
-
// re-emits it as an instruction (holder-turn.js neutralises on the way out).
|
|
513
|
-
text: row ? textOf(row) : '',
|
|
514
|
-
kind: row ? String(row.kind || '') : '',
|
|
515
|
-
at: row && Number.isFinite(Number(row.t)) ? Number(row.t) : null,
|
|
516
|
-
present: Boolean(row),
|
|
517
|
-
};
|
|
518
|
-
});
|
|
519
|
-
return {
|
|
520
|
-
ok: true,
|
|
521
|
-
facet: { id: facet.id, key: facet.key, value: facet.value, confidence: facet.confidence },
|
|
522
|
-
claims,
|
|
523
|
-
missing: claims.filter((c) => !c.present).map((c) => c.entryId),
|
|
524
|
-
};
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
function textOf(row) {
|
|
528
|
-
if (!row || typeof row !== 'object') return '';
|
|
529
|
-
const value = row.text != null ? row.text : (row.content != null ? row.content : row.value);
|
|
530
|
-
return typeof value === 'string' ? value : '';
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
/**
|
|
534
|
-
* Forget one facet: drop the facet from the profile and put the entries behind
|
|
535
|
-
* it on the holder's forget list, which `entriesForHolder()` reads on the next
|
|
536
|
-
* fold — so the claim cannot silently re-derive from the same row.
|
|
537
|
-
*
|
|
538
|
-
* @returns {{ ok:boolean, profile:object, removed:string[], facetIds:string[] }}
|
|
539
|
-
*/
|
|
540
|
-
function forgetFacet(dir, holderId, profile, facetId) {
|
|
541
|
-
const facets = (profile && Array.isArray(profile.facets)) ? profile.facets : [];
|
|
542
|
-
const facet = facets.find((f) => f && f.id === facetId) || null;
|
|
543
|
-
if (!facet) return { ok: false, reason: `no facet ${facetId}`, profile, removed: [], facetIds: [] };
|
|
544
|
-
|
|
545
|
-
const removed = (Array.isArray(facet.sources) ? facet.sources : []).map(String);
|
|
546
|
-
const forgotten = readForgotten(dir, holderId);
|
|
547
|
-
for (const id of removed) forgotten.add(id);
|
|
548
|
-
writeForgotten(dir, holderId, forgotten);
|
|
549
|
-
|
|
550
|
-
// A second facet folded from the same entries loses its sources too: dropping
|
|
551
|
-
// nothing but the clicked facet would leave "why does it know this" pointing
|
|
552
|
-
// at entries the holder just deleted.
|
|
553
|
-
const pruned = facets
|
|
554
|
-
.filter((f) => f && f.id !== facet.id)
|
|
555
|
-
.map((f) => {
|
|
556
|
-
const sources = (Array.isArray(f.sources) ? f.sources : []).filter((id) => !forgotten.has(String(id)));
|
|
557
|
-
return { ...f, sources };
|
|
558
|
-
})
|
|
559
|
-
.filter((f) => (Array.isArray(f.sources) ? f.sources.length : 0) > 0);
|
|
560
|
-
const dropped = facets.filter((f) => f && f.id !== facet.id).length - pruned.length;
|
|
561
|
-
|
|
562
|
-
const next = {
|
|
563
|
-
...(profile || {}),
|
|
564
|
-
holderId,
|
|
565
|
-
facets: pruned,
|
|
566
|
-
budget: { tokens: (profile && profile.budget && profile.budget.tokens) || profileModule.BUDGET_TOKENS, used: 0 },
|
|
567
|
-
coldStart: pruned.length === 0,
|
|
568
|
-
};
|
|
569
|
-
writeProfile(dir, holderId, next);
|
|
570
|
-
return {
|
|
571
|
-
ok: true,
|
|
572
|
-
profile: next,
|
|
573
|
-
removed,
|
|
574
|
-
facetIds: [facet.id].concat(dropped > 0 ? pruned.map((f) => f.id) : []),
|
|
575
|
-
};
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
module.exports = {
|
|
579
|
-
AVATAR_DIR,
|
|
580
|
-
REGISTRY_FILE,
|
|
581
|
-
HOLDERS_DIR,
|
|
582
|
-
LEDGER_FILE,
|
|
583
|
-
PERSONA_FILE,
|
|
584
|
-
PROFILE_FILE,
|
|
585
|
-
FORGOTTEN_FILE,
|
|
586
|
-
SURVIVES_SWITCH,
|
|
587
|
-
MIRROR_SESSIONS,
|
|
588
|
-
mirrorSession,
|
|
589
|
-
registryPath,
|
|
590
|
-
holderDir,
|
|
591
|
-
holderPaths,
|
|
592
|
-
readJsonFile,
|
|
593
|
-
writeJsonFile,
|
|
594
|
-
readLedger,
|
|
595
|
-
appendLedgerRow,
|
|
596
|
-
readRegistry,
|
|
597
|
-
writeRegistry,
|
|
598
|
-
holderRow,
|
|
599
|
-
ensureHolder,
|
|
600
|
-
levelOf,
|
|
601
|
-
readForgotten,
|
|
602
|
-
writeForgotten,
|
|
603
|
-
emptyProfile,
|
|
604
|
-
loadState,
|
|
605
|
-
writeProfile,
|
|
606
|
-
writePersona,
|
|
607
|
-
switchHolder,
|
|
608
|
-
resolveActive,
|
|
609
|
-
applyConflict,
|
|
610
|
-
entriesForHolder,
|
|
611
|
-
stampEntry,
|
|
612
|
-
provenance,
|
|
613
|
-
forgetFacet,
|
|
614
|
-
};
|
|
3
|
+
"use strict";const fs=require("node:fs"),path=require("node:path"),identity=require("./identity.js"),personaModule=require("./persona.js"),profileModule=require("./profile.js"),xp=require("./xp.js"),AVATAR_DIR="avatar",REGISTRY_FILE="holders.json",HOLDERS_DIR="holders",LEDGER_FILE="ledger.jsonl",PERSONA_FILE="persona.json",PROFILE_FILE="profile.json",FORGOTTEN_FILE="forgotten.json",SURVIVES_SWITCH=Object.freeze(["registry"]),MIRROR_SESSIONS=Object.freeze(["avatar:ledger","avatar:profile"]);function mirrorSession(e,r="ledger"){return`avatar:${"profile"===r?"profile":"ledger"}:${e}`}function registryPath(e){return path.join(String(e),AVATAR_DIR,REGISTRY_FILE)}function holderDir(e,r){const t=safeHolderId(r);if(!t)throw new Error("holderDir: expected a holder id (h_ + 8-16 hex)");return path.join(String(e),AVATAR_DIR,HOLDERS_DIR,t)}function holderPaths(e,r){const t=holderDir(e,r);return{dir:t,ledger:path.join(t,LEDGER_FILE),persona:path.join(t,PERSONA_FILE),profile:path.join(t,PROFILE_FILE),forgotten:path.join(t,FORGOTTEN_FILE)}}function safeHolderId(e){const r=null==e?"":String(e).trim();return r&&identity.HOLDER_ID_RE&&identity.HOLDER_ID_RE.test(r)?r:null}function readJsonFile(e){try{const r=fs.readFileSync(e,"utf8"),t=JSON.parse(r);return t&&"object"==typeof t?t:null}catch(e){return null}}function writeJsonFile(e,r){fs.mkdirSync(path.dirname(e),{recursive:!0,mode:448});const t=`${e}.tmp`;return fs.writeFileSync(t,`${JSON.stringify(r,null,2)}\n`),fs.renameSync(t,e),r}function readLedger(e){let r="";try{r=fs.readFileSync(e,"utf8")}catch(e){return[]}const t=[];for(const e of r.split("\n")){const r=e.trim();if(r)try{const e=JSON.parse(r);e&&"object"==typeof e&&t.push(e)}catch(e){}}return t}function appendLedgerRow(e,r){return fs.mkdirSync(path.dirname(e),{recursive:!0,mode:448}),fs.appendFileSync(e,`${JSON.stringify(r)}\n`),r}function readRegistry(e){const r=readJsonFile(registryPath(e));try{return identity.load(r||identity.emptyRegistry())}catch(e){return identity.load(identity.emptyRegistry())}}function writeRegistry(e,r){const t=identity.load(r);return writeJsonFile(registryPath(e),t),t}function holderRow(e,r){const t=safeHolderId(r);return t&&(e.holders||[]).find((e=>e&&e.id===t))||null}function ensureHolder(e,r={}){const t=r.registry||readRegistry(e),o=r.id?safeHolderId(r.id):null,n=o?holderRow(t,o):null;if(n)return{registry:t,row:n,created:!1};const i=identity.addHolder(t,{id:o||void 0,displayName:r.displayName,fingerprint:r.fingerprint,accountId:r.accountId,now:r.now}),s=identity.load(i),d=s.holders[s.holders.length-1]||null;return!1!==r.persist&&writeRegistry(e,s),{registry:s,row:d,created:!0}}function levelOf(e){try{const r=xp.evaluate(Array.isArray(e)?e:[]);return r&&Number.isFinite(r.level)?r.level:1}catch(e){return 1}}function readForgotten(e,r){const t=readJsonFile(holderPaths(e,r).forgotten),o=t&&Array.isArray(t.entries)?t.entries:[];return new Set(o.map(String))}function writeForgotten(e,r,t){const o=Array.from(new Set(Array.from(t).map(String))).sort();return writeJsonFile(holderPaths(e,r).forgotten,{schema:1,holderId:r,entries:o}),new Set(o)}function emptyProfile(e){return{schema:1,holderId:e,facets:[],budget:{tokens:0,used:0},coldStart:!0}}function loadState(e,r,t={}){const o=safeHolderId(r);if(!o)throw new Error("loadState: expected a holder id (h_ + 8-16 hex)");const n=holderRow(t.registry||readRegistry(e),o),i=holderPaths(e,o),s=readLedger(i.ledger),d=readJsonFile(i.persona);let l=personaModule.defaultPersona();if(d)try{const e=personaModule.load(d);l=e&&e.persona?e.persona:e}catch(e){l=personaModule.defaultPersona()}else l=personaModule.defaultPersona();const a=readJsonFile(i.profile)||emptyProfile(o),c=readForgotten(e,o);return{holderId:o,displayName:n&&n.displayName||personaDisplayName(l)||identity.DEFAULT_DISPLAY_NAME,ledger:s,persona:l,profile:a&&a.holderId===o?a:{...a,holderId:o},forgotten:c,level:levelOf(s),scope:MIRROR_SESSIONS.map((e=>mirrorSession(o,e.split(":")[1])))}}function personaDisplayName(e){const r=e&&e.identity&&e.identity.name;return"string"==typeof r&&r.trim()?r.trim():null}function writeProfile(e,r,t){const o={...t||{},holderId:r};return writeJsonFile(holderPaths(e,r).profile,o),o}function writePersona(e,r,t){return writeJsonFile(holderPaths(e,r).persona,{...t||{},holderId:r}),t}function switchHolder(e,r,t={}){const o=safeHolderId(r);if(!o)return{ok:!1,reason:"expected a holder id (h_ + 8-16 hex)",survived:SURVIVES_SWITCH.slice()};const n=t.registry||readRegistry(e);if(!holderRow(n,o)&&!t.create)return{ok:!1,reason:`unknown holder ${o}`,survived:SURVIVES_SWITCH.slice()};let i=n;if(holderRow(n,o)){try{i=identity.setActive(n,o)}catch(e){i={...n,active:o}}i=writeRegistry(e,i)}else{const r=ensureHolder(e,{id:o,displayName:t.displayName,now:t.now});i=writeRegistry(e,identity.setActive(r.registry,o))}const s=loadState(e,o,{registry:i});return{ok:!0,previous:t.previous||n&&n.active||null,state:s,registry:i,survived:SURVIVES_SWITCH.slice()}}function resolveActive(e,r={},t={}){const o=t.registry||readRegistry(e),n=identity.resolve(r,o);if(n.needsMint||!n.holderId)return{registry:o,resolution:n,holderId:null,created:!1};const i=holderRow(o,n.holderId);if(i)return{registry:o,resolution:n,holderId:i.id,created:!1};const s=ensureHolder(e,{id:n.holderId,displayName:n.displayName,fingerprint:n.proposal?n.proposal.fingerprint:void 0,now:t.now,registry:o});return{registry:s.registry,resolution:n,holderId:s.row?s.row.id:n.holderId,created:s.created}}function applyConflict(e,r,t={}){const o=identity.load(e||identity.emptyRegistry()),n=r&&r.conflict,i=r&&r.holderId||o.active,s=t&&"rebind"===t.choice?"rebind":"keep-separate";if(!n||!i)return{ok:!1,action:s,reason:"no conflict to resolve",registry:o,requiresConfirm:!1};const d=n.a===i?n.b:n.a;if("keep-separate"===s)return{ok:!0,action:s,registry:o,requiresConfirm:!1,kept:[i,d].filter(Boolean)};if(!0!==t.confirm)return{ok:!1,action:s,reason:"rebind requires an explicit confirm",registry:o,requiresConfirm:!0,kept:[i,d].filter(Boolean)};const l=new Set((o.holders||[]).filter((e=>e&&e.id!==i&&e.id===d)).map((e=>e.id))),a=(o.holders||[]).map((e=>e?e.id===i?{...e,fingerprint:r.proposal?r.proposal.fingerprint:e.fingerprint}:l.has(e.id)?{...e,fingerprint:void 0}:e:e));return{ok:!0,action:s,requiresConfirm:!1,registry:identity.load({...o,holders:a}),rebound:{kept:i,unbound:Array.from(l)}}}function entriesForHolder(e,r,t={}){const o=Array.isArray(e)?e:[],n=safeHolderId(r),i=t.forgotten instanceof Set?t.forgotten:new Set;return!1!==t.clientFilter&&n?o.filter((e=>{if(!e||"object"!=typeof e)return!1;if(i.has(String(rowId(e))))return!1;try{return profileModule.belongsTo(e,n)}catch(r){return String(e.holder||"")===n}})):o.filter((e=>e&&"object"==typeof e&&!i.has(String(rowId(e)))))}function rowId(e){return e&&"object"==typeof e?null!=e.id?e.id:null!=e.entryId?e.entryId:"":""}function stampEntry(e,r){const t=safeHolderId(r);return e&&"object"==typeof e?t?{...e,holder:t}:{...e,holder:void 0}:e}function provenance(e,r,t,o={}){const n=(e&&Array.isArray(e.facets)?e.facets:[]).find((e=>e&&e.id===t))||null;if(!n)return{ok:!1,reason:`no facet ${t}`,facet:null,claims:[]};const i=new Map;for(const e of Array.isArray(r)?r:[]){const r=rowId(e);r&&i.set(String(r),e)}const s=(Array.isArray(n.sources)?n.sources:[]).map((e=>{const r=i.get(String(e))||null;return{entryId:String(e),text:r?textOf(r):"",kind:r?String(r.kind||""):"",at:r&&Number.isFinite(Number(r.t))?Number(r.t):null,present:Boolean(r)}}));return{ok:!0,facet:{id:n.id,key:n.key,value:n.value,confidence:n.confidence},claims:s,missing:s.filter((e=>!e.present)).map((e=>e.entryId))}}function textOf(e){if(!e||"object"!=typeof e)return"";const r=null!=e.text?e.text:null!=e.content?e.content:e.value;return"string"==typeof r?r:""}function forgetFacet(e,r,t,o){const n=t&&Array.isArray(t.facets)?t.facets:[],i=n.find((e=>e&&e.id===o))||null;if(!i)return{ok:!1,reason:`no facet ${o}`,profile:t,removed:[],facetIds:[]};const s=(Array.isArray(i.sources)?i.sources:[]).map(String),d=readForgotten(e,r);for(const e of s)d.add(e);writeForgotten(e,r,d);const l=n.filter((e=>e&&e.id!==i.id)).map((e=>{const r=(Array.isArray(e.sources)?e.sources:[]).filter((e=>!d.has(String(e))));return{...e,sources:r}})).filter((e=>(Array.isArray(e.sources)?e.sources.length:0)>0)),a=n.filter((e=>e&&e.id!==i.id)).length-l.length,c={...t||{},holderId:r,facets:l,budget:{tokens:t&&t.budget&&t.budget.tokens||profileModule.BUDGET_TOKENS,used:0},coldStart:0===l.length};return writeProfile(e,r,c),{ok:!0,profile:c,removed:s,facetIds:[i.id].concat(a>0?l.map((e=>e.id)):[])}}module.exports={AVATAR_DIR:AVATAR_DIR,REGISTRY_FILE:REGISTRY_FILE,HOLDERS_DIR:HOLDERS_DIR,LEDGER_FILE:LEDGER_FILE,PERSONA_FILE:PERSONA_FILE,PROFILE_FILE:PROFILE_FILE,FORGOTTEN_FILE:FORGOTTEN_FILE,SURVIVES_SWITCH:SURVIVES_SWITCH,MIRROR_SESSIONS:MIRROR_SESSIONS,mirrorSession:mirrorSession,registryPath:registryPath,holderDir:holderDir,holderPaths:holderPaths,readJsonFile:readJsonFile,writeJsonFile:writeJsonFile,readLedger:readLedger,appendLedgerRow:appendLedgerRow,readRegistry:readRegistry,writeRegistry:writeRegistry,holderRow:holderRow,ensureHolder:ensureHolder,levelOf:levelOf,readForgotten:readForgotten,writeForgotten:writeForgotten,emptyProfile:emptyProfile,loadState:loadState,writeProfile:writeProfile,writePersona:writePersona,switchHolder:switchHolder,resolveActive:resolveActive,applyConflict:applyConflict,entriesForHolder:entriesForHolder,stampEntry:stampEntry,provenance:provenance,forgetFacet:forgetFacet};
|