@adam0white/human-framework 2.15.0 → 2.17.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 +36 -0
- package/dist/agenda/agenda.d.ts +2 -0
- package/dist/agenda/agenda.js +11 -1
- package/dist/conversation/conversation.d.ts +13 -4
- package/dist/conversation/conversation.js +24 -2
- package/dist/index.d.ts +4 -3
- package/dist/index.js +4 -3
- package/dist/migrate.js +5 -0
- package/dist/narrate/lexicon.js +23 -0
- package/dist/narrate/narrate.d.ts +7 -1
- package/dist/narrate/narrate.js +17 -1
- package/dist/person.d.ts +6 -1
- package/dist/person.js +13 -8
- package/dist/persona-calendar.d.ts +39 -0
- package/dist/persona-calendar.js +411 -0
- package/dist/persona.d.ts +1 -1
- package/dist/persona.js +5 -2
- package/dist/projects/projects.d.ts +8 -2
- package/dist/projects/projects.js +142 -2
- package/dist/promptings/promptings.d.ts +3 -1
- package/dist/promptings/promptings.js +13 -1
- package/dist/prospection/prospection.js +15 -2
- package/dist/review/review.d.ts +4 -1
- package/dist/review/review.js +12 -1
- package/dist/types.d.ts +35 -1
- package/dist/types.js +1 -1
- 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
|
+
}
|
package/dist/persona.d.ts
CHANGED
package/dist/persona.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* identity, age, family and health compatibility before returning a new Person. Neither input is mutated.
|
|
5
5
|
* This is not a world migration, identity registry, transport, hash, authentication or general calendar mapper.
|
|
6
6
|
* v1 admits zero-duration travel and healthy compatible bodies only; family/bonds remain host-authored and must
|
|
7
|
-
* match source provenance.
|
|
7
|
+
* match source provenance. Portable v1 accepts its published 2.14.1 stamp and the current engine;
|
|
8
|
+
* the 2.14.1 to 2.17.0 migration changes no portable field semantics. 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
|
*/
|
|
9
10
|
import { hasUnresolvedMakeUps, sameChain } from './agenda/agenda.js';
|
|
10
11
|
import { createPerson } from './person.js';
|
|
@@ -185,7 +186,9 @@ export function validatePersonaProjection(input) {
|
|
|
185
186
|
text(input.mappingVersion);
|
|
186
187
|
text(input.name);
|
|
187
188
|
keys(input.source, ['schema', 'engine', 'id', 'now']);
|
|
188
|
-
|
|
189
|
+
// Both published passport releases used 2.14.1; the next engine upgrade is stamp-only here.
|
|
190
|
+
const compatibleEngine = input.source.engine === ENGINE_VERSION || input.source.engine === '2.14.1';
|
|
191
|
+
if (input.source.schema !== PERSON_SCHEMA || !compatibleEngine)
|
|
189
192
|
fail('unsupported source schema/engine; migrate the source before projection');
|
|
190
193
|
text(input.source.id);
|
|
191
194
|
num(input.source.now);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Minute, Person, ProjectsSpec, ProjectsState, Prospect } from '../types.ts';
|
|
1
|
+
import type { Goal, Minute, Person, ProjectsSpec, ProjectsState, Prospect } from '../types.ts';
|
|
2
2
|
export declare const PROJECTS_DEFAULTS: {
|
|
3
3
|
readonly maxProspects: 8;
|
|
4
4
|
readonly maxHistory: 8;
|
|
@@ -13,10 +13,16 @@ export declare const PROJECTS_DEFAULTS: {
|
|
|
13
13
|
};
|
|
14
14
|
/** Enable or reconfigure without discarding retained prospects and history. */
|
|
15
15
|
export declare function enableProjects(p: Person, spec?: ProjectsSpec): ProjectsState;
|
|
16
|
+
/** Turn an active held project into a host task. Undefined means there is no active held project.
|
|
17
|
+
* Reuses identity, adoption time, progress, peak and progress clock; never revives an abandoned project.
|
|
18
|
+
* Retains its temporal horizon unless the task supplies a valid replacement.
|
|
19
|
+
* The caller supplies task meaning and deadlines. No second goal or fresh progress is created.
|
|
20
|
+
*/
|
|
21
|
+
export declare function adoptProjectTask(p: Person, prospectId: string, task: Omit<Goal, 'id' | 'status' | 'progress' | 'adoptedAt' | 'project' | 'lastAdvancedAt'>, now?: Minute): Goal | undefined;
|
|
16
22
|
/** Read-only, deterministic appeal. Thresholds close the relaxed gate before the bounded bonus. */
|
|
17
23
|
export declare function projectAppeal(p: Person, prospect: Prospect, now?: Minute): number;
|
|
18
24
|
/** Only the composite calls this, after attention. Newest event minute wins; equal-minute input order wins. */
|
|
19
|
-
export declare function perceiveProspect(p: Person, input: Prospect, at: Minute): void;
|
|
25
|
+
export declare function perceiveProspect(p: Person, input: Prospect, at: Minute, attributeOwnAct?: boolean): void;
|
|
20
26
|
/** Called on each tick segment, including one-minute begin/tick/finish host loops. No RNG. */
|
|
21
27
|
export declare function considerProjects(p: Person, now: Minute): void;
|
|
22
28
|
/** Conservative restore: bounded fields, finite past timestamps, and no ambiguous duplicate ids. */
|
|
@@ -9,7 +9,12 @@
|
|
|
9
9
|
* goals. All kinds serve competence and leisure: making is chosen as a free-time pursuit, not duty.
|
|
10
10
|
* Behaviour 2 credits only new perceived highs; event-credited projects cannot observe reversals and stay unchanged.
|
|
11
11
|
* Restore recovers one active project, keeping the newest (id breaks ties) and abandoning extras.
|
|
12
|
-
*
|
|
12
|
+
* Explicit protectProgress blocks goal credit for host-declared reductions and exact contextual acts
|
|
13
|
+
* followed within five minutes by an attended drop after one's own completion. This is a bounded
|
|
14
|
+
* causal assumption, not proof of causation. Only direct saw/outcome readings without another actor
|
|
15
|
+
* can teach that link. It learns no effects from unattended world state or testimony.
|
|
16
|
+
* adoptProjectTask reuses an active project goal, preserving net credit and pursuit identity.
|
|
17
|
+
* No invented prospects, implicit host task interpretation, world search, persistence, shielding, faith lens,
|
|
13
18
|
* LLM or RNG. Without the slice, perception and minute bookkeeping write nothing.
|
|
14
19
|
*/
|
|
15
20
|
import { emotionLevel } from '../affect/index.js';
|
|
@@ -44,6 +49,8 @@ function cleanSpec(input) {
|
|
|
44
49
|
const out = {};
|
|
45
50
|
if (!isObj(input))
|
|
46
51
|
return out;
|
|
52
|
+
if (typeof input.protectProgress === 'boolean')
|
|
53
|
+
out.protectProgress = input.protectProgress;
|
|
47
54
|
for (const key of KEYS) {
|
|
48
55
|
const x = input[key];
|
|
49
56
|
const unit = key === 'calmUrgency' || key === 'fearBelow' || key === 'adoptAppeal';
|
|
@@ -90,6 +97,70 @@ export function enableProjects(p, spec = {}) {
|
|
|
90
97
|
p.projects = state;
|
|
91
98
|
return state;
|
|
92
99
|
}
|
|
100
|
+
/** Whether this offer is understood to undo this prospect. Read-only; exact context, no action-only generalisation. @internal */
|
|
101
|
+
export function diminishesProject(p, aff, prospectId) {
|
|
102
|
+
const s = p.projects;
|
|
103
|
+
return (s?.spec?.protectProgress === true &&
|
|
104
|
+
((aff.diminishes?.includes(prospectId) ?? false) ||
|
|
105
|
+
(s.setbacks?.some((x) => x.prospectId === prospectId &&
|
|
106
|
+
x.action === aff.action &&
|
|
107
|
+
x.targetId === aff.targetId &&
|
|
108
|
+
x.placeId === aff.placeId) ??
|
|
109
|
+
false)));
|
|
110
|
+
}
|
|
111
|
+
/** Own completed acts alone can explain the next attended drop. Failed/interrupted acts clear attribution. @internal */
|
|
112
|
+
export function noteProjectAct(p, aff, completed, at) {
|
|
113
|
+
const s = p.projects;
|
|
114
|
+
if (!s?.spec?.protectProgress)
|
|
115
|
+
return;
|
|
116
|
+
delete s.pendingAct;
|
|
117
|
+
if (!completed ||
|
|
118
|
+
!str(aff.action) ||
|
|
119
|
+
(aff.targetId !== undefined && !str(aff.targetId)) ||
|
|
120
|
+
(aff.placeId !== undefined && !str(aff.placeId)))
|
|
121
|
+
return;
|
|
122
|
+
const readings = s.prospects
|
|
123
|
+
.filter((x) => x.progress !== undefined &&
|
|
124
|
+
p.agenda.goals.some((g) => g.status === 'active' && g.project?.prospectId === x.id))
|
|
125
|
+
.map((x) => ({ prospectId: x.id, progress: x.progress }));
|
|
126
|
+
if (!readings.length)
|
|
127
|
+
return;
|
|
128
|
+
s.pendingAct = {
|
|
129
|
+
action: aff.action,
|
|
130
|
+
at,
|
|
131
|
+
readings,
|
|
132
|
+
...(aff.targetId !== undefined ? { targetId: aff.targetId } : {}),
|
|
133
|
+
...(aff.placeId !== undefined ? { placeId: aff.placeId } : {}),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/** Turn an active held project into a host task. Undefined means there is no active held project.
|
|
137
|
+
* Reuses identity, adoption time, progress, peak and progress clock; never revives an abandoned project.
|
|
138
|
+
* Retains its temporal horizon unless the task supplies a valid replacement.
|
|
139
|
+
* The caller supplies task meaning and deadlines. No second goal or fresh progress is created.
|
|
140
|
+
*/
|
|
141
|
+
export function adoptProjectTask(p, prospectId, task, now = p.now) {
|
|
142
|
+
if (!p.projects || !minute(now, p.now))
|
|
143
|
+
return undefined;
|
|
144
|
+
const held = p.agenda.goals.find((g) => g.status === 'active' && g.project?.prospectId === prospectId);
|
|
145
|
+
if (!held)
|
|
146
|
+
return undefined;
|
|
147
|
+
// Use agenda's copying and sanitization, then retain the existing goal rather than allocating a new id.
|
|
148
|
+
const nextId = p.agenda.nextId;
|
|
149
|
+
const staged = adoptGoal(p, task, now);
|
|
150
|
+
p.agenda.goals.pop();
|
|
151
|
+
p.agenda.nextId = nextId;
|
|
152
|
+
held.label = staged.label;
|
|
153
|
+
held.serves = staged.serves;
|
|
154
|
+
held.advancedBy = staged.advancedBy;
|
|
155
|
+
held.importance = staged.importance;
|
|
156
|
+
held.baseImportance = staged.baseImportance;
|
|
157
|
+
delete held.deadline;
|
|
158
|
+
if (staged.deadline !== undefined)
|
|
159
|
+
held.deadline = staged.deadline;
|
|
160
|
+
if (staged.temporal !== undefined)
|
|
161
|
+
held.temporal = staged.temporal;
|
|
162
|
+
return held;
|
|
163
|
+
}
|
|
93
164
|
function relaxed(p) {
|
|
94
165
|
const needs = readPerson(p).needs;
|
|
95
166
|
return {
|
|
@@ -166,7 +237,7 @@ export function noteProjectStatus(p, now) {
|
|
|
166
237
|
recordProjectStatus(p, g, now);
|
|
167
238
|
}
|
|
168
239
|
/** Only the composite calls this, after attention. Newest event minute wins; equal-minute input order wins. */
|
|
169
|
-
export function perceiveProspect(p, input, at) {
|
|
240
|
+
export function perceiveProspect(p, input, at, attributeOwnAct = false) {
|
|
170
241
|
const s = p.projects;
|
|
171
242
|
if (!s || !minute(at, p.now))
|
|
172
243
|
return;
|
|
@@ -178,6 +249,32 @@ export function perceiveProspect(p, input, at) {
|
|
|
178
249
|
return;
|
|
179
250
|
if (p.now - at > setting(s, 'forgetMinutes'))
|
|
180
251
|
return;
|
|
252
|
+
const act = s.pendingAct;
|
|
253
|
+
if (s.spec?.protectProgress && act && at >= act.at) {
|
|
254
|
+
const before = act.readings.find((x) => x.prospectId === prospect.id);
|
|
255
|
+
if (before && prospect.progress !== undefined) {
|
|
256
|
+
if (attributeOwnAct && at - act.at <= 5 && prospect.progress < before.progress) {
|
|
257
|
+
const learned = {
|
|
258
|
+
prospectId: prospect.id,
|
|
259
|
+
action: act.action,
|
|
260
|
+
...(act.targetId !== undefined ? { targetId: act.targetId } : {}),
|
|
261
|
+
...(act.placeId !== undefined ? { placeId: act.placeId } : {}),
|
|
262
|
+
};
|
|
263
|
+
const list = s.setbacks ?? [];
|
|
264
|
+
if (!list.some((x) => x.prospectId === learned.prospectId &&
|
|
265
|
+
x.action === learned.action &&
|
|
266
|
+
x.targetId === learned.targetId &&
|
|
267
|
+
x.placeId === learned.placeId))
|
|
268
|
+
list.push(learned);
|
|
269
|
+
s.setbacks = list.slice(-16);
|
|
270
|
+
}
|
|
271
|
+
act.readings = act.readings.filter((x) => x.prospectId !== prospect.id);
|
|
272
|
+
if (!act.readings.length)
|
|
273
|
+
delete s.pendingAct;
|
|
274
|
+
}
|
|
275
|
+
if (at - act.at > 5)
|
|
276
|
+
delete s.pendingAct;
|
|
277
|
+
}
|
|
181
278
|
const held = { ...prospect, lastSeenAt: at };
|
|
182
279
|
if (old)
|
|
183
280
|
s.prospects[s.prospects.indexOf(old)] = held;
|
|
@@ -268,6 +365,49 @@ export function sanitizeProjects(input, now) {
|
|
|
268
365
|
return undefined;
|
|
269
366
|
out.currentGoalId = input.currentGoalId;
|
|
270
367
|
}
|
|
368
|
+
if (input.setbacks !== undefined) {
|
|
369
|
+
if (!Array.isArray(input.setbacks) || input.setbacks.length > 16)
|
|
370
|
+
return undefined;
|
|
371
|
+
out.setbacks = [];
|
|
372
|
+
for (const x of input.setbacks) {
|
|
373
|
+
if (!isObj(x) ||
|
|
374
|
+
!str(x.prospectId) ||
|
|
375
|
+
!str(x.action) ||
|
|
376
|
+
(x.targetId !== undefined && !str(x.targetId)) ||
|
|
377
|
+
(x.placeId !== undefined && !str(x.placeId)))
|
|
378
|
+
return undefined;
|
|
379
|
+
out.setbacks.push({
|
|
380
|
+
prospectId: x.prospectId,
|
|
381
|
+
action: x.action,
|
|
382
|
+
...(x.targetId !== undefined ? { targetId: x.targetId } : {}),
|
|
383
|
+
...(x.placeId !== undefined ? { placeId: x.placeId } : {}),
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const act = input.pendingAct;
|
|
388
|
+
if (act !== undefined) {
|
|
389
|
+
if (!isObj(act) ||
|
|
390
|
+
!str(act.action) ||
|
|
391
|
+
!minute(act.at, now) ||
|
|
392
|
+
(act.targetId !== undefined && !str(act.targetId)) ||
|
|
393
|
+
(act.placeId !== undefined && !str(act.placeId)) ||
|
|
394
|
+
!Array.isArray(act.readings) ||
|
|
395
|
+
act.readings.length > PROJECTS_DEFAULTS.maxProspects)
|
|
396
|
+
return undefined;
|
|
397
|
+
const readings = [];
|
|
398
|
+
for (const x of act.readings) {
|
|
399
|
+
if (!isObj(x) || !str(x.prospectId) || !unit(x.progress))
|
|
400
|
+
return undefined;
|
|
401
|
+
readings.push({ prospectId: x.prospectId, progress: x.progress });
|
|
402
|
+
}
|
|
403
|
+
out.pendingAct = {
|
|
404
|
+
action: act.action,
|
|
405
|
+
at: act.at,
|
|
406
|
+
readings,
|
|
407
|
+
...(act.targetId !== undefined ? { targetId: act.targetId } : {}),
|
|
408
|
+
...(act.placeId !== undefined ? { placeId: act.placeId } : {}),
|
|
409
|
+
};
|
|
410
|
+
}
|
|
271
411
|
const seen = new Set();
|
|
272
412
|
for (const entry of input.prospects.slice(0, PROJECTS_DEFAULTS.maxProspects)) {
|
|
273
413
|
const prospect = cleanProspect(entry);
|
|
@@ -70,7 +70,9 @@ export interface PromptingsNoted {
|
|
|
70
70
|
*/
|
|
71
71
|
export declare function notePromptings(p: Person, seen: readonly PromptingSeen[], chosenId: string | null, now: Minute): PromptingsNoted;
|
|
72
72
|
/** Vocabulary for one prompt under a held reading (narration only; it changes nothing). */
|
|
73
|
-
export declare function promptingLine(reading: PromptingReading, outcome: PromptingRecord['outcome'], kind?: PromptingResponseKind
|
|
73
|
+
export declare function promptingLine(reading: PromptingReading, outcome: PromptingRecord['outcome'], kind?: PromptingResponseKind, options?: {
|
|
74
|
+
voice?: 'first' | 'third';
|
|
75
|
+
}): string;
|
|
74
76
|
/** Read-only summary of recent prompts (a lens; computes nothing new about the person). */
|
|
75
77
|
export interface PromptingsReadingView {
|
|
76
78
|
reading: PromptingReading;
|