@adam0white/human-framework 2.8.0 → 2.9.1
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/dist/agenda/agenda.d.ts +9 -1
- package/dist/agenda/agenda.js +37 -16
- package/dist/beliefs/beliefs.d.ts +22 -0
- package/dist/beliefs/beliefs.js +51 -0
- package/dist/cognition/cognition.js +8 -6
- package/dist/conscience/conscience.d.ts +7 -3
- package/dist/conscience/conscience.js +5 -1
- package/dist/core/math.d.ts +1 -0
- package/dist/core/math.js +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/migrate.js +16 -1
- package/dist/person.js +121 -98
- package/dist/promptings/promptings.d.ts +15 -2
- package/dist/promptings/promptings.js +82 -26
- package/dist/prospection/prospection.js +8 -2
- package/dist/restore.d.ts +9 -0
- package/dist/restore.js +20 -0
- package/dist/review/review.js +16 -2
- package/dist/sim/sim.d.ts +7 -2
- package/dist/sim/sim.js +118 -93
- package/dist/types.d.ts +41 -5
- package/dist/types.js +1 -1
- package/dist/will/will.js +94 -65
- package/package.json +1 -1
package/dist/person.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SCOPE: the composite. `createPerson` wires every faculty's constructor; `tick` advances all faculties in
|
|
3
3
|
* closed form from `p.now` to a later minute under the current activity's body load (idle awake otherwise),
|
|
4
|
-
* splitting at the activity's end; `perceive` runs attention, then beliefs,
|
|
5
|
-
* others' acts, memory and appraisal; `decide` scores offers and resolves the
|
|
6
|
-
* suggestion immediately); `begin` and `finish` bracket an activity and route the
|
|
7
|
-
* skills, habits, memory, conscience, agenda, relationships and trust in advising
|
|
8
|
-
* `command`, `releaseCommand` and `previewCommand`; `knockDown` and `standUp`
|
|
9
|
-
* downed state; `observeSkill` is observational learning; `setRetention` sets the per-person retention
|
|
4
|
+
* splitting at the activity's end and the absolute hourly grid; `perceive` runs attention, then beliefs,
|
|
5
|
+
* relationships, judgement of others' acts, memory and appraisal; `decide` scores offers and resolves the
|
|
6
|
+
* will (`answerNow` answers a suggestion immediately); `begin` and `finish` bracket an activity and route the
|
|
7
|
+
* outcome into body, needs, skills, habits, memory, conscience, agenda, relationships and trust in advising
|
|
8
|
+
* voices. Direct control is `command`, `releaseCommand` and `previewCommand`; `knockDown` and `standUp`
|
|
9
|
+
* move a person in and out of the downed state; `observeSkill` is observational learning; `setRetention` sets the per-person retention
|
|
10
10
|
* bounds; `readPerson` is the read-only summary. Snapshot and restore live in `restore.ts`. This file owns
|
|
11
11
|
* `now`, `activity`, `trace`, `nextDecision` and `retention`; every other slice is written only through its
|
|
12
12
|
* owning module. It sequences the modules and makes no behavioural claims of its own beyond the engineering
|
|
@@ -1136,6 +1136,115 @@ export function learningFor(p, domain) {
|
|
|
1136
1136
|
export function observeSkill(p, skill, minutes, modelLevel, domain) {
|
|
1137
1137
|
return learnByWatching(p, skill, minutes, modelLevel, learningFor(p, domain), p.now);
|
|
1138
1138
|
}
|
|
1139
|
+
/** Realized need changes, using readings captured before and after the outcome's deltas. */
|
|
1140
|
+
function realizedNeedDeltas(act, given, before, after) {
|
|
1141
|
+
const aff = act.affordance;
|
|
1142
|
+
const realized = {};
|
|
1143
|
+
for (const id of [...PHYSIOLOGICAL_NEEDS, ...PSYCHOLOGICAL_NEEDS]) {
|
|
1144
|
+
if (given[id] === undefined)
|
|
1145
|
+
continue;
|
|
1146
|
+
realized[id] =
|
|
1147
|
+
id === 'food' || id === 'water' || id === 'relief'
|
|
1148
|
+
? levelOf(after, id) - levelOf(before, id)
|
|
1149
|
+
: given[id];
|
|
1150
|
+
}
|
|
1151
|
+
for (const id of ['sleep', 'rest']) {
|
|
1152
|
+
if (aff.advertises[id] !== undefined && given[id] === undefined) {
|
|
1153
|
+
realized[id] = levelOf(after, id) - (act.needsAtStart[id] ?? levelOf(before, id));
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
return realized;
|
|
1157
|
+
}
|
|
1158
|
+
/** Record the deed and apply its appraisal, repentance, repair and regulation. */
|
|
1159
|
+
function respondToConscience(p, act, now, completed, minutes) {
|
|
1160
|
+
const aff = act.affordance;
|
|
1161
|
+
const deed = recordDeed(p, aff, act.intention, now, completed, act.necessity ? { necessity: true } : {});
|
|
1162
|
+
for (const ev of deed.appraisal)
|
|
1163
|
+
appraise(p, ev);
|
|
1164
|
+
const tags = aff.tags ?? [];
|
|
1165
|
+
if (completed && (tags.includes('worship') || tags.includes('repent'))) {
|
|
1166
|
+
// Worship closes breaches against God alone; guilt toward a wronged person waits for repair.
|
|
1167
|
+
const repented = new Set();
|
|
1168
|
+
for (const b of p.conscience.breaches) {
|
|
1169
|
+
if (!b.repaired && b.victimId === undefined && repent(p, b.id, now))
|
|
1170
|
+
repented.add(b.normId);
|
|
1171
|
+
}
|
|
1172
|
+
for (const b of p.conscience.breaches)
|
|
1173
|
+
if (!b.repaired && b.victimId !== undefined)
|
|
1174
|
+
repented.delete(b.normId);
|
|
1175
|
+
for (const normId of [...repented].sort())
|
|
1176
|
+
release(p, { id: 'guilt', causePrefix: `breach:${normId}` }, 0.6);
|
|
1177
|
+
regulate(p, minutes, 'worship');
|
|
1178
|
+
}
|
|
1179
|
+
if (completed && (tags.includes('repair') || tags.includes('apologize')) && aff.targetId !== undefined) {
|
|
1180
|
+
recordRepair(p, aff.targetId, now);
|
|
1181
|
+
release(p, { id: 'guilt', targetId: aff.targetId });
|
|
1182
|
+
socialEvent(p, { at: now, kind: 'apology', otherId: aff.targetId, byMe: true, magnitude: 0.7 });
|
|
1183
|
+
}
|
|
1184
|
+
if (act.mode === 'sleep' || tags.includes('rest') || tags.includes('reflection')) {
|
|
1185
|
+
regulate(p, minutes, tags.includes('reflection') ? 'reflection' : 'rest');
|
|
1186
|
+
}
|
|
1187
|
+
return { deed, tags };
|
|
1188
|
+
}
|
|
1189
|
+
/** Record relationships and character experience from a completed activity. */
|
|
1190
|
+
function recordParticipation(p, aff, tags, now, minutes) {
|
|
1191
|
+
const D = PERSON_DEFAULTS;
|
|
1192
|
+
if (p.character && (aff.with ?? []).some((o) => o !== p.id))
|
|
1193
|
+
noteCharacterSocial(p, minutes);
|
|
1194
|
+
const kind = tags.includes('work') ? 'shared-work' : 'chat';
|
|
1195
|
+
for (const other of aff.with ?? []) {
|
|
1196
|
+
if (other === p.id)
|
|
1197
|
+
continue;
|
|
1198
|
+
socialEvent(p, { at: now, kind, otherId: other, byMe: true, magnitude: D.participationMagnitude });
|
|
1199
|
+
}
|
|
1200
|
+
if (aff.targetId !== undefined && aff.targetId !== p.id) {
|
|
1201
|
+
const care = tags.includes('help') ? 'help' : tags.includes('gift') ? 'gift' : undefined;
|
|
1202
|
+
if (care)
|
|
1203
|
+
socialEvent(p, { at: now, kind: care, otherId: aff.targetId, byMe: true, magnitude: 0.7 });
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
/** Learn from each advising voice after the activity's conscience and agenda results are known. */
|
|
1207
|
+
function learnActivityVoices(p, act, after, now, felt, broken, breached) {
|
|
1208
|
+
const aff = act.affordance;
|
|
1209
|
+
const D = PERSON_DEFAULTS;
|
|
1210
|
+
// Trust in every voice that pushed this activity: how it felt, minus bodily harm the host's deltas do not
|
|
1211
|
+
// show (needs that fell during the activity and are now urgent).
|
|
1212
|
+
const voices = act.suggestions ?? (act.suggestion ? [act.suggestion] : []);
|
|
1213
|
+
if (voices.length > 0) {
|
|
1214
|
+
let harm = 0;
|
|
1215
|
+
for (const id of ['food', 'water', 'sleep', 'rest']) {
|
|
1216
|
+
if (urgencyOf(after, id) < D.voiceHarmUrgency)
|
|
1217
|
+
continue;
|
|
1218
|
+
harm += Math.max(0, (act.needsAtStart[id] ?? levelOf(after, id)) - levelOf(after, id));
|
|
1219
|
+
}
|
|
1220
|
+
// Doing what a voice pushed broke a commitment or the person's own standards: that costs the voice as harm
|
|
1221
|
+
// does (review 2026-10-03: thirty broken fasts under insistence had moved trust only 0.50 -> 0.41). Omissions
|
|
1222
|
+
// the push caused later (a missed prayer during a pushed activity) are not attributed here.
|
|
1223
|
+
let breach = 0;
|
|
1224
|
+
for (const c of broken)
|
|
1225
|
+
breach += D.missedDistress * c.importance;
|
|
1226
|
+
breach += D.missedDistress * 0.5 * breached;
|
|
1227
|
+
for (const res of voices) {
|
|
1228
|
+
const protest = res.verdict === 'complied' || res.verdict === 'commanded';
|
|
1229
|
+
const reason = breach > 0
|
|
1230
|
+
? protest
|
|
1231
|
+
? 'breach-under-protest'
|
|
1232
|
+
: 'breach'
|
|
1233
|
+
: harm > 0
|
|
1234
|
+
? protest
|
|
1235
|
+
? 'harm-under-protest'
|
|
1236
|
+
: 'harm'
|
|
1237
|
+
: felt >= 0
|
|
1238
|
+
? 'went-well'
|
|
1239
|
+
: 'went-badly';
|
|
1240
|
+
learnFromVoice(p, res, clampSigned(felt - D.voiceHarmGain * harm - breach), {
|
|
1241
|
+
at: now,
|
|
1242
|
+
action: aff.action,
|
|
1243
|
+
reason,
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1139
1248
|
/**
|
|
1140
1249
|
* Apply what happened. Physiological deltas go to the body, psychological ones to the needs, then skills,
|
|
1141
1250
|
* habits, memory (expectation learning and an episode), conscience (deed, breaches, repair), agenda,
|
|
@@ -1179,20 +1288,7 @@ export function finish(p, outcome, opts = {}) {
|
|
|
1179
1288
|
// Realized deltas: what the body actually took in for food/water/relief (a drink at full hydration
|
|
1180
1289
|
// realizes nothing), host values for psychological needs, and measured since the start for sleep/rest.
|
|
1181
1290
|
const after = readNeeds(p, readBodyOf(p));
|
|
1182
|
-
const realized =
|
|
1183
|
-
for (const id of [...PHYSIOLOGICAL_NEEDS, ...PSYCHOLOGICAL_NEEDS]) {
|
|
1184
|
-
if (given[id] === undefined)
|
|
1185
|
-
continue;
|
|
1186
|
-
realized[id] =
|
|
1187
|
-
id === 'food' || id === 'water' || id === 'relief'
|
|
1188
|
-
? levelOf(after, id) - levelOf(before, id)
|
|
1189
|
-
: given[id];
|
|
1190
|
-
}
|
|
1191
|
-
for (const id of ['sleep', 'rest']) {
|
|
1192
|
-
if (aff.advertises[id] !== undefined && given[id] === undefined) {
|
|
1193
|
-
realized[id] = levelOf(after, id) - (act.needsAtStart[id] ?? levelOf(before, id));
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1291
|
+
const realized = realizedNeedDeltas(act, given, before, after);
|
|
1196
1292
|
// Felt valence.
|
|
1197
1293
|
let felt = 0;
|
|
1198
1294
|
for (const id of Object.keys(realized)) {
|
|
@@ -1255,32 +1351,7 @@ export function finish(p, outcome, opts = {}) {
|
|
|
1255
1351
|
episode.voiceId = act.suggestion.voiceId;
|
|
1256
1352
|
remember(p, episode);
|
|
1257
1353
|
// Conscience.
|
|
1258
|
-
const deed =
|
|
1259
|
-
for (const ev of deed.appraisal)
|
|
1260
|
-
appraise(p, ev);
|
|
1261
|
-
const tags = aff.tags ?? [];
|
|
1262
|
-
if (completed && (tags.includes('worship') || tags.includes('repent'))) {
|
|
1263
|
-
// Worship closes breaches against God alone; guilt toward a wronged person waits for repair.
|
|
1264
|
-
const repented = new Set();
|
|
1265
|
-
for (const b of p.conscience.breaches) {
|
|
1266
|
-
if (!b.repaired && b.victimId === undefined && repent(p, b.id, now))
|
|
1267
|
-
repented.add(b.normId);
|
|
1268
|
-
}
|
|
1269
|
-
for (const b of p.conscience.breaches)
|
|
1270
|
-
if (!b.repaired && b.victimId !== undefined)
|
|
1271
|
-
repented.delete(b.normId);
|
|
1272
|
-
for (const normId of [...repented].sort())
|
|
1273
|
-
release(p, { id: 'guilt', causePrefix: `breach:${normId}` }, 0.6);
|
|
1274
|
-
regulate(p, minutes, 'worship');
|
|
1275
|
-
}
|
|
1276
|
-
if (completed && (tags.includes('repair') || tags.includes('apologize')) && aff.targetId !== undefined) {
|
|
1277
|
-
recordRepair(p, aff.targetId, now);
|
|
1278
|
-
release(p, { id: 'guilt', targetId: aff.targetId });
|
|
1279
|
-
socialEvent(p, { at: now, kind: 'apology', otherId: aff.targetId, byMe: true, magnitude: 0.7 });
|
|
1280
|
-
}
|
|
1281
|
-
if (act.mode === 'sleep' || tags.includes('rest') || tags.includes('reflection')) {
|
|
1282
|
-
regulate(p, minutes, tags.includes('reflection') ? 'reflection' : 'rest');
|
|
1283
|
-
}
|
|
1354
|
+
const { deed, tags } = respondToConscience(p, act, now, completed, minutes);
|
|
1284
1355
|
// Agenda.
|
|
1285
1356
|
const routed = { ...outcome };
|
|
1286
1357
|
if (routed.fulfills === undefined && aff.fulfills !== undefined)
|
|
@@ -1303,58 +1374,10 @@ export function finish(p, outcome, opts = {}) {
|
|
|
1303
1374
|
}
|
|
1304
1375
|
}
|
|
1305
1376
|
// Relationships from taking part together.
|
|
1306
|
-
if (completed)
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
for (const other of aff.with ?? []) {
|
|
1311
|
-
if (other === p.id)
|
|
1312
|
-
continue;
|
|
1313
|
-
socialEvent(p, { at: now, kind, otherId: other, byMe: true, magnitude: D.participationMagnitude });
|
|
1314
|
-
}
|
|
1315
|
-
if (aff.targetId !== undefined && aff.targetId !== p.id) {
|
|
1316
|
-
const care = tags.includes('help') ? 'help' : tags.includes('gift') ? 'gift' : undefined;
|
|
1317
|
-
if (care)
|
|
1318
|
-
socialEvent(p, { at: now, kind: care, otherId: aff.targetId, byMe: true, magnitude: 0.7 });
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
// Trust in every voice that pushed this activity: how it felt, minus bodily harm the host's deltas do not
|
|
1322
|
-
// show (needs that fell during the activity and are now urgent).
|
|
1323
|
-
const voices = act.suggestions ?? (act.suggestion ? [act.suggestion] : []);
|
|
1324
|
-
if (voices.length > 0) {
|
|
1325
|
-
let harm = 0;
|
|
1326
|
-
for (const id of ['food', 'water', 'sleep', 'rest']) {
|
|
1327
|
-
if (urgencyOf(after, id) < D.voiceHarmUrgency)
|
|
1328
|
-
continue;
|
|
1329
|
-
harm += Math.max(0, (act.needsAtStart[id] ?? levelOf(after, id)) - levelOf(after, id));
|
|
1330
|
-
}
|
|
1331
|
-
// Doing what a voice pushed broke a commitment or the person's own standards: that costs the voice as harm
|
|
1332
|
-
// does (review 2026-10-03: thirty broken fasts under insistence had moved trust only 0.50 -> 0.41). Omissions
|
|
1333
|
-
// the push caused later (a missed prayer during a pushed activity) are not attributed here.
|
|
1334
|
-
let breach = 0;
|
|
1335
|
-
for (const c of broken)
|
|
1336
|
-
breach += D.missedDistress * c.importance;
|
|
1337
|
-
breach += D.missedDistress * 0.5 * deed.breached.length;
|
|
1338
|
-
for (const res of voices) {
|
|
1339
|
-
const protest = res.verdict === 'complied' || res.verdict === 'commanded';
|
|
1340
|
-
const reason = breach > 0
|
|
1341
|
-
? protest
|
|
1342
|
-
? 'breach-under-protest'
|
|
1343
|
-
: 'breach'
|
|
1344
|
-
: harm > 0
|
|
1345
|
-
? protest
|
|
1346
|
-
? 'harm-under-protest'
|
|
1347
|
-
: 'harm'
|
|
1348
|
-
: felt >= 0
|
|
1349
|
-
? 'went-well'
|
|
1350
|
-
: 'went-badly';
|
|
1351
|
-
learnFromVoice(p, res, clampSigned(felt - D.voiceHarmGain * harm - breach), {
|
|
1352
|
-
at: now,
|
|
1353
|
-
action: aff.action,
|
|
1354
|
-
reason,
|
|
1355
|
-
});
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1377
|
+
if (completed)
|
|
1378
|
+
recordParticipation(p, aff, tags, now, minutes);
|
|
1379
|
+
// Voice learning follows both conscience and agenda, so it includes their breaches.
|
|
1380
|
+
learnActivityVoices(p, act, after, now, felt, broken, deed.breached.length);
|
|
1358
1381
|
// Chronicle, while the activity is still current.
|
|
1359
1382
|
const noted = { kept: kept.map((c) => c.id) };
|
|
1360
1383
|
if (act.suggestion)
|
|
@@ -26,8 +26,17 @@ export declare const PROMPTINGS_DEFAULTS: {
|
|
|
26
26
|
* keeps its practice and last use (the spec's `practice` only seeds new responses). Returns the state.
|
|
27
27
|
*/
|
|
28
28
|
export declare function enablePromptings(p: Person, spec?: PromptingsSpec): PromptingsState;
|
|
29
|
-
/** Practice of a response at `now`, after disuse decay. Pure. */
|
|
30
|
-
export declare function practiceAt(r: PromptingResponse, now: Minute): Unit;
|
|
29
|
+
/** Practice of a response at `now`, after disuse decay (`halfLifeDays` defaults to 60, as before 2.9.0). Pure. */
|
|
30
|
+
export declare function practiceAt(r: PromptingResponse, now: Minute, halfLifeDays?: number): Unit;
|
|
31
|
+
/** What a prompt offers a cue to match (2.9.0). */
|
|
32
|
+
export interface PromptContext {
|
|
33
|
+
action: string;
|
|
34
|
+
tags?: readonly string[] | undefined;
|
|
35
|
+
/** The opposing term's source: 'norm:x' or 'goal:g'. */
|
|
36
|
+
opposedBy: string;
|
|
37
|
+
}
|
|
38
|
+
/** Whether a response's cue matches a prompt: its action, one of its tags, or the opposing norm or goal. Pure. */
|
|
39
|
+
export declare function responseCueMatches(cue: string, at: PromptContext): boolean;
|
|
31
40
|
/** Whether an option's terms carry a noticed prompt: its strongest pull and the opposing term. Pure. */
|
|
32
41
|
export declare function promptIn(terms: readonly Term[], tags: readonly string[] | undefined): {
|
|
33
42
|
source: string;
|
|
@@ -46,6 +55,8 @@ export interface PromptingSeen {
|
|
|
46
55
|
opposedBy: string;
|
|
47
56
|
/** Whether a turning-away term was applied. */
|
|
48
57
|
turnApplied: boolean;
|
|
58
|
+
/** The offer's tags (2.9.0), for matching a response's cue. */
|
|
59
|
+
tags?: readonly string[] | undefined;
|
|
49
60
|
}
|
|
50
61
|
/** What `notePromptings` did, for the composite (the `fault` guilt goes through affect). */
|
|
51
62
|
export interface PromptingsNoted {
|
|
@@ -68,10 +79,12 @@ export interface PromptingsReadingView {
|
|
|
68
79
|
turned: number;
|
|
69
80
|
passed: number;
|
|
70
81
|
assented: number;
|
|
82
|
+
/** `cue` only when the response has one (2.9.0). */
|
|
71
83
|
responses: {
|
|
72
84
|
id: string;
|
|
73
85
|
kind: PromptingResponseKind;
|
|
74
86
|
practice: Unit;
|
|
87
|
+
cue?: string;
|
|
75
88
|
}[];
|
|
76
89
|
/** Vocabulary for the last few prompts, newest last. */
|
|
77
90
|
lines: string[];
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Detection reads an option's traced terms and changes none of them: the pull is the sum of positive `habit`,
|
|
10
10
|
* `emotion:*`, `need:*` and `promptings:echo` terms (every positive term on an offer tagged `temptation`); it is a
|
|
11
|
-
* prompt when the pull reaches `noticeAt` while a `norm:*` or `goal:*` term on the same option is negative
|
|
11
|
+
* prompt when the pull reaches `noticeAt` while a `norm:*` or `goal:*` term on the same option is negative (goal
|
|
12
|
+
* terms are never negative, see `agendaTerms`, so in practice the opposition is a held norm).
|
|
12
13
|
* Abstention windows are not detected (they veto, and carry no term). A person with a response gets one extra
|
|
13
14
|
* term, `promptings:turn` = -turnMax × practice, on that option; it is bounded and absent at survival desperation,
|
|
14
15
|
* so a desperate bodily need still wins (2:286). Each prompt is recorded once per decision point (a review that
|
|
@@ -16,6 +17,16 @@
|
|
|
16
17
|
* or `passed` (not chosen, no response). Practice grows with diminishing gains once per decision point where the
|
|
17
18
|
* response was used (more when it held; a vetoed option earns none, since the will blocked it), and halves over `practiceHalfLifeDays` of disuse.
|
|
18
19
|
*
|
|
20
|
+
* Cued responses and host time (2.9.0, both opt-in): a response may carry a `cue` (an action, an offer tag, or the
|
|
21
|
+
* opposing norm or goal), for a phrase learned at one pull. At a prompt the person uses the most practised response
|
|
22
|
+
* whose cue matches it; when none matches, the most practised response without a cue. A cued response never serves
|
|
23
|
+
* a prompt it does not match, so a phrase learned at the alarm does not also turn the person from gossip (the
|
|
24
|
+
* literal "global best" fallback would bring that back). Each response used at a decision point gains once. Without
|
|
25
|
+
* cues every response is uncued and this is the 2.4.0 rule exactly. `practiceHalfLifeDays` in `enablePromptings`
|
|
26
|
+
* replaces the 60-day half-life for the whole person (a one-week game can let a phrase fade); it is one setting per
|
|
27
|
+
* person, not per response, because the need is the host's time scale and no source gives phrases different decay.
|
|
28
|
+
* Practice is decayed lazily from `lastUsedAt`, so a new half-life also applies to disuse already elapsed.
|
|
29
|
+
*
|
|
19
30
|
* Held readings (reconciliation.md §5): `whisper`, `inclination`, `intrusion` and `none` give identical decisions
|
|
20
31
|
* and differ only in the vocabulary of `promptingLine`. `fault` (the scrupulous reading, which both the tradition
|
|
21
32
|
* and clinicians reject) adds a small guilt emotion at the prompt itself, from the person's own understanding;
|
|
@@ -71,7 +82,14 @@ export function enablePromptings(p, spec = {}) {
|
|
|
71
82
|
continue;
|
|
72
83
|
const held = prev?.responses.find((x) => x.id === r.id);
|
|
73
84
|
const seed = Number.isFinite(r.practice) ? clamp01(r.practice) : 0.1;
|
|
74
|
-
|
|
85
|
+
const next = held
|
|
86
|
+
? { ...held, kind: r.kind }
|
|
87
|
+
: { id: r.id, kind: r.kind, practice: seed, lastUsedAt: p.now };
|
|
88
|
+
// The spec's cue wins; a response re-enabled without one loses its old cue.
|
|
89
|
+
delete next.cue;
|
|
90
|
+
if (cueOk(r.cue))
|
|
91
|
+
next.cue = r.cue;
|
|
92
|
+
responses.push(next);
|
|
75
93
|
}
|
|
76
94
|
const state = {
|
|
77
95
|
reading: spec.reading && READINGS.includes(spec.reading) ? spec.reading : 'none',
|
|
@@ -82,22 +100,44 @@ export function enablePromptings(p, spec = {}) {
|
|
|
82
100
|
};
|
|
83
101
|
if (spec.readingSource !== undefined)
|
|
84
102
|
state.readingSource = spec.readingSource;
|
|
103
|
+
if (halfLifeOk(spec.practiceHalfLifeDays))
|
|
104
|
+
state.practiceHalfLifeDays = spec.practiceHalfLifeDays;
|
|
85
105
|
p.promptings = state;
|
|
86
106
|
return state;
|
|
87
107
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
108
|
+
const cueOk = (x) => typeof x === 'string' && x !== '' && x.length <= 200;
|
|
109
|
+
const halfLifeOk = (x) => typeof x === 'number' && Number.isFinite(x) && x > 0 && x <= 36500;
|
|
110
|
+
const halfLifeOf = (s) => s.practiceHalfLifeDays ?? PROMPTINGS_DEFAULTS.practiceHalfLifeDays;
|
|
111
|
+
/** Practice of a response at `now`, after disuse decay (`halfLifeDays` defaults to 60, as before 2.9.0). Pure. */
|
|
112
|
+
export function practiceAt(r, now, halfLifeDays = PROMPTINGS_DEFAULTS.practiceHalfLifeDays) {
|
|
113
|
+
return clamp01(halve(r.practice, now - r.lastUsedAt, halfLifeDays));
|
|
114
|
+
}
|
|
115
|
+
/** Whether a response's cue matches a prompt: its action, one of its tags, or the opposing norm or goal. Pure. */
|
|
116
|
+
export function responseCueMatches(cue, at) {
|
|
117
|
+
if (cue === at.action || cue === at.opposedBy || (at.tags?.includes(cue) ?? false))
|
|
118
|
+
return true;
|
|
119
|
+
const colon = at.opposedBy.indexOf(':');
|
|
120
|
+
return colon >= 0 && cue === at.opposedBy.slice(colon + 1);
|
|
91
121
|
}
|
|
92
|
-
/**
|
|
93
|
-
|
|
94
|
-
|
|
122
|
+
/**
|
|
123
|
+
* The response a person would use now at prompt `at`: the most practised whose cue matches, else the most
|
|
124
|
+
* practised without a cue; ties by id. Without cues this is the most practised response (2.4.0).
|
|
125
|
+
*/
|
|
126
|
+
function bestResponse(s, now, at) {
|
|
127
|
+
let cued;
|
|
128
|
+
let plain;
|
|
129
|
+
const better = (b, r, x) => !b || x > b.practice || (x === b.practice && r.id < b.r.id);
|
|
130
|
+
const half = halfLifeOf(s);
|
|
95
131
|
for (const r of s.responses) {
|
|
96
|
-
const practice = practiceAt(r, now);
|
|
97
|
-
if (
|
|
98
|
-
|
|
132
|
+
const practice = practiceAt(r, now, half);
|
|
133
|
+
if (r.cue === undefined) {
|
|
134
|
+
if (better(plain, r, practice))
|
|
135
|
+
plain = { r, practice };
|
|
136
|
+
}
|
|
137
|
+
else if (responseCueMatches(r.cue, at) && better(cued, r, practice))
|
|
138
|
+
cued = { r, practice };
|
|
99
139
|
}
|
|
100
|
-
return
|
|
140
|
+
return cued ?? plain;
|
|
101
141
|
}
|
|
102
142
|
const isPull = (source, tagged) => tagged ||
|
|
103
143
|
source === 'habit' ||
|
|
@@ -145,10 +185,13 @@ export function promptingTerms(p, action, terms, tags, now, desperation) {
|
|
|
145
185
|
}
|
|
146
186
|
if (desperation >= PROMPTINGS_DEFAULTS.survivalDesperation)
|
|
147
187
|
return out;
|
|
148
|
-
|
|
149
|
-
if (!best || best.practice <= 0)
|
|
188
|
+
if (s.responses.length === 0)
|
|
150
189
|
return out;
|
|
151
|
-
|
|
190
|
+
const seen = promptIn([...terms, ...out], tags);
|
|
191
|
+
if (!seen)
|
|
192
|
+
return out;
|
|
193
|
+
const best = bestResponse(s, now, { action, tags, opposedBy: seen.opposedBy });
|
|
194
|
+
if (!best || best.practice <= 0)
|
|
152
195
|
return out;
|
|
153
196
|
const v = round(-PROMPTINGS_DEFAULTS.turnMax * best.practice);
|
|
154
197
|
if (v < 0)
|
|
@@ -165,11 +208,12 @@ export function notePromptings(p, seen, chosenId, now) {
|
|
|
165
208
|
if (!s || seen.length === 0)
|
|
166
209
|
return noted;
|
|
167
210
|
const D = PROMPTINGS_DEFAULTS;
|
|
168
|
-
|
|
169
|
-
//
|
|
170
|
-
|
|
211
|
+
// One practice gain per response per decision point, however many options it served: the larger one if any held.
|
|
212
|
+
// Responses are chosen before any practice moves, as `promptingTerms` chose them.
|
|
213
|
+
const gains = new Map();
|
|
171
214
|
for (const x of seen) {
|
|
172
215
|
const assented = x.affordanceId === chosenId;
|
|
216
|
+
const best = bestResponse(s, now, x);
|
|
173
217
|
const used = x.turnApplied && best !== undefined;
|
|
174
218
|
const rec = {
|
|
175
219
|
at: now,
|
|
@@ -181,7 +225,7 @@ export function notePromptings(p, seen, chosenId, now) {
|
|
|
181
225
|
};
|
|
182
226
|
if (used && best) {
|
|
183
227
|
rec.responseId = best.r.id;
|
|
184
|
-
|
|
228
|
+
gains.set(best.r, Math.max(gains.get(best.r) ?? 0, assented ? D.gainAttempt : D.gainTurned));
|
|
185
229
|
}
|
|
186
230
|
s.recent.push(rec);
|
|
187
231
|
noted.records.push(rec);
|
|
@@ -194,10 +238,12 @@ export function notePromptings(p, seen, chosenId, now) {
|
|
|
194
238
|
}
|
|
195
239
|
}
|
|
196
240
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
241
|
+
for (const [r, gain] of gains) {
|
|
242
|
+
if (gain <= 0)
|
|
243
|
+
continue;
|
|
244
|
+
const practice = practiceAt(r, now, halfLifeOf(s));
|
|
245
|
+
r.practice = round(clamp01(practice + gain * (1 - practice)));
|
|
246
|
+
r.lastUsedAt = now;
|
|
201
247
|
}
|
|
202
248
|
if (s.recent.length > D.maxRecent)
|
|
203
249
|
s.recent.splice(0, s.recent.length - D.maxRecent);
|
|
@@ -249,7 +295,12 @@ export function promptingsReading(p, now = p.now) {
|
|
|
249
295
|
turned: count('turned'),
|
|
250
296
|
passed: count('passed'),
|
|
251
297
|
assented: count('assented'),
|
|
252
|
-
responses: s.responses.map((r) => ({
|
|
298
|
+
responses: s.responses.map((r) => ({
|
|
299
|
+
id: r.id,
|
|
300
|
+
kind: r.kind,
|
|
301
|
+
practice: round(practiceAt(r, now, halfLifeOf(s))),
|
|
302
|
+
...(r.cue !== undefined ? { cue: r.cue } : {}),
|
|
303
|
+
})),
|
|
253
304
|
lines: s.recent.slice(-5).map((r) => promptingLine(s.reading, r.outcome, kindOf(r.responseId))),
|
|
254
305
|
};
|
|
255
306
|
if (s.readingSource !== undefined)
|
|
@@ -298,12 +349,15 @@ export function sanitizePromptings(input, now) {
|
|
|
298
349
|
continue;
|
|
299
350
|
if (!minute(r.lastUsedAt) || r.lastUsedAt > now || responses.some((x) => x.id === r.id))
|
|
300
351
|
continue;
|
|
301
|
-
|
|
352
|
+
const kept = {
|
|
302
353
|
id: r.id,
|
|
303
354
|
kind: r.kind,
|
|
304
355
|
practice: r.practice,
|
|
305
356
|
lastUsedAt: r.lastUsedAt,
|
|
306
|
-
}
|
|
357
|
+
};
|
|
358
|
+
if (cueOk(r.cue))
|
|
359
|
+
kept.cue = r.cue;
|
|
360
|
+
responses.push(kept);
|
|
307
361
|
}
|
|
308
362
|
const recent = [];
|
|
309
363
|
for (const r of input.recent.slice(-D.maxRecent)) {
|
|
@@ -344,5 +398,7 @@ export function sanitizePromptings(input, now) {
|
|
|
344
398
|
};
|
|
345
399
|
if (str(input.readingSource))
|
|
346
400
|
out.readingSource = input.readingSource;
|
|
401
|
+
if (halfLifeOk(input.practiceHalfLifeDays))
|
|
402
|
+
out.practiceHalfLifeDays = input.practiceHalfLifeDays;
|
|
347
403
|
return out;
|
|
348
404
|
}
|
|
@@ -260,9 +260,15 @@ export function sanitizeProspection(input, now, personId) {
|
|
|
260
260
|
!minute(r.receivedAt) ||
|
|
261
261
|
r.receivedAt > now)
|
|
262
262
|
continue;
|
|
263
|
-
|
|
263
|
+
const validated = {
|
|
264
|
+
sourceId: r.sourceId,
|
|
265
|
+
id: r.id,
|
|
266
|
+
revision: r.revision,
|
|
267
|
+
receivedAt: r.receivedAt,
|
|
268
|
+
};
|
|
269
|
+
if (revisions.some((old) => identity(old, validated)))
|
|
264
270
|
return undefined;
|
|
265
|
-
revisions.push(
|
|
271
|
+
revisions.push(validated);
|
|
266
272
|
}
|
|
267
273
|
const valid = [];
|
|
268
274
|
for (const e of entries) {
|
package/dist/restore.d.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import type { Person } from './types.ts';
|
|
2
2
|
/** A deep copy of the person as plain JSON (what a host saves). */
|
|
3
3
|
export declare function snapshot(p: Person): Person;
|
|
4
|
+
/**
|
|
5
|
+
* A deterministic 16-hex-digit digest of the canonical saved person. Canonicalization runs the saved form
|
|
6
|
+
* through `restore` (including migrations, defaults, bounds and optional-slice sanitizers), then sorts every
|
|
7
|
+
* object's keys recursively; array order stays significant. Thus key insertion order and empty optional slices
|
|
8
|
+
* that `restore` drops do not change the digest. Includes the engine stamp. Reads only; never mutates `p`.
|
|
9
|
+
* Uses 64-bit FNV-1a over JSON's UTF-16 code units, with no platform or crypto dependency. This is a change
|
|
10
|
+
* detector, not a cryptographic integrity check; collisions are possible. Invalid saves throw as in `restore`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function fingerprint(p: Person): string;
|
|
4
13
|
/**
|
|
5
14
|
* Validate a saved person and fill defaults for missing or mistyped fields, slice by slice and field by
|
|
6
15
|
* field. A save from an earlier supported engine is upgraded by `migrate` first. Throws on a wrong schema, a
|
package/dist/restore.js
CHANGED
|
@@ -31,6 +31,26 @@ import { sanitizeWill } from './will/index.js';
|
|
|
31
31
|
export function snapshot(p) {
|
|
32
32
|
return structuredClone(p);
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* A deterministic 16-hex-digit digest of the canonical saved person. Canonicalization runs the saved form
|
|
36
|
+
* through `restore` (including migrations, defaults, bounds and optional-slice sanitizers), then sorts every
|
|
37
|
+
* object's keys recursively; array order stays significant. Thus key insertion order and empty optional slices
|
|
38
|
+
* that `restore` drops do not change the digest. Includes the engine stamp. Reads only; never mutates `p`.
|
|
39
|
+
* Uses 64-bit FNV-1a over JSON's UTF-16 code units, with no platform or crypto dependency. This is a change
|
|
40
|
+
* detector, not a cryptographic integrity check; collisions are possible. Invalid saves throw as in `restore`.
|
|
41
|
+
*/
|
|
42
|
+
export function fingerprint(p) {
|
|
43
|
+
const json = JSON.stringify(restore(snapshot(p)), (_key, value) => {
|
|
44
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
45
|
+
return value;
|
|
46
|
+
return Object.fromEntries(Object.entries(value).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
|
|
47
|
+
});
|
|
48
|
+
let hash = 0xcbf29ce484222325n;
|
|
49
|
+
for (let i = 0; i < json.length; i++) {
|
|
50
|
+
hash = BigInt.asUintN(64, (hash ^ BigInt(json.charCodeAt(i))) * 0x100000001b3n);
|
|
51
|
+
}
|
|
52
|
+
return hash.toString(16).padStart(16, '0');
|
|
53
|
+
}
|
|
34
54
|
// Unlike core `isObj`, arrays pass: a required slice saved as an array is filled from defaults, not refused.
|
|
35
55
|
const isObject = (x) => typeof x === 'object' && x !== null;
|
|
36
56
|
const isMinute = (x) => isNum(x) && Math.abs(x) <= MAX_MINUTE;
|
package/dist/review/review.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* the commitments the chronicle recorded as kept or broken since their last review, and for each one they broke
|
|
4
4
|
* forms or renews a *resolve*: an if–then intention for that commitment's next occasion (Gollwitzer & Sheeran
|
|
5
5
|
* 2006, research/reconciliation.md E1). While a matching commitment is pending, the resolve adds one bounded term,
|
|
6
|
-
* `review:resolve` (each resolve counted once per option, the sum clamped to ±`resolveScale
|
|
6
|
+
* `review:resolve` (each resolve counted once per option, the sum clamped to ±`resolveScale`; 2.9.0: the term's
|
|
7
|
+
* `ref` is the resolve's key when exactly one resolve weighed in), with the sign of the commitment's own term (toward a duty, away from an abstention's breaking
|
|
7
8
|
* act), scaled by its strength. Strength grows with diminishing gains at each further lapse, relaxes once the
|
|
8
9
|
* commitment is kept, and fades over `halfLifeDays` of disuse. Released and excused commitments (exemptions,
|
|
9
10
|
* sleep, necessity) form no resolve: what was beyond capacity is not held against the person (2:286). The review
|
|
@@ -23,6 +24,11 @@
|
|
|
23
24
|
* piety, repentance as a ruling (tawba conditions are not modelled), effects on sleep onset (Harvey's model, E6, is
|
|
24
25
|
* not evidence enough), or that review improves outcomes beyond the borrowed if–then shape. It draws no randomness;
|
|
25
26
|
* without `enableReview` nothing here runs and existing runs replay byte for byte.
|
|
27
|
+
*
|
|
28
|
+
* Host notes (2.9.0, docs only). `reviewLine` sets commitment labels into "I didn't keep …" and "Today I kept …",
|
|
29
|
+
* so a label should be a noun phrase ("my call to Mum", "the evening prayer"), not a verb phrase ("calling Mum").
|
|
30
|
+
* A recurring commitment lapses on any day its window passes unlived: a host that skips a day (no service, a
|
|
31
|
+
* fast-forward) should still live, or release, that day's occasions, or the review counts each one as missed.
|
|
26
32
|
*/
|
|
27
33
|
import { clamp01, dexp, isObj, minuteOfDay, round } from '../core/index.js';
|
|
28
34
|
export const REVIEW_DEFAULTS = {
|
|
@@ -72,6 +78,7 @@ export function reviewTerms(p, terms) {
|
|
|
72
78
|
return [];
|
|
73
79
|
let value = 0;
|
|
74
80
|
const used = new Set();
|
|
81
|
+
let only;
|
|
75
82
|
for (const t of terms) {
|
|
76
83
|
const m = /^(commitment|abstain):(.+)$/.exec(t.source);
|
|
77
84
|
if (!m || t.value === 0)
|
|
@@ -83,11 +90,18 @@ export function reviewTerms(p, terms) {
|
|
|
83
90
|
if (!r || used.has(r.key))
|
|
84
91
|
continue;
|
|
85
92
|
used.add(r.key);
|
|
93
|
+
only = r.key;
|
|
86
94
|
value += Math.sign(t.value) * REVIEW_DEFAULTS.resolveScale * r.strength;
|
|
87
95
|
}
|
|
88
96
|
const max = REVIEW_DEFAULTS.resolveScale;
|
|
89
97
|
value = Math.max(-max, Math.min(max, value));
|
|
90
|
-
|
|
98
|
+
if (value === 0)
|
|
99
|
+
return [];
|
|
100
|
+
const term = { source: 'review:resolve', value: round(value) };
|
|
101
|
+
// Name the resolve when exactly one weighed in (2.9.0, The Pass): "he turned from the jar, as he said he would".
|
|
102
|
+
if (used.size === 1 && only !== undefined)
|
|
103
|
+
term.ref = only;
|
|
104
|
+
return [term];
|
|
91
105
|
}
|
|
92
106
|
const fade = (s, days) => days <= 0 ? s : s * dexp((-Math.LN2 * days) / REVIEW_DEFAULTS.halfLifeDays);
|
|
93
107
|
/**
|
package/dist/sim/sim.d.ts
CHANGED
|
@@ -225,6 +225,7 @@ export declare function communityState(c: Community): CommunityState;
|
|
|
225
225
|
/**
|
|
226
226
|
* A community over `people`. With `prior` (from `communityState`, possibly through JSON), the host-side state is
|
|
227
227
|
* resumed from it, so a run restored from people + community + world state continues exactly as it would have.
|
|
228
|
+
* `prior` is copied: after stepping, write back `communityState(c)`; the old `prior` stays stale.
|
|
228
229
|
*/
|
|
229
230
|
export declare function createCommunity(people: Person[], prior?: CommunityState): Community;
|
|
230
231
|
/** Add a person to a community (a newborn, an arrival); keeps the id order the driver relies on. */
|
|
@@ -237,9 +238,13 @@ export declare function birth(c: Community, a: Person, b: Person, spec: ChildSpe
|
|
|
237
238
|
/**
|
|
238
239
|
* Tell `dead`'s death at `at` to everyone alive in the community who has a tie to them (1.8.0): a told `death`
|
|
239
240
|
* percept through `perceive`, so they grieve, the tie is kept and marked, and a spouse is widowed. Each listener's
|
|
240
|
-
*
|
|
241
|
+
* receipt time is at least their own `now`. `late: 'now'` explicitly permits listeners past `at`, as does the
|
|
242
|
+
* existing default. `late: 'throw'` requires all eligible listeners to be at or before `at` and throws before
|
|
243
|
+
* delivering any news if one is ahead. This changes no listener's clock.
|
|
241
244
|
*/
|
|
242
|
-
export declare function tellDeath(c: Community, dead: Person, at: Minute
|
|
245
|
+
export declare function tellDeath(c: Community, dead: Person, at: Minute, opts?: {
|
|
246
|
+
late?: 'throw' | 'now';
|
|
247
|
+
}): void;
|
|
243
248
|
/** Next minute at which this person needs attention. */
|
|
244
249
|
export declare function nextEventAt(c: Community, p: Person): Minute;
|
|
245
250
|
/**
|