@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
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCOPE: opt-in calendar transport between declared chapter clocks. Hosts map every retained reference and
|
|
3
|
+
* supply identical source-backed norm meanings. Carries pending/continuing duties, owed records and their
|
|
4
|
+
* receipts, held understanding and lapse timestamps. Does not choose dates, waive debt, simulate travel,
|
|
5
|
+
* create rulings, migrate activities/projects or change the v1 persona path. Neither input is mutated.
|
|
6
|
+
*/
|
|
7
|
+
import { isContinuingCommitment } from './agenda/agenda.js';
|
|
8
|
+
import { createPerson } from './person.js';
|
|
9
|
+
import { admitPersona, projectPersona, validatePersonaProjection, } from './persona.js';
|
|
10
|
+
import { restore, snapshot } from './restore.js';
|
|
11
|
+
import { MAX_MINUTE, } from './types.js';
|
|
12
|
+
function fail(message) {
|
|
13
|
+
throw new Error(`persona calendar: ${message}`);
|
|
14
|
+
}
|
|
15
|
+
function object(x) {
|
|
16
|
+
if (!x || typeof x !== 'object' || Array.isArray(x))
|
|
17
|
+
fail('expected object');
|
|
18
|
+
}
|
|
19
|
+
function keys(x, allowed, required = allowed) {
|
|
20
|
+
object(x);
|
|
21
|
+
if (Object.keys(x).some((k) => !allowed.includes(k)) || required.some((k) => !Object.hasOwn(x, k)))
|
|
22
|
+
fail('unsupported or missing field');
|
|
23
|
+
}
|
|
24
|
+
function text(x) {
|
|
25
|
+
if (typeof x !== 'string' ||
|
|
26
|
+
!x.length ||
|
|
27
|
+
x.length > 1024 ||
|
|
28
|
+
['__proto__', 'constructor', 'prototype'].includes(x))
|
|
29
|
+
fail('invalid reference');
|
|
30
|
+
}
|
|
31
|
+
function num(x, min = -MAX_MINUTE, max = MAX_MINUTE) {
|
|
32
|
+
if (typeof x !== 'number' || !Number.isFinite(x) || x < min || x > max)
|
|
33
|
+
fail('invalid timestamp/number');
|
|
34
|
+
}
|
|
35
|
+
function list(x) {
|
|
36
|
+
if (!Array.isArray(x) || x.length > 128)
|
|
37
|
+
fail('invalid bounded list');
|
|
38
|
+
}
|
|
39
|
+
function strings(x) {
|
|
40
|
+
list(x);
|
|
41
|
+
for (const v of x)
|
|
42
|
+
text(v);
|
|
43
|
+
}
|
|
44
|
+
function same(a, b) {
|
|
45
|
+
const canonical = (x) => JSON.stringify(x, (_k, v) => v && typeof v === 'object' && !Array.isArray(v)
|
|
46
|
+
? Object.fromEntries(Object.entries(v).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))
|
|
47
|
+
: v);
|
|
48
|
+
return canonical(a) === canonical(b);
|
|
49
|
+
}
|
|
50
|
+
function choice(value, allowed) {
|
|
51
|
+
return typeof value === 'string' && allowed.includes(value);
|
|
52
|
+
}
|
|
53
|
+
const standings = ['obligatory', 'recommended', 'permitted', 'disliked', 'forbidden'];
|
|
54
|
+
const kinds = ['promise', 'duty', 'appointment', 'worship', 'job', 'abstain'];
|
|
55
|
+
const reasons = ['illness', 'travel', 'necessity', 'sleep', 'unconscious', 'missed', 'broken'];
|
|
56
|
+
function sources(x) {
|
|
57
|
+
list(x);
|
|
58
|
+
if (!x.length)
|
|
59
|
+
fail('norm provenance required');
|
|
60
|
+
for (const v of x) {
|
|
61
|
+
keys(v, ['kind', 'ref']);
|
|
62
|
+
text(v.ref);
|
|
63
|
+
if (!choice(v.kind, ['revelation', 'hadith', 'interpretation', 'empirical', 'assumption']))
|
|
64
|
+
fail('invalid provenance');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function catalog(x) {
|
|
68
|
+
list(x);
|
|
69
|
+
const ids = new Set();
|
|
70
|
+
for (const v of x) {
|
|
71
|
+
keys(v, ['id', 'label', 'standing', 'sources', 'exemptions'], ['id', 'label', 'standing', 'sources']);
|
|
72
|
+
text(v.id);
|
|
73
|
+
text(v.label);
|
|
74
|
+
sources(v.sources);
|
|
75
|
+
if (ids.has(v.id) || !choice(v.standing, standings))
|
|
76
|
+
fail('invalid norm catalog');
|
|
77
|
+
ids.add(v.id);
|
|
78
|
+
if (v.exemptions !== undefined) {
|
|
79
|
+
list(v.exemptions);
|
|
80
|
+
const conditions = new Set();
|
|
81
|
+
for (const e of v.exemptions) {
|
|
82
|
+
keys(e, ['when', 'makeUp', 'sources']);
|
|
83
|
+
if (!choice(e.when, ['illness', 'travel']) || typeof e.makeUp !== 'boolean' || conditions.has(e.when))
|
|
84
|
+
fail('invalid exemption');
|
|
85
|
+
conditions.add(e.when);
|
|
86
|
+
sources(e.sources);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function calendarCommitment(x) {
|
|
92
|
+
keys(x, [
|
|
93
|
+
'id',
|
|
94
|
+
'kind',
|
|
95
|
+
'toId',
|
|
96
|
+
'actions',
|
|
97
|
+
'targetId',
|
|
98
|
+
'from',
|
|
99
|
+
'until',
|
|
100
|
+
'normId',
|
|
101
|
+
'importance',
|
|
102
|
+
'status',
|
|
103
|
+
'recurEvery',
|
|
104
|
+
'label',
|
|
105
|
+
'violatedBy',
|
|
106
|
+
'firm',
|
|
107
|
+
'exemptWhen',
|
|
108
|
+
'exempt',
|
|
109
|
+
'makeUpOf',
|
|
110
|
+
'chain',
|
|
111
|
+
'recurUntil',
|
|
112
|
+
], ['id', 'kind', 'actions', 'from', 'until', 'importance', 'status']);
|
|
113
|
+
for (const field of ['id', 'toId', 'targetId', 'normId', 'label', 'makeUpOf', 'chain'])
|
|
114
|
+
if (x[field] !== undefined)
|
|
115
|
+
text(x[field]);
|
|
116
|
+
if (!choice(x.kind, kinds) || !choice(x.status, ['pending', 'kept', 'broken', 'released']))
|
|
117
|
+
fail('invalid commitment');
|
|
118
|
+
strings(x.actions);
|
|
119
|
+
num(x.from);
|
|
120
|
+
num(x.until);
|
|
121
|
+
num(x.importance, 0, 1);
|
|
122
|
+
if (x.until < x.from || (x.kind !== 'abstain' && !x.actions.length))
|
|
123
|
+
fail('invalid commitment window/actions');
|
|
124
|
+
if (x.violatedBy !== undefined)
|
|
125
|
+
strings(x.violatedBy);
|
|
126
|
+
if (x.recurEvery !== undefined)
|
|
127
|
+
num(x.recurEvery, 1);
|
|
128
|
+
if (x.recurUntil !== undefined)
|
|
129
|
+
num(x.recurUntil);
|
|
130
|
+
if (x.firm !== undefined && x.firm !== true)
|
|
131
|
+
fail('invalid firm');
|
|
132
|
+
if (x.exemptWhen !== undefined) {
|
|
133
|
+
strings(x.exemptWhen);
|
|
134
|
+
if (x.exemptWhen.some((v) => !['illness', 'travel'].includes(v)))
|
|
135
|
+
fail('invalid exemption condition');
|
|
136
|
+
}
|
|
137
|
+
if (x.exempt !== undefined) {
|
|
138
|
+
keys(x.exempt, ['reason', 'at']);
|
|
139
|
+
num(x.exempt.at);
|
|
140
|
+
if (!choice(x.exempt.reason, ['illness', 'travel', 'necessity', 'sleep', 'unconscious']))
|
|
141
|
+
fail('invalid exemption reason');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function owed(x) {
|
|
145
|
+
keys(x, ['ofId', 'normId', 'kind', 'violatedBy', 'actions', 'reason', 'lapseSince', 'at', 'label', 'scheduledAs'], ['ofId', 'kind', 'actions', 'reason', 'at']);
|
|
146
|
+
for (const field of ['ofId', 'normId', 'label', 'scheduledAs'])
|
|
147
|
+
if (x[field] !== undefined)
|
|
148
|
+
text(x[field]);
|
|
149
|
+
if (!choice(x.kind, kinds) || !choice(x.reason, reasons))
|
|
150
|
+
fail('invalid owed record');
|
|
151
|
+
strings(x.actions);
|
|
152
|
+
num(x.at);
|
|
153
|
+
if (x.violatedBy !== undefined)
|
|
154
|
+
strings(x.violatedBy);
|
|
155
|
+
if (x.lapseSince !== undefined)
|
|
156
|
+
num(x.lapseSince);
|
|
157
|
+
}
|
|
158
|
+
function boundedJson(input) {
|
|
159
|
+
const pending = [{ value: input, depth: 0 }];
|
|
160
|
+
const seen = new Set();
|
|
161
|
+
let nodes = 0;
|
|
162
|
+
while (pending.length) {
|
|
163
|
+
const item = pending.pop();
|
|
164
|
+
if (!item)
|
|
165
|
+
break;
|
|
166
|
+
if (++nodes > 16384 || item.depth > 16)
|
|
167
|
+
fail('JSON bounds exceeded');
|
|
168
|
+
const { value, depth } = item;
|
|
169
|
+
if (value === null || typeof value === 'boolean')
|
|
170
|
+
continue;
|
|
171
|
+
if (typeof value === 'string') {
|
|
172
|
+
if (value.length > 1024)
|
|
173
|
+
fail('text bounds exceeded');
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (typeof value === 'number') {
|
|
177
|
+
num(value);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (typeof value !== 'object' || seen.has(value))
|
|
181
|
+
fail('non-JSON or cyclic input');
|
|
182
|
+
seen.add(value);
|
|
183
|
+
if (!Array.isArray(value) &&
|
|
184
|
+
Object.getPrototypeOf(value) !== Object.prototype &&
|
|
185
|
+
Object.getPrototypeOf(value) !== null)
|
|
186
|
+
fail('non-JSON object');
|
|
187
|
+
for (const [key, child] of Object.entries(value)) {
|
|
188
|
+
if (['__proto__', 'constructor', 'prototype'].includes(key))
|
|
189
|
+
fail('unsafe reference');
|
|
190
|
+
pending.push({ value: child, depth: depth + 1 });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/** Validate portable calendar data strictly; no restore defaults or debt settlement. */
|
|
195
|
+
export function validateCalendarPersona(input) {
|
|
196
|
+
boundedJson(input);
|
|
197
|
+
keys(input, ['format', 'person', 'calendar']);
|
|
198
|
+
if (input.format !== 'hf-persona-calendar/1')
|
|
199
|
+
fail('unsupported format');
|
|
200
|
+
const person = validatePersonaProjection(input.person);
|
|
201
|
+
if (person.commitments.length)
|
|
202
|
+
fail('calendar owns portable commitments');
|
|
203
|
+
const c = input.calendar;
|
|
204
|
+
keys(c, ['offset', 'commitments', 'owed', 'lapse', 'heldNorms', 'norms']);
|
|
205
|
+
num(c.offset);
|
|
206
|
+
list(c.commitments);
|
|
207
|
+
for (const v of c.commitments)
|
|
208
|
+
calendarCommitment(v);
|
|
209
|
+
list(c.owed);
|
|
210
|
+
for (const v of c.owed)
|
|
211
|
+
owed(v);
|
|
212
|
+
const commitments = c.commitments;
|
|
213
|
+
const debts = c.owed;
|
|
214
|
+
if (new Set(commitments.map((v) => v.id)).size !== c.commitments.length ||
|
|
215
|
+
new Set(debts.map((v) => v.ofId)).size !== c.owed.length)
|
|
216
|
+
fail('duplicate obligation id');
|
|
217
|
+
if (c.lapse !== null) {
|
|
218
|
+
keys(c.lapse, ['since', 'missed']);
|
|
219
|
+
num(c.lapse.since);
|
|
220
|
+
num(c.lapse.missed, 0);
|
|
221
|
+
if (!Number.isInteger(c.lapse.missed))
|
|
222
|
+
fail('invalid lapse');
|
|
223
|
+
}
|
|
224
|
+
catalog(c.norms);
|
|
225
|
+
list(c.heldNorms);
|
|
226
|
+
for (const v of c.heldNorms) {
|
|
227
|
+
keys(v, ['normId', 'standing', 'conviction']);
|
|
228
|
+
text(v.normId);
|
|
229
|
+
num(v.conviction, 0, 1);
|
|
230
|
+
if (!choice(v.standing, standings))
|
|
231
|
+
fail('invalid held understanding');
|
|
232
|
+
}
|
|
233
|
+
const heldNorms = c.heldNorms;
|
|
234
|
+
if (new Set(heldNorms.map((v) => v.normId)).size !== c.heldNorms.length)
|
|
235
|
+
fail('duplicate held norm');
|
|
236
|
+
const normIds = new Set(c.norms.map((n) => n.id));
|
|
237
|
+
for (const record of [...commitments, ...debts, ...heldNorms])
|
|
238
|
+
if (record.normId !== undefined && !normIds.has(record.normId))
|
|
239
|
+
fail('missing norm definition');
|
|
240
|
+
const arrival = person.source.now + c.offset;
|
|
241
|
+
num(arrival);
|
|
242
|
+
for (const debt of debts) {
|
|
243
|
+
const scheduled = commitments.find((v) => v.id === debt.scheduledAs);
|
|
244
|
+
// Pruned source history stays absent and unresolved; never invent a paid receipt.
|
|
245
|
+
if (scheduled && scheduled.makeUpOf !== debt.ofId)
|
|
246
|
+
fail('conflicting scheduled make-up reference');
|
|
247
|
+
if (debt.at > arrival || (debt.lapseSince !== undefined && debt.lapseSince > arrival))
|
|
248
|
+
fail('future owed evidence');
|
|
249
|
+
}
|
|
250
|
+
if (c.lapse !== null && c.lapse.since > arrival)
|
|
251
|
+
fail('future lapse evidence');
|
|
252
|
+
for (const duty of commitments)
|
|
253
|
+
if (duty.exempt && duty.exempt.at > arrival)
|
|
254
|
+
fail('future exemption evidence');
|
|
255
|
+
// Match the engine's owning-slice bounds exactly. A rejected import cannot be silently truncated.
|
|
256
|
+
const probe = createPerson({
|
|
257
|
+
id: 'calendar-validation',
|
|
258
|
+
name: 'validation',
|
|
259
|
+
seed: 0,
|
|
260
|
+
bornAt: 0,
|
|
261
|
+
sex: 'female',
|
|
262
|
+
});
|
|
263
|
+
probe.agenda.commitments = structuredClone(c.commitments);
|
|
264
|
+
probe.agenda.owed = structuredClone(c.owed);
|
|
265
|
+
if (c.lapse !== null)
|
|
266
|
+
probe.agenda.lapse = structuredClone(c.lapse);
|
|
267
|
+
probe.conscience.norms = structuredClone(c.heldNorms);
|
|
268
|
+
const checked = restore(probe);
|
|
269
|
+
if (!same(probe.agenda, checked.agenda) || !same(probe.conscience.norms, checked.conscience.norms))
|
|
270
|
+
fail('calendar exceeds supported engine bounds');
|
|
271
|
+
return { format: 'hf-persona-calendar/1', person, calendar: structuredClone(c) };
|
|
272
|
+
}
|
|
273
|
+
/** Source declares a complete semantic ID map, a clock offset and source-backed norm catalog. */
|
|
274
|
+
export function projectCalendarPersona(source, mapping) {
|
|
275
|
+
const p = restore(snapshot(source));
|
|
276
|
+
const m = mapping.calendar;
|
|
277
|
+
keys(m, ['version', 'offset', 'references', 'sourceNorms']);
|
|
278
|
+
if (m.version !== 'hf-persona-calendar-map/1')
|
|
279
|
+
fail('unsupported calendar mapping');
|
|
280
|
+
num(m.offset);
|
|
281
|
+
keys(m.references, ['commitments', 'chains', 'actions', 'entities', 'norms']);
|
|
282
|
+
for (const record of Object.values(m.references)) {
|
|
283
|
+
object(record);
|
|
284
|
+
if (Object.keys(record).length > 512)
|
|
285
|
+
fail('too many references');
|
|
286
|
+
for (const [key, value] of Object.entries(record)) {
|
|
287
|
+
text(key);
|
|
288
|
+
text(value);
|
|
289
|
+
}
|
|
290
|
+
if (new Set(Object.values(record)).size !== Object.values(record).length)
|
|
291
|
+
fail('duplicate mapped reference');
|
|
292
|
+
}
|
|
293
|
+
catalog(m.sourceNorms);
|
|
294
|
+
const ref = (kind, id) => {
|
|
295
|
+
const value = Object.hasOwn(m.references[kind], id) ? m.references[kind][id] : undefined;
|
|
296
|
+
if (value === undefined)
|
|
297
|
+
fail(`unmapped ${kind} reference ${id}`);
|
|
298
|
+
return value;
|
|
299
|
+
};
|
|
300
|
+
const shift = (at) => {
|
|
301
|
+
const value = at + m.offset;
|
|
302
|
+
num(value);
|
|
303
|
+
return value;
|
|
304
|
+
};
|
|
305
|
+
const debts = p.agenda.owed ?? [];
|
|
306
|
+
const selected = p.agenda.commitments.filter((c) => isContinuingCommitment(p.agenda, c) || debts.some((d) => d.ofId === c.id || d.scheduledAs === c.id));
|
|
307
|
+
const commitments = selected.map((old) => {
|
|
308
|
+
const c = structuredClone(old);
|
|
309
|
+
c.id = ref('commitments', old.id);
|
|
310
|
+
c.actions = old.actions.map((a) => ref('actions', a));
|
|
311
|
+
c.from = shift(old.from);
|
|
312
|
+
c.until = shift(old.until);
|
|
313
|
+
for (const [field, kind] of [
|
|
314
|
+
['toId', 'entities'],
|
|
315
|
+
['targetId', 'entities'],
|
|
316
|
+
['normId', 'norms'],
|
|
317
|
+
['chain', 'chains'],
|
|
318
|
+
['makeUpOf', 'commitments'],
|
|
319
|
+
]) {
|
|
320
|
+
const value = old[field];
|
|
321
|
+
if (value !== undefined)
|
|
322
|
+
c[field] = ref(kind, value);
|
|
323
|
+
}
|
|
324
|
+
if (old.violatedBy !== undefined)
|
|
325
|
+
c.violatedBy = old.violatedBy.map((a) => ref('actions', a));
|
|
326
|
+
if (old.recurUntil !== undefined)
|
|
327
|
+
c.recurUntil = shift(old.recurUntil);
|
|
328
|
+
if (c.exempt !== undefined)
|
|
329
|
+
c.exempt.at = shift(c.exempt.at);
|
|
330
|
+
return c;
|
|
331
|
+
});
|
|
332
|
+
const mappedOwed = debts.map((old) => {
|
|
333
|
+
const d = structuredClone(old);
|
|
334
|
+
d.ofId = ref('commitments', old.ofId);
|
|
335
|
+
d.at = shift(old.at);
|
|
336
|
+
d.actions = old.actions.map((a) => ref('actions', a));
|
|
337
|
+
if (old.normId !== undefined)
|
|
338
|
+
d.normId = ref('norms', old.normId);
|
|
339
|
+
if (old.scheduledAs !== undefined)
|
|
340
|
+
d.scheduledAs = ref('commitments', old.scheduledAs);
|
|
341
|
+
if (old.violatedBy !== undefined)
|
|
342
|
+
d.violatedBy = old.violatedBy.map((a) => ref('actions', a));
|
|
343
|
+
if (old.lapseSince !== undefined)
|
|
344
|
+
d.lapseSince = shift(old.lapseSince);
|
|
345
|
+
return d;
|
|
346
|
+
});
|
|
347
|
+
const heldNorms = p.conscience.norms.map((n) => ({ ...n, normId: ref('norms', n.normId) }));
|
|
348
|
+
const norms = m.sourceNorms.map((n) => ({ ...structuredClone(n), id: ref('norms', n.id) }));
|
|
349
|
+
const stripped = snapshot(p);
|
|
350
|
+
stripped.agenda.commitments = [];
|
|
351
|
+
delete stripped.agenda.owed;
|
|
352
|
+
delete stripped.agenda.lapse;
|
|
353
|
+
const person = projectPersona(stripped, {
|
|
354
|
+
version: mapping.version,
|
|
355
|
+
skills: mapping.skills,
|
|
356
|
+
...(mapping.episodes === undefined ? {} : { episodes: mapping.episodes }),
|
|
357
|
+
});
|
|
358
|
+
person.limitations = person.limitations.map((line) => line === 'source habits/methods/beliefs/social/conscience omitted; target authored state'
|
|
359
|
+
? 'source habits/methods/beliefs/social and conscience breaches omitted; target authored state; held norms transported'
|
|
360
|
+
: line);
|
|
361
|
+
for (const debt of mappedOwed) {
|
|
362
|
+
if (debt.scheduledAs !== undefined && !commitments.some((c) => c.id === debt.scheduledAs))
|
|
363
|
+
person.limitations.push(`owed:${debt.ofId}:scheduled history absent; payment unproven`);
|
|
364
|
+
}
|
|
365
|
+
return validateCalendarPersona({
|
|
366
|
+
format: 'hf-persona-calendar/1',
|
|
367
|
+
person,
|
|
368
|
+
calendar: {
|
|
369
|
+
offset: m.offset,
|
|
370
|
+
commitments,
|
|
371
|
+
owed: mappedOwed,
|
|
372
|
+
lapse: p.agenda.lapse ? { ...p.agenda.lapse, since: shift(p.agenda.lapse.since) } : null,
|
|
373
|
+
heldNorms,
|
|
374
|
+
norms,
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
/** Receiver declares identical norm meanings and a clean calendar at the exact declared opening clock. */
|
|
379
|
+
export function admitCalendarPersona(input, target, policy) {
|
|
380
|
+
const projection = validateCalendarPersona(input);
|
|
381
|
+
catalog(policy.targetNorms);
|
|
382
|
+
const c = projection.calendar;
|
|
383
|
+
if (target.now - projection.person.source.now !== c.offset)
|
|
384
|
+
fail('calendar clock offset conflict');
|
|
385
|
+
const ordered = (ns) => [...ns].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
386
|
+
if (!same(ordered(c.norms), ordered(policy.targetNorms)))
|
|
387
|
+
fail('norm semantic compatibility conflict');
|
|
388
|
+
if (target.agenda.commitments.length || target.agenda.owed?.length || target.agenda.lapse)
|
|
389
|
+
fail('target calendar must be empty');
|
|
390
|
+
const result = admitPersona(projection.person, target, policy);
|
|
391
|
+
const p = result.person;
|
|
392
|
+
p.agenda.commitments = c.commitments;
|
|
393
|
+
if (c.owed.length)
|
|
394
|
+
p.agenda.owed = c.owed;
|
|
395
|
+
if (c.lapse !== null)
|
|
396
|
+
p.agenda.lapse = c.lapse;
|
|
397
|
+
p.conscience.norms = c.heldNorms;
|
|
398
|
+
p.agenda.nextId = Math.max(p.agenda.nextId, p.agenda.commitments.length + 1);
|
|
399
|
+
// Agenda-generated ids are c<N>; reserve imported ids before normal host scheduling resumes.
|
|
400
|
+
for (const record of [
|
|
401
|
+
...c.commitments.map((x) => x.id),
|
|
402
|
+
...c.owed.flatMap((x) => [x.ofId, ...(x.scheduledAs === undefined ? [] : [x.scheduledAs])]),
|
|
403
|
+
]) {
|
|
404
|
+
if (/^c\d+$/.test(record))
|
|
405
|
+
p.agenda.nextId = Math.max(p.agenda.nextId, Number(record.slice(1)) + 1);
|
|
406
|
+
}
|
|
407
|
+
const checked = restore(p);
|
|
408
|
+
if (!same(p, checked))
|
|
409
|
+
fail('admitted calendar exceeds supported engine bounds');
|
|
410
|
+
return { ...result, person: checked };
|
|
411
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Commitment, Episode, Person, Skill } from './types.ts';
|
|
2
|
+
export interface PersonaEpisodeMap {
|
|
3
|
+
id: string;
|
|
4
|
+
action?: string;
|
|
5
|
+
actorId?: string;
|
|
6
|
+
targetId?: string;
|
|
7
|
+
placeId?: string;
|
|
8
|
+
voiceId?: string;
|
|
9
|
+
/** Tags are semantic keys too; omitted means no portable tags. */
|
|
10
|
+
tags?: string[];
|
|
11
|
+
}
|
|
12
|
+
export interface PersonaCommitmentMap {
|
|
13
|
+
id: string;
|
|
14
|
+
actions: string[];
|
|
15
|
+
toId?: string;
|
|
16
|
+
targetId?: string;
|
|
17
|
+
normId?: string;
|
|
18
|
+
chain?: string;
|
|
19
|
+
/** Explicit target-calendar window. Duration and recurrence must remain identical. */
|
|
20
|
+
from: number;
|
|
21
|
+
until: number;
|
|
22
|
+
}
|
|
23
|
+
export interface PersonaProjection {
|
|
24
|
+
version: string;
|
|
25
|
+
skills: Record<string, string>;
|
|
26
|
+
episodes?: Record<string, PersonaEpisodeMap>;
|
|
27
|
+
/** Every pending or continuing recurring obligation requires a named binding. */
|
|
28
|
+
commitments?: Record<string, PersonaCommitmentMap>;
|
|
29
|
+
}
|
|
30
|
+
export interface PersonaProjectionSnapshot {
|
|
31
|
+
format: 'hf-persona-state/1';
|
|
32
|
+
mappingVersion: string;
|
|
33
|
+
source: {
|
|
34
|
+
schema: Person['schema'];
|
|
35
|
+
engine: string;
|
|
36
|
+
id: string;
|
|
37
|
+
now: number;
|
|
38
|
+
};
|
|
39
|
+
name: string;
|
|
40
|
+
life: Person['life'];
|
|
41
|
+
rng: Person['rng'];
|
|
42
|
+
traits: Person['traits'];
|
|
43
|
+
values: Person['values'];
|
|
44
|
+
health: {
|
|
45
|
+
health: number;
|
|
46
|
+
fitness: number;
|
|
47
|
+
circadianPeak: number;
|
|
48
|
+
};
|
|
49
|
+
family: Person['family'] | null;
|
|
50
|
+
bonds: Person['bonds'] | null;
|
|
51
|
+
skills: Record<string, Skill>;
|
|
52
|
+
skillRetention: Person['skillRetention'] | null;
|
|
53
|
+
episodes: Episode[];
|
|
54
|
+
/** Already bound to the target calendar by the explicitly versioned host mapping. */
|
|
55
|
+
commitments: Commitment[];
|
|
56
|
+
limitations: string[];
|
|
57
|
+
}
|
|
58
|
+
export interface PersonaAdmission {
|
|
59
|
+
mappingVersion: string;
|
|
60
|
+
sourceId: string;
|
|
61
|
+
targetId: string;
|
|
62
|
+
}
|
|
63
|
+
export interface PersonaAdmissionResult {
|
|
64
|
+
person: Person;
|
|
65
|
+
report: {
|
|
66
|
+
mappingVersion: string;
|
|
67
|
+
offset: number;
|
|
68
|
+
limitations: string[];
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** Validates untrusted portable data without restoring defaults or accepting extra live fields. */
|
|
72
|
+
export declare function validatePersonaProjection(input: unknown): PersonaProjectionSnapshot;
|
|
73
|
+
/** Hosts must migrate supported old saves first. No random draws or mutation occur here. */
|
|
74
|
+
export declare function projectPersona(source: Person, mapping: PersonaProjection): PersonaProjectionSnapshot;
|
|
75
|
+
/** Return a detached target opening; a later shelf change cannot affect it. Host owns cast/branch/death authority. */
|
|
76
|
+
export declare function admitPersona(input: unknown, target: Person, policy: PersonaAdmission): PersonaAdmissionResult;
|