@adam0white/human-framework 2.14.1 → 2.16.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 +145 -87
- package/dist/agenda/agenda.d.ts +16 -0
- package/dist/agenda/agenda.js +57 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +4 -2
- package/dist/persona-calendar.d.ts +39 -0
- package/dist/persona-calendar.js +411 -0
- package/dist/persona.d.ts +76 -0
- package/dist/persona.js +457 -0
- package/package.json +1 -1
package/dist/persona.js
ADDED
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCOPE: opt-in, deterministic projection between host-authored chapter openings. Copies exact temperament,
|
|
3
|
+
* values and RNG, explicitly mapped skills, episodes and commitments, with declared clock offsets. Checks
|
|
4
|
+
* identity, age, family and health compatibility before returning a new Person. Neither input is mutated.
|
|
5
|
+
* This is not a world migration, identity registry, transport, hash, authentication or general calendar mapper.
|
|
6
|
+
* v1 admits zero-duration travel and healthy compatible bodies only; family/bonds remain host-authored and must
|
|
7
|
+
* match source provenance. Pregnancy and nonempty temporal bonds are unsupported in v1. All other world-dependent stores stay with the source and are disclosed as omitted.
|
|
8
|
+
*/
|
|
9
|
+
import { hasUnresolvedMakeUps, sameChain } from './agenda/agenda.js';
|
|
10
|
+
import { createPerson } from './person.js';
|
|
11
|
+
import { restore, snapshot } from './restore.js';
|
|
12
|
+
import { ENGINE_VERSION, MAX_MINUTE, PERSON_SCHEMA } from './types.js';
|
|
13
|
+
function fail(reason) {
|
|
14
|
+
throw new Error(`persona: ${reason}`);
|
|
15
|
+
}
|
|
16
|
+
function obj(x) {
|
|
17
|
+
if (!x || typeof x !== 'object' || Array.isArray(x))
|
|
18
|
+
fail('expected object');
|
|
19
|
+
}
|
|
20
|
+
function keys(x, allowed, required = allowed) {
|
|
21
|
+
obj(x);
|
|
22
|
+
if (Object.keys(x).some((k) => !allowed.includes(k)) || required.some((k) => !Object.hasOwn(x, k)))
|
|
23
|
+
fail('unsupported or missing field');
|
|
24
|
+
}
|
|
25
|
+
function text(x) {
|
|
26
|
+
if (typeof x !== 'string' ||
|
|
27
|
+
!x.length ||
|
|
28
|
+
x.length > 1024 ||
|
|
29
|
+
['__proto__', 'constructor', 'prototype'].includes(x))
|
|
30
|
+
fail('invalid text/reference');
|
|
31
|
+
}
|
|
32
|
+
function num(x, min = -MAX_MINUTE, max = MAX_MINUTE) {
|
|
33
|
+
if (typeof x !== 'number' || !Number.isFinite(x) || x < min || x > max)
|
|
34
|
+
fail('invalid number/timestamp');
|
|
35
|
+
}
|
|
36
|
+
function texts(x, max = 128) {
|
|
37
|
+
if (!Array.isArray(x) || x.length > max)
|
|
38
|
+
fail('invalid list');
|
|
39
|
+
for (const value of x)
|
|
40
|
+
text(value);
|
|
41
|
+
}
|
|
42
|
+
function canonical(x) {
|
|
43
|
+
return JSON.stringify(x, (_key, value) => value && typeof value === 'object' && !Array.isArray(value)
|
|
44
|
+
? Object.fromEntries(Object.entries(value).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))
|
|
45
|
+
: value);
|
|
46
|
+
}
|
|
47
|
+
function same(a, b) {
|
|
48
|
+
return canonical(a) === canonical(b);
|
|
49
|
+
}
|
|
50
|
+
function compatibleBody(p) {
|
|
51
|
+
if (!p.body.alive)
|
|
52
|
+
fail('dead person cannot travel or be revived');
|
|
53
|
+
if (p.body.health <= 0 ||
|
|
54
|
+
p.body.injuries.length ||
|
|
55
|
+
p.body.illnesses.length ||
|
|
56
|
+
p.body.downed ||
|
|
57
|
+
Object.keys(p.body.exposures ?? {}).length)
|
|
58
|
+
fail('unsupported health condition');
|
|
59
|
+
}
|
|
60
|
+
function boundRecord(x) {
|
|
61
|
+
obj(x);
|
|
62
|
+
if (Object.keys(x).length > 128)
|
|
63
|
+
fail('too many mappings');
|
|
64
|
+
for (const key of Object.keys(x))
|
|
65
|
+
text(key);
|
|
66
|
+
}
|
|
67
|
+
function episode(x) {
|
|
68
|
+
keys(x, [
|
|
69
|
+
'id',
|
|
70
|
+
'at',
|
|
71
|
+
'kind',
|
|
72
|
+
'action',
|
|
73
|
+
'actorId',
|
|
74
|
+
'targetId',
|
|
75
|
+
'placeId',
|
|
76
|
+
'valence',
|
|
77
|
+
'salience',
|
|
78
|
+
'summary',
|
|
79
|
+
'tags',
|
|
80
|
+
'voiceId',
|
|
81
|
+
'recalledAt',
|
|
82
|
+
], ['id', 'at', 'kind', 'valence', 'salience', 'summary', 'tags']);
|
|
83
|
+
for (const key of ['id', 'kind', 'summary', 'action', 'actorId', 'targetId', 'placeId', 'voiceId'])
|
|
84
|
+
if (x[key] !== undefined)
|
|
85
|
+
text(x[key]);
|
|
86
|
+
num(x.at);
|
|
87
|
+
num(x.valence, -1, 1);
|
|
88
|
+
num(x.salience, 0, 1);
|
|
89
|
+
texts(x.tags);
|
|
90
|
+
if (x.recalledAt !== undefined)
|
|
91
|
+
num(x.recalledAt);
|
|
92
|
+
}
|
|
93
|
+
function commitment(x) {
|
|
94
|
+
keys(x, [
|
|
95
|
+
'id',
|
|
96
|
+
'kind',
|
|
97
|
+
'toId',
|
|
98
|
+
'actions',
|
|
99
|
+
'targetId',
|
|
100
|
+
'from',
|
|
101
|
+
'until',
|
|
102
|
+
'normId',
|
|
103
|
+
'importance',
|
|
104
|
+
'status',
|
|
105
|
+
'recurEvery',
|
|
106
|
+
'label',
|
|
107
|
+
'chain',
|
|
108
|
+
], ['id', 'kind', 'actions', 'from', 'until', 'importance', 'status']);
|
|
109
|
+
for (const key of ['id', 'toId', 'targetId', 'normId', 'label', 'chain'])
|
|
110
|
+
if (x[key] !== undefined)
|
|
111
|
+
text(x[key]);
|
|
112
|
+
if (!['promise', 'duty', 'appointment', 'worship', 'job'].includes(String(x.kind)) ||
|
|
113
|
+
x.status !== 'pending')
|
|
114
|
+
fail('unsupported obligation');
|
|
115
|
+
texts(x.actions);
|
|
116
|
+
if (!x.actions.length)
|
|
117
|
+
fail('empty obligation actions');
|
|
118
|
+
num(x.from);
|
|
119
|
+
num(x.until);
|
|
120
|
+
if (x.until < x.from)
|
|
121
|
+
fail('invalid obligation window');
|
|
122
|
+
num(x.importance, 0, 1);
|
|
123
|
+
if (x.recurEvery !== undefined)
|
|
124
|
+
num(x.recurEvery, 1);
|
|
125
|
+
}
|
|
126
|
+
function boundedJson(input) {
|
|
127
|
+
const pending = [{ value: input, depth: 0 }];
|
|
128
|
+
const seen = new Set();
|
|
129
|
+
let nodes = 0;
|
|
130
|
+
while (pending.length) {
|
|
131
|
+
const item = pending.pop();
|
|
132
|
+
if (!item)
|
|
133
|
+
break;
|
|
134
|
+
if (++nodes > 8192 || item.depth > 16)
|
|
135
|
+
fail('JSON bounds exceeded');
|
|
136
|
+
const { value, depth } = item;
|
|
137
|
+
if (value === null || typeof value === 'boolean')
|
|
138
|
+
continue;
|
|
139
|
+
if (typeof value === 'string') {
|
|
140
|
+
if (value.length > 1024)
|
|
141
|
+
fail('text bounds exceeded');
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (typeof value === 'number') {
|
|
145
|
+
num(value);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (typeof value !== 'object' || seen.has(value))
|
|
149
|
+
fail('non-JSON or cyclic input');
|
|
150
|
+
seen.add(value);
|
|
151
|
+
if (!Array.isArray(value) &&
|
|
152
|
+
Object.getPrototypeOf(value) !== Object.prototype &&
|
|
153
|
+
Object.getPrototypeOf(value) !== null)
|
|
154
|
+
fail('non-JSON object');
|
|
155
|
+
for (const [key, child] of Object.entries(value)) {
|
|
156
|
+
if (['__proto__', 'prototype', 'constructor'].includes(key))
|
|
157
|
+
fail('unsafe reference');
|
|
158
|
+
pending.push({ value: child, depth: depth + 1 });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/** Validates untrusted portable data without restoring defaults or accepting extra live fields. */
|
|
163
|
+
export function validatePersonaProjection(input) {
|
|
164
|
+
boundedJson(input);
|
|
165
|
+
keys(input, [
|
|
166
|
+
'format',
|
|
167
|
+
'mappingVersion',
|
|
168
|
+
'source',
|
|
169
|
+
'name',
|
|
170
|
+
'life',
|
|
171
|
+
'rng',
|
|
172
|
+
'traits',
|
|
173
|
+
'values',
|
|
174
|
+
'health',
|
|
175
|
+
'family',
|
|
176
|
+
'bonds',
|
|
177
|
+
'skills',
|
|
178
|
+
'skillRetention',
|
|
179
|
+
'episodes',
|
|
180
|
+
'commitments',
|
|
181
|
+
'limitations',
|
|
182
|
+
]);
|
|
183
|
+
if (input.format !== 'hf-persona-state/1')
|
|
184
|
+
fail('unsupported format');
|
|
185
|
+
text(input.mappingVersion);
|
|
186
|
+
text(input.name);
|
|
187
|
+
keys(input.source, ['schema', 'engine', 'id', 'now']);
|
|
188
|
+
if (input.source.schema !== PERSON_SCHEMA || input.source.engine !== ENGINE_VERSION)
|
|
189
|
+
fail('unsupported source schema/engine; migrate the source before projection');
|
|
190
|
+
text(input.source.id);
|
|
191
|
+
num(input.source.now);
|
|
192
|
+
keys(input.life, ['bornAt', 'sex']);
|
|
193
|
+
num(input.life.bornAt);
|
|
194
|
+
if (!['female', 'male'].includes(String(input.life.sex)))
|
|
195
|
+
fail('invalid sex');
|
|
196
|
+
keys(input.rng, ['a', 'b', 'c', 'd']);
|
|
197
|
+
for (const [key, value] of Object.entries(input.rng)) {
|
|
198
|
+
num(value, key === 'a' ? -0x80000000 : 0, key === 'a' ? 0x7fffffff : 0xffffffff);
|
|
199
|
+
if (!Number.isInteger(value))
|
|
200
|
+
fail('invalid RNG');
|
|
201
|
+
}
|
|
202
|
+
const base = createPerson({ id: 'validation', name: 'validation', seed: 0, bornAt: 0, sex: 'female' });
|
|
203
|
+
for (const slice of ['traits', 'values']) {
|
|
204
|
+
keys(input[slice], Object.keys(base[slice]));
|
|
205
|
+
for (const value of Object.values(input[slice]))
|
|
206
|
+
num(value, 0, 1);
|
|
207
|
+
}
|
|
208
|
+
keys(input.health, ['health', 'fitness', 'circadianPeak']);
|
|
209
|
+
num(input.health.health, Number.MIN_VALUE, 1);
|
|
210
|
+
num(input.health.fitness, 0, 1);
|
|
211
|
+
num(input.health.circadianPeak, 0, 1440);
|
|
212
|
+
boundRecord(input.skills);
|
|
213
|
+
for (const value of Object.values(input.skills)) {
|
|
214
|
+
keys(value, ['level', 'practice', 'lastPracticed']);
|
|
215
|
+
num(value.level, 0, 1);
|
|
216
|
+
num(value.practice, 0);
|
|
217
|
+
num(value.lastPracticed);
|
|
218
|
+
if (value.lastPracticed > input.source.now)
|
|
219
|
+
fail('future skill evidence');
|
|
220
|
+
}
|
|
221
|
+
if (input.skillRetention !== null) {
|
|
222
|
+
keys(input.skillRetention, ['consolidationHours']);
|
|
223
|
+
num(input.skillRetention.consolidationHours, Number.MIN_VALUE);
|
|
224
|
+
}
|
|
225
|
+
if (input.family !== null) {
|
|
226
|
+
obj(input.family);
|
|
227
|
+
if (input.family.pregnancy !== undefined)
|
|
228
|
+
fail('unsupported temporal family state: pregnancy');
|
|
229
|
+
}
|
|
230
|
+
if (input.bonds !== null) {
|
|
231
|
+
obj(input.bonds);
|
|
232
|
+
if (input.bonds.mourning !== undefined ||
|
|
233
|
+
(Array.isArray(input.bonds.courtships) && input.bonds.courtships.length > 0) ||
|
|
234
|
+
(Array.isArray(input.bonds.marriages) && input.bonds.marriages.length > 0)) {
|
|
235
|
+
fail('unsupported temporal bonds state');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
for (const field of ['family', 'bonds']) {
|
|
239
|
+
if (input[field] !== null) {
|
|
240
|
+
obj(input[field]);
|
|
241
|
+
const check = snapshot(base);
|
|
242
|
+
Object.assign(check, { [field]: input[field] });
|
|
243
|
+
if (!same(restore(check)[field], input[field]))
|
|
244
|
+
fail(`invalid ${field} provenance`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (!Array.isArray(input.episodes) ||
|
|
248
|
+
input.episodes.length > 128 ||
|
|
249
|
+
!Array.isArray(input.commitments) ||
|
|
250
|
+
input.commitments.length > 128)
|
|
251
|
+
fail('invalid portable lists');
|
|
252
|
+
for (const value of input.episodes) {
|
|
253
|
+
episode(value);
|
|
254
|
+
if (value.at > input.source.now ||
|
|
255
|
+
(value.recalledAt !== undefined && value.recalledAt > input.source.now))
|
|
256
|
+
fail('future episode evidence');
|
|
257
|
+
}
|
|
258
|
+
for (const value of input.commitments)
|
|
259
|
+
commitment(value);
|
|
260
|
+
for (const list of [input.episodes, input.commitments])
|
|
261
|
+
if (new Set(list.map((x) => x.id)).size !== list.length)
|
|
262
|
+
fail('duplicate portable id');
|
|
263
|
+
texts(input.limitations, 256);
|
|
264
|
+
return structuredClone(input);
|
|
265
|
+
}
|
|
266
|
+
/** Hosts must migrate supported old saves first. No random draws or mutation occur here. */
|
|
267
|
+
export function projectPersona(source, mapping) {
|
|
268
|
+
const p = restore(snapshot(source));
|
|
269
|
+
compatibleBody(p);
|
|
270
|
+
if (p.activity)
|
|
271
|
+
fail('running activity');
|
|
272
|
+
if (p.agenda.goals.some((g) => g.status === 'active') || p.projects?.currentGoalId)
|
|
273
|
+
fail('active goal/project');
|
|
274
|
+
if (hasUnresolvedMakeUps(p) || p.agenda.lapse)
|
|
275
|
+
fail('unmapped owed obligation');
|
|
276
|
+
text(mapping.version);
|
|
277
|
+
boundRecord(mapping.skills);
|
|
278
|
+
const skills = Object.create(null);
|
|
279
|
+
for (const [from, to] of Object.entries(mapping.skills).sort()) {
|
|
280
|
+
text(to);
|
|
281
|
+
const skill = p.skills[from];
|
|
282
|
+
if (!Object.hasOwn(p.skills, from) || !skill || Object.hasOwn(skills, to))
|
|
283
|
+
fail('missing/duplicate skill mapping');
|
|
284
|
+
skills[to] = structuredClone(skill);
|
|
285
|
+
}
|
|
286
|
+
const episodes = [];
|
|
287
|
+
for (const [id, map] of Object.entries(mapping.episodes ?? {}).sort()) {
|
|
288
|
+
const old = p.memory.episodes.find((e) => e.id === id);
|
|
289
|
+
if (!old)
|
|
290
|
+
fail('missing approved episode');
|
|
291
|
+
keys(map, ['id', 'action', 'actorId', 'targetId', 'placeId', 'voiceId', 'tags'], ['id']);
|
|
292
|
+
// Every existing semantic reference needs a declared mapping; omission must never preserve host-local ids.
|
|
293
|
+
for (const ref of ['action', 'actorId', 'targetId', 'placeId', 'voiceId'])
|
|
294
|
+
if (old[ref] !== undefined && map[ref] === undefined)
|
|
295
|
+
fail(`unmapped episode ${ref}`);
|
|
296
|
+
const copy = {
|
|
297
|
+
id: map.id,
|
|
298
|
+
at: old.at,
|
|
299
|
+
kind: old.kind,
|
|
300
|
+
valence: old.valence,
|
|
301
|
+
salience: old.salience,
|
|
302
|
+
summary: old.summary,
|
|
303
|
+
tags: map.tags ?? [],
|
|
304
|
+
};
|
|
305
|
+
for (const ref of ['action', 'actorId', 'targetId', 'placeId', 'voiceId'])
|
|
306
|
+
if (map[ref] !== undefined)
|
|
307
|
+
copy[ref] = map[ref];
|
|
308
|
+
if (old.recalledAt !== undefined)
|
|
309
|
+
copy.recalledAt = old.recalledAt;
|
|
310
|
+
episodes.push(copy);
|
|
311
|
+
}
|
|
312
|
+
const commitments = [];
|
|
313
|
+
const bindings = mapping.commitments ?? {};
|
|
314
|
+
for (const old of p.agenda.commitments) {
|
|
315
|
+
// Closed history no longer owns the chain once an actual later instance exists, including a release.
|
|
316
|
+
if (old.status !== 'pending' &&
|
|
317
|
+
p.agenda.commitments.some((next) => next.from > old.from && sameChain(old, next)))
|
|
318
|
+
continue;
|
|
319
|
+
const ongoing = old.status === 'pending' ||
|
|
320
|
+
(old.recurEvery !== undefined &&
|
|
321
|
+
old.status !== 'released' &&
|
|
322
|
+
(old.recurUntil === undefined || old.recurUntil > p.now));
|
|
323
|
+
if (!ongoing)
|
|
324
|
+
continue;
|
|
325
|
+
const map = Object.hasOwn(bindings, old.id) ? bindings[old.id] : undefined;
|
|
326
|
+
if (!map || old.status !== 'pending')
|
|
327
|
+
fail(`unmapped obligation ${old.id}`);
|
|
328
|
+
keys(map, ['id', 'actions', 'toId', 'targetId', 'normId', 'chain', 'from', 'until'], ['id', 'actions', 'from', 'until']);
|
|
329
|
+
if (old.until - old.from !== map.until - map.from)
|
|
330
|
+
fail('obligation duration changed');
|
|
331
|
+
for (const ref of ['toId', 'targetId', 'normId', 'chain'])
|
|
332
|
+
if (old[ref] !== undefined && map[ref] === undefined)
|
|
333
|
+
fail(`unmapped obligation ${ref}`);
|
|
334
|
+
const copy = {
|
|
335
|
+
id: map.id,
|
|
336
|
+
kind: old.kind,
|
|
337
|
+
actions: [...map.actions],
|
|
338
|
+
from: map.from,
|
|
339
|
+
until: map.until,
|
|
340
|
+
importance: old.importance,
|
|
341
|
+
status: 'pending',
|
|
342
|
+
};
|
|
343
|
+
for (const ref of ['toId', 'targetId', 'normId', 'chain'])
|
|
344
|
+
if (map[ref] !== undefined)
|
|
345
|
+
copy[ref] = map[ref];
|
|
346
|
+
if (old.recurEvery !== undefined)
|
|
347
|
+
copy.recurEvery = old.recurEvery;
|
|
348
|
+
if (old.label !== undefined)
|
|
349
|
+
copy.label = old.label;
|
|
350
|
+
if (old.exempt ||
|
|
351
|
+
old.makeUpOf ||
|
|
352
|
+
old.recurUntil !== undefined ||
|
|
353
|
+
old.violatedBy ||
|
|
354
|
+
old.firm ||
|
|
355
|
+
old.exemptWhen)
|
|
356
|
+
fail('unsupported obligation semantics');
|
|
357
|
+
commitments.push(copy);
|
|
358
|
+
}
|
|
359
|
+
for (const id of Object.keys(bindings))
|
|
360
|
+
if (!p.agenda.commitments.some((c) => c.id === id && c.status === 'pending'))
|
|
361
|
+
fail('missing obligation binding');
|
|
362
|
+
const limitations = [
|
|
363
|
+
'source transient body/needs/affect/ambient omitted; target arrival policy',
|
|
364
|
+
'source habits/methods/beliefs/social/conscience omitted; target authored state',
|
|
365
|
+
'source optional behaviour stores and decision history omitted; target authored state',
|
|
366
|
+
'family/bonds provenance checked; target authored state',
|
|
367
|
+
];
|
|
368
|
+
for (const key of Object.keys(p.skills).sort())
|
|
369
|
+
if (!Object.hasOwn(mapping.skills, key))
|
|
370
|
+
limitations.push(`skill:${key}`);
|
|
371
|
+
for (const e of p.memory.episodes)
|
|
372
|
+
if (!Object.hasOwn(mapping.episodes ?? {}, e.id))
|
|
373
|
+
limitations.push(`episode:${e.id}`);
|
|
374
|
+
return validatePersonaProjection({
|
|
375
|
+
format: 'hf-persona-state/1',
|
|
376
|
+
mappingVersion: mapping.version,
|
|
377
|
+
source: { schema: source.schema, engine: p.engine, id: p.id, now: p.now },
|
|
378
|
+
name: p.name,
|
|
379
|
+
life: p.life,
|
|
380
|
+
rng: p.rng,
|
|
381
|
+
traits: p.traits,
|
|
382
|
+
values: p.values,
|
|
383
|
+
health: { health: p.body.health, fitness: p.body.fitness, circadianPeak: p.body.circadianPeak },
|
|
384
|
+
family: p.family ?? null,
|
|
385
|
+
bonds: p.bonds ?? null,
|
|
386
|
+
skills,
|
|
387
|
+
skillRetention: p.skillRetention ?? null,
|
|
388
|
+
episodes,
|
|
389
|
+
commitments,
|
|
390
|
+
limitations,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
/** Return a detached target opening; a later shelf change cannot affect it. Host owns cast/branch/death authority. */
|
|
394
|
+
export function admitPersona(input, target, policy) {
|
|
395
|
+
const projection = validatePersonaProjection(input);
|
|
396
|
+
const p = restore(snapshot(target));
|
|
397
|
+
if (projection.mappingVersion !== policy.mappingVersion)
|
|
398
|
+
fail('unsupported mapping');
|
|
399
|
+
if (projection.source.id !== policy.sourceId || p.id !== policy.targetId)
|
|
400
|
+
fail('identity binding conflict');
|
|
401
|
+
compatibleBody(p);
|
|
402
|
+
const offset = p.now - projection.source.now;
|
|
403
|
+
const shift = (at) => {
|
|
404
|
+
const mapped = at + offset;
|
|
405
|
+
num(mapped);
|
|
406
|
+
return mapped;
|
|
407
|
+
};
|
|
408
|
+
if (p.life.sex !== projection.life.sex ||
|
|
409
|
+
p.life.bornAt !== shift(projection.life.bornAt) ||
|
|
410
|
+
p.name !== projection.name)
|
|
411
|
+
fail('identity/age conflict');
|
|
412
|
+
if (!same(p.family ?? null, projection.family) || !same(p.bonds ?? null, projection.bonds))
|
|
413
|
+
fail('family/bonds conflict');
|
|
414
|
+
if (!same({ health: p.body.health, fitness: p.body.fitness, circadianPeak: p.body.circadianPeak }, projection.health))
|
|
415
|
+
fail('health compatibility conflict');
|
|
416
|
+
if (p.activity)
|
|
417
|
+
fail('target already active');
|
|
418
|
+
p.traits = projection.traits;
|
|
419
|
+
p.values = projection.values;
|
|
420
|
+
p.rng = projection.rng;
|
|
421
|
+
// A sparse projection replaces target priors for portable skill/history stores, never blends evidence.
|
|
422
|
+
p.skills = projection.skills;
|
|
423
|
+
if (projection.skillRetention)
|
|
424
|
+
p.skillRetention = projection.skillRetention;
|
|
425
|
+
else
|
|
426
|
+
delete p.skillRetention;
|
|
427
|
+
for (const skill of Object.values(p.skills))
|
|
428
|
+
skill.lastPracticed = shift(skill.lastPracticed);
|
|
429
|
+
p.memory.episodes = projection.episodes;
|
|
430
|
+
for (const e of p.memory.episodes) {
|
|
431
|
+
e.at = shift(e.at);
|
|
432
|
+
if (e.recalledAt !== undefined)
|
|
433
|
+
e.recalledAt = shift(e.recalledAt);
|
|
434
|
+
}
|
|
435
|
+
p.memory.nextEpisode = Math.max(p.memory.nextEpisode, p.memory.episodes.length + 1);
|
|
436
|
+
for (const e of p.memory.episodes) {
|
|
437
|
+
if (/^e\d+$/.test(e.id)) {
|
|
438
|
+
const next = Number(e.id.slice(1)) + 1;
|
|
439
|
+
num(next, 0);
|
|
440
|
+
p.memory.nextEpisode = Math.max(p.memory.nextEpisode, next);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
for (const c of projection.commitments) {
|
|
444
|
+
if (c.until < p.now ||
|
|
445
|
+
p.agenda.commitments.some((old) => old.id === c.id || (c.chain && old.chain === c.chain)))
|
|
446
|
+
fail('target obligation conflict');
|
|
447
|
+
p.agenda.commitments.push(c);
|
|
448
|
+
}
|
|
449
|
+
p.agenda.nextId = Math.max(p.agenda.nextId, p.agenda.commitments.length + 1);
|
|
450
|
+
const restored = restore(p);
|
|
451
|
+
if (!same(restored, p))
|
|
452
|
+
fail('admitted state exceeds supported bounds');
|
|
453
|
+
return {
|
|
454
|
+
person: restored,
|
|
455
|
+
report: { mappingVersion: policy.mappingVersion, offset, limitations: [...projection.limitations] },
|
|
456
|
+
};
|
|
457
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adam0white/human-framework",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.16.0",
|
|
4
4
|
"description": "Deterministic, explainable simulated people: body, needs, affect, memory, beliefs, skills, habits, relationships, values and a will that can refuse a suggestion",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"simulation",
|