@adam0white/human-framework 2.6.0 → 2.8.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/dist/body/body.d.ts +5 -0
- package/dist/body/body.js +9 -0
- package/dist/cognition/cognition.d.ts +12 -0
- package/dist/cognition/cognition.js +57 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/migrate.js +11 -1
- package/dist/person.d.ts +6 -0
- package/dist/person.js +16 -3
- package/dist/prospection/forecast.d.ts +15 -3
- package/dist/prospection/forecast.js +23 -6
- package/dist/restore.js +3 -0
- package/dist/review/index.d.ts +1 -0
- package/dist/review/index.js +1 -0
- package/dist/review/review.d.ts +47 -0
- package/dist/review/review.js +239 -0
- package/dist/types.d.ts +71 -1
- package/dist/types.js +1 -1
- package/dist/will/will.d.ts +39 -0
- package/dist/will/will.js +67 -1
- package/package.json +1 -1
package/dist/body/body.d.ts
CHANGED
|
@@ -125,6 +125,11 @@ export interface BodyPerceptionContext {
|
|
|
125
125
|
fastingDays: number;
|
|
126
126
|
}
|
|
127
127
|
export type BodyParams = typeof BODY_DEFAULTS;
|
|
128
|
+
/**
|
|
129
|
+
* Turn on fullness (2.8.0): consumption for a need already past its comfort level carries a small cost in choice
|
|
130
|
+
* (see the fullness SCOPE in `cognition/`). Idempotent; off by default so existing hosts replay exactly.
|
|
131
|
+
*/
|
|
132
|
+
export declare function enableFullness(p: Person): void;
|
|
128
133
|
/** Turn on the fatigue store (2.6.0): accumulated exertion that recovers with rest and sleep. Idempotent. */
|
|
129
134
|
export declare function enableFatigueStore(p: Person): void;
|
|
130
135
|
export declare function createBody(init: PersonSpec['body'] | undefined, now: Minute): BodyState;
|
package/dist/body/body.js
CHANGED
|
@@ -238,9 +238,18 @@ export function sanitizeBody(b, now) {
|
|
|
238
238
|
delete b.lastSleep;
|
|
239
239
|
if (b.lastDowned !== undefined && !span(b.lastDowned))
|
|
240
240
|
delete b.lastDowned;
|
|
241
|
+
if (b.fullness !== undefined && b.fullness !== true)
|
|
242
|
+
delete b.fullness;
|
|
241
243
|
if (b.depletion !== undefined)
|
|
242
244
|
b.depletion = typeof b.depletion === 'number' && Number.isFinite(b.depletion) ? clamp01(b.depletion) : 0;
|
|
243
245
|
}
|
|
246
|
+
/**
|
|
247
|
+
* Turn on fullness (2.8.0): consumption for a need already past its comfort level carries a small cost in choice
|
|
248
|
+
* (see the fullness SCOPE in `cognition/`). Idempotent; off by default so existing hosts replay exactly.
|
|
249
|
+
*/
|
|
250
|
+
export function enableFullness(p) {
|
|
251
|
+
p.body.fullness = true;
|
|
252
|
+
}
|
|
244
253
|
/** Turn on the fatigue store (2.6.0): accumulated exertion that recovers with rest and sleep. Idempotent. */
|
|
245
254
|
export function enableFatigueStore(p) {
|
|
246
255
|
if (p.body.depletion === undefined)
|
|
@@ -66,6 +66,16 @@ export declare const COGNITION_DEFAULTS: {
|
|
|
66
66
|
commitmentInertiaFade: number;
|
|
67
67
|
/** Social pull per partner on a joint offer: jointTrust × (relationship trust - 0.5). */
|
|
68
68
|
jointTrust: number;
|
|
69
|
+
/**
|
|
70
|
+
* Staying with a partner who is going (2.8.0, `Affordance.alongside`): 'together:<id>' = togetherScale ×
|
|
71
|
+
* closeness. It does not satiate with belonging: being together now does not remove the pull of not being left.
|
|
72
|
+
*/
|
|
73
|
+
togetherScale: number;
|
|
74
|
+
/**
|
|
75
|
+
* Fullness (2.8.0, opt-in `body.fullness`): 'full:<need>' = -fullnessScale × gain × over², over = how far the
|
|
76
|
+
* need's level is past its comfort threshold toward 1, for food, water and relief.
|
|
77
|
+
*/
|
|
78
|
+
fullnessScale: number;
|
|
69
79
|
/** Multiplier on standing advice terms relative to a live suggestion of the same strength (N9). */
|
|
70
80
|
rememberedScale: number;
|
|
71
81
|
/**
|
|
@@ -129,6 +139,8 @@ export interface DecideContext extends ConsiderContext {
|
|
|
129
139
|
quiet?: boolean;
|
|
130
140
|
/** A host command in force (1.6.0); passed to the will. */
|
|
131
141
|
command?: Command;
|
|
142
|
+
/** Drop the running activity's inertia bonus (2.8.0); passed to the will. */
|
|
143
|
+
reconsider?: boolean;
|
|
132
144
|
}
|
|
133
145
|
export interface Decision {
|
|
134
146
|
record: DecisionRecord;
|
|
@@ -23,6 +23,17 @@
|
|
|
23
23
|
* bias; and because losses are amplified too, a host must carry the benefit of paying a debt (a commitment,
|
|
24
24
|
* an advertised `safety` gain) or paying looks only like a loss. The spec's "security need" is read as the
|
|
25
25
|
* `safety` need: `security` is a value, not a need, and no need is added.
|
|
26
|
+
*
|
|
27
|
+
* SCOPE (2.8.0, both additive): an offer naming `alongside` partners (people already taking that course) gets one
|
|
28
|
+
* `together:<id>` term per known, living partner, `togetherScale` × closeness, without belonging satiation: the
|
|
29
|
+
* pull is not to be left behind, which stays when the pair is together and belonging is full (The Rope's resting
|
|
30
|
+
* partner, 2026-10-18 reproduction: belonging terms near 0.01 against inertia of 0.07-0.15). Shape only, after
|
|
31
|
+
* joint commitment and the sense of commitment in joint action (Gilbert 1990; Michael and Pacherie 2015; cited unaudited, research/empirical-models.md §6b); it does
|
|
32
|
+
* not model who leads, negotiation, or obligations between the partners. With opt-in `body.fullness`, an offer
|
|
33
|
+
* that advertises food, water or relief while that need is past its comfort threshold gets `full:<need>` =
|
|
34
|
+
* -fullnessScale × gain × over²: eating or drinking when full is mildly unpleasant (negative alliesthesia, unaudited citation, Cabanac
|
|
35
|
+
* 1971), so idle choice stops cycling short consumption at full hydration (The Rope, owner Kestrel export
|
|
36
|
+
* 2026-10-19). It models no bladder, sensory-specific satiety by food, or overeating for comfort.
|
|
26
37
|
*/
|
|
27
38
|
import { emotionLevel, inBreak } from '../affect/index.js';
|
|
28
39
|
import { agendaTermsWithGoalEstimates } from '../agenda/index.js';
|
|
@@ -32,11 +43,12 @@ import { clamp01, cmpStr, dexp, minuteOfDay, round } from '../core/index.js';
|
|
|
32
43
|
import { habitEase, habitPull } from '../habits/index.js';
|
|
33
44
|
import { expectedEffect } from '../memory/index.js';
|
|
34
45
|
import { methodEvidence } from '../methods/index.js';
|
|
35
|
-
import { urgencyOf } from '../needs/index.js';
|
|
46
|
+
import { levelOf, needThreshold, urgencyOf } from '../needs/index.js';
|
|
36
47
|
import { partneringTerms } from '../partnering/index.js';
|
|
37
48
|
import { promptIn, promptingTerms } from '../promptings/index.js';
|
|
38
49
|
import { bodyForecastTerm, receivedGoalRoute } from '../prospection/index.js';
|
|
39
|
-
import {
|
|
50
|
+
import { reviewTerms } from '../review/index.js';
|
|
51
|
+
import { closeness, companionSteadiness, isDeceasedTie, relationshipWith, socialTerms, } from '../social/index.js';
|
|
40
52
|
import { PHYSIOLOGICAL_NEEDS, VALUE_KEYS } from '../types.js';
|
|
41
53
|
import { adviceWeight, resolveChoice, safeUtility, standingAdvice, suggestionTargets, voicesIn, } from '../will/index.js';
|
|
42
54
|
export const COGNITION_DEFAULTS = {
|
|
@@ -102,6 +114,16 @@ export const COGNITION_DEFAULTS = {
|
|
|
102
114
|
commitmentInertiaFade: 0.15,
|
|
103
115
|
/** Social pull per partner on a joint offer: jointTrust × (relationship trust - 0.5). */
|
|
104
116
|
jointTrust: 0.6,
|
|
117
|
+
/**
|
|
118
|
+
* Staying with a partner who is going (2.8.0, `Affordance.alongside`): 'together:<id>' = togetherScale ×
|
|
119
|
+
* closeness. It does not satiate with belonging: being together now does not remove the pull of not being left.
|
|
120
|
+
*/
|
|
121
|
+
togetherScale: 0.12,
|
|
122
|
+
/**
|
|
123
|
+
* Fullness (2.8.0, opt-in `body.fullness`): 'full:<need>' = -fullnessScale × gain × over², over = how far the
|
|
124
|
+
* need's level is past its comfort threshold toward 1, for food, water and relief.
|
|
125
|
+
*/
|
|
126
|
+
fullnessScale: 0.03,
|
|
105
127
|
// --- lane will+cognition ---
|
|
106
128
|
/** Multiplier on standing advice terms relative to a live suggestion of the same strength (N9). */
|
|
107
129
|
rememberedScale: 1,
|
|
@@ -225,6 +247,8 @@ export function materialValue(p, material, scarcity) {
|
|
|
225
247
|
const sat = Math.sign(material) * (1 - dexp(-Math.abs(material) / scale));
|
|
226
248
|
return K.materialWeight * sat * 2 * weight * (1 + K.scarcityMaterialGain * scarcity);
|
|
227
249
|
}
|
|
250
|
+
/** Needs whose consumption can overfill (2.8.0 fullness): what `body.consume` feeds. */
|
|
251
|
+
const FULL_NEEDS = ['food', 'water', 'relief'];
|
|
228
252
|
/** Score one option. Pure apart from reads; terms are rounded for stable snapshots. */
|
|
229
253
|
export function consider(p, aff, ctx) {
|
|
230
254
|
const K = COGNITION_DEFAULTS;
|
|
@@ -247,6 +271,19 @@ export function consider(p, aff, ctx) {
|
|
|
247
271
|
const scarce = id === 'safety' ? 1 + K.scarcitySafetyGain * scarcity : 1;
|
|
248
272
|
push(`need:${id}`, K.needScale * u * gain * success * scarce);
|
|
249
273
|
}
|
|
274
|
+
// Fullness (2.8.0, opt-in): consuming for a need already past comfort is mildly unpleasant (alliesthesia).
|
|
275
|
+
if (p.body.fullness) {
|
|
276
|
+
for (const id of FULL_NEEDS) {
|
|
277
|
+
const gain = aff.advertises[id] ?? 0;
|
|
278
|
+
if (!(gain > 0))
|
|
279
|
+
continue;
|
|
280
|
+
const level = levelOf(ctx.needs, id);
|
|
281
|
+
const comfort = needThreshold(p, id);
|
|
282
|
+
const over = clamp01((level - comfort) / Math.max(1e-6, 1 - comfort));
|
|
283
|
+
if (over > 0)
|
|
284
|
+
push(`full:${id}`, -K.fullnessScale * Math.min(1, gain) * over * over);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
250
287
|
if (expected.samples > 0) {
|
|
251
288
|
let relevance = 1;
|
|
252
289
|
if (expected.valence > 0) {
|
|
@@ -323,6 +360,16 @@ export function consider(p, aff, ctx) {
|
|
|
323
360
|
// Social pulls. Per-person emotion tendencies enter at the same scale as tag tendencies.
|
|
324
361
|
for (const t of socialTerms(p, aff, social))
|
|
325
362
|
push(t.source, K.socialScale * (t.value > 0 ? social.satiation * refractory() : 1) * t.value);
|
|
363
|
+
// Going with partners who are already taking this course (2.8.0).
|
|
364
|
+
if (aff.alongside) {
|
|
365
|
+
for (const id of [...new Set(aff.alongside)].sort()) {
|
|
366
|
+
if (id === p.id || !p.social.relationships.some((r) => r.otherId === id))
|
|
367
|
+
continue;
|
|
368
|
+
if (isDeceasedTie(relationshipWith(p, id)))
|
|
369
|
+
continue;
|
|
370
|
+
push(`together:${id}`, K.togetherScale * closeness(p, id));
|
|
371
|
+
}
|
|
372
|
+
}
|
|
326
373
|
// Joint offers: whether I trust the partners to do their share.
|
|
327
374
|
if (tags.includes('joint')) {
|
|
328
375
|
for (const id of [...(aff.with ?? [])].sort()) {
|
|
@@ -409,6 +456,10 @@ export function consider(p, aff, ctx) {
|
|
|
409
456
|
if (p.promptings)
|
|
410
457
|
for (const t of promptingTerms(p, aff.action, terms, aff.tags, ctx.now, ctx.desperation))
|
|
411
458
|
push(t.source, t.value);
|
|
459
|
+
// Evening review (2.7.0, opt-in): a resolve formed after a lapse leans on that commitment's own term.
|
|
460
|
+
if (p.review)
|
|
461
|
+
for (const t of reviewTerms(p, terms))
|
|
462
|
+
push(t.source, t.value);
|
|
412
463
|
let utility = 0;
|
|
413
464
|
for (const t of terms)
|
|
414
465
|
utility += t.value;
|
|
@@ -448,6 +499,8 @@ export function scoreAll(p, affordances, ctx) {
|
|
|
448
499
|
};
|
|
449
500
|
if (ctx.command)
|
|
450
501
|
willCtx.command = ctx.command;
|
|
502
|
+
if (ctx.reconsider)
|
|
503
|
+
willCtx.reconsider = true;
|
|
451
504
|
return { sorted, considered, willCtx };
|
|
452
505
|
}
|
|
453
506
|
/**
|
|
@@ -474,6 +527,8 @@ export function decide(p, affordances, ctx) {
|
|
|
474
527
|
};
|
|
475
528
|
if (res.suggestion)
|
|
476
529
|
record.suggestion = res.suggestion;
|
|
530
|
+
if (ctx.reconsider)
|
|
531
|
+
record.reconsider = true;
|
|
477
532
|
if (res.suggestions && (res.suggestions.length > 1 || ctx.suggestions !== undefined))
|
|
478
533
|
record.suggestions = res.suggestions;
|
|
479
534
|
const needDeltas = {};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Human Framework v1 public surface. */
|
|
2
|
-
export declare const FRAMEWORK_VERSION = "2.
|
|
2
|
+
export declare const FRAMEWORK_VERSION = "2.8.0";
|
|
3
3
|
export type { CrisisEvent, PracticeKind } from './affect/index.ts';
|
|
4
4
|
export { AFFECT_DEFAULTS, actionTendencies, advanceAffect, appraise, breakAllows, breakBehaviour, breakHazard, CRISIS_DEFAULTS, checkCrisis, createAffect, easeBreak, emotionLevel, enableBreaks, feel, inBreak, readAffect, regulate, release as releaseEmotion, skipCrisis, strain, tendencyEmotions, } from './affect/index.ts';
|
|
5
5
|
export type { GoalTemplate, PrayerCalendar, PrayerSchedule, PrayerTimes, Retimer, } from './agenda/index.ts';
|
|
@@ -30,8 +30,9 @@ export * from './partnering/index.ts';
|
|
|
30
30
|
export * from './person.ts';
|
|
31
31
|
export * from './promptings/index.ts';
|
|
32
32
|
export type { ConsequenceEvidence } from './prospection/index.ts';
|
|
33
|
-
export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, FORECAST_DEFAULTS, forecastLine, PROSPECTION_DEFAULTS, } from './prospection/index.ts';
|
|
33
|
+
export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, FORECAST_DEFAULTS, forecastLine, forecastThresholds, PROSPECTION_DEFAULTS, } from './prospection/index.ts';
|
|
34
34
|
export * from './restore.ts';
|
|
35
|
+
export * from './review/index.ts';
|
|
35
36
|
export * from './scenarios/index.ts';
|
|
36
37
|
export * from './sim/index.ts';
|
|
37
38
|
export * from './skills/index.ts';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Human Framework v1 public surface. */
|
|
2
|
-
export const FRAMEWORK_VERSION = '2.
|
|
2
|
+
export const FRAMEWORK_VERSION = '2.8.0';
|
|
3
3
|
export { AFFECT_DEFAULTS, actionTendencies, advanceAffect, appraise, breakAllows, breakBehaviour, breakHazard, CRISIS_DEFAULTS, checkCrisis, createAffect, easeBreak, emotionLevel, enableBreaks, feel, inBreak, readAffect, regulate, release as releaseEmotion, skipCrisis, strain, tendencyEmotions, } from './affect/index.js';
|
|
4
4
|
// `agenda.release` collides with `affect.release`; the agenda one is re-exported under a clearer name.
|
|
5
5
|
export { AGENDA_DEFAULTS, abandonGoal, adoptGoal, advanceAgenda, agendaTerms, applyExemptions, CARE_DEFAULTS, calendarRetimer, careDuty, commitmentPressure, createAgenda, DEFAULT_PRAYER_TIMES, EID_PRAYER_IMPORTANCE, EXEMPTION_DEFAULTS, eidPrayer, eidWindow, fastWindow, iftarWindow, inMakruhTime, isUnderWay, MAKRUH_DEFAULTS, makruhWindows, missedExcuse, onFinished, owedMakeUps, PRAYER_IMPORTANCE, PURPOSE_DEFAULTS, prayerWindows, pressureReachedAt, promise, proposeGoals, RAMADAN_DEFAULTS, ramadanFast, ramadanMeals, release as releaseCommitment, retimeCommitments, revisePurposes, scheduleMakeUp, spanMeetsWindow, suhoorWindow, timesFor, violatesAbstention, } from './agenda/index.js';
|
|
@@ -26,8 +26,9 @@ export * from './needs/index.js';
|
|
|
26
26
|
export * from './partnering/index.js';
|
|
27
27
|
export * from './person.js';
|
|
28
28
|
export * from './promptings/index.js';
|
|
29
|
-
export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, FORECAST_DEFAULTS, forecastLine, PROSPECTION_DEFAULTS, } from './prospection/index.js';
|
|
29
|
+
export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, FORECAST_DEFAULTS, forecastLine, forecastThresholds, PROSPECTION_DEFAULTS, } from './prospection/index.js';
|
|
30
30
|
export * from './restore.js';
|
|
31
|
+
export * from './review/index.js';
|
|
31
32
|
export * from './scenarios/index.js';
|
|
32
33
|
export * from './sim/index.js';
|
|
33
34
|
export * from './skills/index.js';
|
package/dist/migrate.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* field of the optional surroundings slice (`ambient.now` → `ambient.percept`) and changes no behaviour (the optional `social.impressions` and `social.reserve`, like `retention`, are optional and start
|
|
9
9
|
* absent in a migrated save); `restore` then fills and sanitizes
|
|
10
10
|
* the slices as for any save. 2.0.0 to 2.1.0 adds optional conditional method state, absent
|
|
11
|
-
* until explicitly used, so existing hosts retain their previous behavior; that step stamps the version. 2.1.0 to 2.2.0 preserves optional method records and stamps the version; successful alternatives now retain independent support. 2.2.0 to 2.3.0 stamps the version; received consequences and temporal goals remain absent unless explicitly supplied. 2.3.0 to 2.4.0 stamps the version; promptings remain absent unless enabled. 2.4.0 to 2.5.0 stamps the version; the bodily forecast reads only offers that declare `followedBy`. 2.5.0 to 2.6.0 stamps the version; the fatigue store stays absent unless enabled. A migrated save continues under the current engine's rules: it restores and runs,
|
|
11
|
+
* until explicitly used, so existing hosts retain their previous behavior; that step stamps the version. 2.1.0 to 2.2.0 preserves optional method records and stamps the version; successful alternatives now retain independent support. 2.2.0 to 2.3.0 stamps the version; received consequences and temporal goals remain absent unless explicitly supplied. 2.3.0 to 2.4.0 stamps the version; promptings remain absent unless enabled. 2.4.0 to 2.5.0 stamps the version; the bodily forecast reads only offers that declare `followedBy`. 2.5.0 to 2.6.0 stamps the version; the fatigue store stays absent unless enabled. 2.6.0 to 2.7.0 stamps the version; the evening review stays absent unless enabled. A migrated save continues under the current engine's rules: it restores and runs,
|
|
12
12
|
* but it does not reproduce what the old engine would have done next where the rules changed (1.4.0 to 1.5.0
|
|
13
13
|
* changed how standing advice is heard; 1.8.0 to 1.9.0 changed the omission rule). Saves older than 1.4.0 and unknown versions are refused with an error.
|
|
14
14
|
*
|
|
@@ -58,6 +58,16 @@ export const MIGRATIONS = {
|
|
|
58
58
|
note: 'the surroundings field `ambient.now` is renamed `ambient.percept`; per-person retention is optional and starts absent; behaviour unchanged',
|
|
59
59
|
apply: renameAmbient,
|
|
60
60
|
},
|
|
61
|
+
'2.7.0': {
|
|
62
|
+
to: '2.8.0',
|
|
63
|
+
note: 'fullness (body.fullness) is optional and starts absent; offers may name alongside partners and decisions may reconsider; existing hosts retain exact behavior',
|
|
64
|
+
apply: stamp,
|
|
65
|
+
},
|
|
66
|
+
'2.6.0': {
|
|
67
|
+
to: '2.7.0',
|
|
68
|
+
note: 'the evening review (review) is optional and starts absent; existing hosts retain exact behavior',
|
|
69
|
+
apply: stamp,
|
|
70
|
+
},
|
|
61
71
|
'2.5.0': {
|
|
62
72
|
to: '2.6.0',
|
|
63
73
|
note: "the fatigue store (body.depletion) is optional and starts absent; the bodily forecast now applies an open fast's hunger damping, which changes only offers that declare followedBy",
|
package/dist/person.d.ts
CHANGED
|
@@ -103,6 +103,12 @@ export interface DecideOptions {
|
|
|
103
103
|
necessity?: boolean;
|
|
104
104
|
/** Why this decision was forced (recorded on the decision); defaults to the activity's pending interrupt. */
|
|
105
105
|
interrupt?: string;
|
|
106
|
+
/**
|
|
107
|
+
* Reconsider on the merits (2.8.0): the running activity gets no inertia bonus in this decision, for a review the
|
|
108
|
+
* host forces because something changed (a partner leaving, a call). Promise inertia (`begin` with a promise) and
|
|
109
|
+
* every other term stay. Absent = the usual hysteresis.
|
|
110
|
+
*/
|
|
111
|
+
reconsider?: boolean;
|
|
106
112
|
}
|
|
107
113
|
/**
|
|
108
114
|
* How this person would answer a suggestion right now, without changing them or consuming randomness
|
package/dist/person.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
import { actionTendencies, advanceAffect, appraise, CRISIS_DEFAULTS, checkCrisis, createAffect, easeBreak, emotionLevel, feel, readAffect, regulate, release, skipAffect, skipCrisis, strain, tendencyEmotions, } from './affect/index.js';
|
|
23
23
|
import { advanceAgenda, createAgenda, onFinished, promise, proposeGoals, revisePurposes, } from './agenda/index.js';
|
|
24
24
|
import { advanceBeliefs, attend, believe, confirm, credence } from './beliefs/index.js';
|
|
25
|
-
import { advanceBody, BODY_DEFAULTS, checkDowned, clearDowned, consume, createBody, downedAllows, enableFatigueStore, expose, injure, nextBodyThreshold, readBody, setDowned, sicken, skipBody, } from './body/index.js';
|
|
25
|
+
import { advanceBody, BODY_DEFAULTS, checkDowned, clearDowned, consume, createBody, downedAllows, enableFatigueStore, enableFullness, expose, injure, nextBodyThreshold, readBody, setDowned, sicken, skipBody, } from './body/index.js';
|
|
26
26
|
import { ageCharacter, enableCharacterChange, noteCharacterDay, noteCharacterSocial, } from './character/index.js';
|
|
27
27
|
import { closeDay, enableYearbook, endDay, noteAnswer, noteCommitments, noteDecision, noteMood, noteOutcome, trimChronicle, } from './chronicle/index.js';
|
|
28
28
|
import { decide as cognitionDecide, desperationOf, scoreAll } from './cognition/index.js';
|
|
@@ -37,7 +37,8 @@ import { intentionFor, narrateDecision, voiceLine } from './narrate/index.js';
|
|
|
37
37
|
import { advanceNeeds, createNeeds, levelOf, meanSatisfaction, readNeeds, satisfy, urgencyOf, } from './needs/index.js';
|
|
38
38
|
import { widow } from './partnering/index.js';
|
|
39
39
|
import { enablePromptings, nafsPatterns, notePromptings } from './promptings/index.js';
|
|
40
|
-
import { bodyForecastFor, consumeConsequences, forecastLine, pruneConsequences, receiveConsequences, } from './prospection/index.js';
|
|
40
|
+
import { bodyForecastFor, consumeConsequences, forecastLine, forecastThresholds, pruneConsequences, receiveConsequences, } from './prospection/index.js';
|
|
41
|
+
import { enableReview, reviewIfDue } from './review/index.js';
|
|
41
42
|
import { enableSkillRetention, learnByWatching, practise, seedSkills, } from './skills/index.js';
|
|
42
43
|
import { advanceSocial, applyReputationBelief, careFor, closeness, judge, markDeceased, relationshipWith, seedRelationships, socialEvent, threatAppraisal, } from './social/index.js';
|
|
43
44
|
import { ENGINE_VERSION, MAX_MINUTE, PERSON_SCHEMA, PHYSIOLOGICAL_NEEDS, PSYCHOLOGICAL_NEEDS, TRAIT_KEYS, VALUE_KEYS, } from './types.js';
|
|
@@ -156,8 +157,12 @@ export function createPerson(spec) {
|
|
|
156
157
|
enableSkillRetention(p);
|
|
157
158
|
if (on?.fatigueStore)
|
|
158
159
|
enableFatigueStore(p);
|
|
160
|
+
if (on?.fullness)
|
|
161
|
+
enableFullness(p);
|
|
159
162
|
if (on?.promptings)
|
|
160
163
|
enablePromptings(p, on.promptings);
|
|
164
|
+
if (on?.review)
|
|
165
|
+
enableReview(p, on.review);
|
|
161
166
|
return p;
|
|
162
167
|
}
|
|
163
168
|
/**
|
|
@@ -418,6 +423,12 @@ export function tick(p, now) {
|
|
|
418
423
|
closed = closeDay(p);
|
|
419
424
|
applyRecall(p, recallByCue(p, { at: end }));
|
|
420
425
|
}
|
|
426
|
+
// Evening review (2.7.0, opt-in): from its hour, or at the close of a day that passed without one.
|
|
427
|
+
if (p.review) {
|
|
428
|
+
const done = reviewIfDue(p, end, (p.chronicle ?? []).slice(-2), closed);
|
|
429
|
+
if (done && done.distress > 0)
|
|
430
|
+
feel(p, 'distress', done.distress, 'review', end);
|
|
431
|
+
}
|
|
421
432
|
if (minuteOfDay(end) === 0) {
|
|
422
433
|
// Lasting gists (1.8.0, opt-in): at midnight, episodes past the horizon fold into gists.
|
|
423
434
|
if (p.memory.gists)
|
|
@@ -747,6 +758,8 @@ function decisionInputs(p, affordances, id, opts) {
|
|
|
747
758
|
const cmd = opts.command ?? p.will.command;
|
|
748
759
|
if (cmd)
|
|
749
760
|
ctx.command = commandOf(cmd);
|
|
761
|
+
if (opts.reconsider)
|
|
762
|
+
ctx.reconsider = true;
|
|
750
763
|
return { list, ctx };
|
|
751
764
|
}
|
|
752
765
|
/** The `Command` part of a command in force (without the will's bookkeeping). */
|
|
@@ -1421,5 +1434,5 @@ export function nafsReading(p) {
|
|
|
1421
1434
|
*/
|
|
1422
1435
|
export function bodyForecast(p, aff) {
|
|
1423
1436
|
const spans = bodyForecastFor(p, aff, lifeModifiers(p), fastingCtx(p));
|
|
1424
|
-
return { spans, line: forecastLine(spans) };
|
|
1437
|
+
return { spans, line: forecastLine(spans, forecastThresholds(aff)) };
|
|
1425
1438
|
}
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
* open wound can read as being spent, and death in the forecast reads as fully spent. The term is absolute
|
|
17
17
|
* (see `bodyForecastTerm`). It does not plan, search routes, schedule rests on the way or
|
|
18
18
|
* predict injuries, and it is not a calibrated estimate of human anticipation. Every number is an engineering
|
|
19
|
-
* default.
|
|
19
|
+
* default. 2.8.0: an offer may carry its own thresholds (`Affordance.forecastAt`, from The Glance's night
|
|
20
|
+
* keepers); offers without it read exactly as before.
|
|
20
21
|
*/
|
|
21
22
|
import { type BodyForecastSpan, type BodyPerceptionContext } from '../body/index.ts';
|
|
22
23
|
import type { Affordance, LifeModifiers, Person, Term } from '../types.ts';
|
|
@@ -31,6 +32,14 @@ export declare const FORECAST_DEFAULTS: {
|
|
|
31
32
|
readonly maxSpans: 4;
|
|
32
33
|
readonly maxMinutes: number;
|
|
33
34
|
};
|
|
35
|
+
/**
|
|
36
|
+
* The forecast thresholds for an offer (2.8.0): its `forecastAt` when it forms a valid pair, else the defaults.
|
|
37
|
+
* Pure.
|
|
38
|
+
*/
|
|
39
|
+
export declare function forecastThresholds(aff: Pick<Affordance, 'forecastAt'>): {
|
|
40
|
+
comfortAt: number;
|
|
41
|
+
spentAt: number;
|
|
42
|
+
};
|
|
34
43
|
/** The person's forecast for an offer: the act, then its follow-on spans. Empty without `followedBy`. Pure. */
|
|
35
44
|
export declare function bodyForecastFor(p: Person, aff: Affordance, mods: LifeModifiers, perception?: BodyPerceptionContext): BodyForecastSpan[];
|
|
36
45
|
/**
|
|
@@ -41,5 +50,8 @@ export declare function bodyForecastFor(p: Person, aff: Affordance, mods: LifeMo
|
|
|
41
50
|
* too, which cancelled the very case the forecast exists for. Pure.
|
|
42
51
|
*/
|
|
43
52
|
export declare function bodyForecastTerm(p: Person, aff: Affordance, mods: LifeModifiers, perception?: BodyPerceptionContext): Term | undefined;
|
|
44
|
-
/** A first-person line for the forecast (narration only). */
|
|
45
|
-
export declare function forecastLine(spans: readonly BodyForecastSpan[]
|
|
53
|
+
/** A first-person line for the forecast (narration only); `at` is the offer's thresholds (2.8.0, default as before). */
|
|
54
|
+
export declare function forecastLine(spans: readonly BodyForecastSpan[], at?: {
|
|
55
|
+
comfortAt: number;
|
|
56
|
+
spentAt: number;
|
|
57
|
+
}): string;
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
* open wound can read as being spent, and death in the forecast reads as fully spent. The term is absolute
|
|
17
17
|
* (see `bodyForecastTerm`). It does not plan, search routes, schedule rests on the way or
|
|
18
18
|
* predict injuries, and it is not a calibrated estimate of human anticipation. Every number is an engineering
|
|
19
|
-
* default.
|
|
19
|
+
* default. 2.8.0: an offer may carry its own thresholds (`Affordance.forecastAt`, from The Glance's night
|
|
20
|
+
* keepers); offers without it read exactly as before.
|
|
20
21
|
*/
|
|
21
22
|
import { BODY_DEFAULTS, forecastBody, } from '../body/index.js';
|
|
22
23
|
import { clamp01, round } from '../core/index.js';
|
|
@@ -31,6 +32,21 @@ export const FORECAST_DEFAULTS = {
|
|
|
31
32
|
maxSpans: 4,
|
|
32
33
|
maxMinutes: 24 * 60,
|
|
33
34
|
};
|
|
35
|
+
/**
|
|
36
|
+
* The forecast thresholds for an offer (2.8.0): its `forecastAt` when it forms a valid pair, else the defaults.
|
|
37
|
+
* Pure.
|
|
38
|
+
*/
|
|
39
|
+
export function forecastThresholds(aff) {
|
|
40
|
+
const D = FORECAST_DEFAULTS;
|
|
41
|
+
const at = aff.forecastAt;
|
|
42
|
+
if (!at || typeof at !== 'object')
|
|
43
|
+
return { comfortAt: D.comfortAt, spentAt: D.spentAt };
|
|
44
|
+
const c = Number.isFinite(at.comfortAt) ? at.comfortAt : D.comfortAt;
|
|
45
|
+
const s = Number.isFinite(at.spentAt) ? at.spentAt : D.spentAt;
|
|
46
|
+
return c >= 0 && c < s && s <= 1
|
|
47
|
+
? { comfortAt: c, spentAt: s }
|
|
48
|
+
: { comfortAt: D.comfortAt, spentAt: D.spentAt };
|
|
49
|
+
}
|
|
34
50
|
const smooth = (a, b, x) => {
|
|
35
51
|
const t = clamp01((x - a) / (b - a));
|
|
36
52
|
return t * t * (3 - 2 * t);
|
|
@@ -81,12 +97,13 @@ export function bodyForecastTerm(p, aff, mods, perception) {
|
|
|
81
97
|
if (spans.length === 0)
|
|
82
98
|
return undefined;
|
|
83
99
|
const D = FORECAST_DEFAULTS;
|
|
100
|
+
const at = forecastThresholds(aff);
|
|
84
101
|
const worst = Math.max(...spans.map((s) => s.worst));
|
|
85
|
-
const v = round(-D.forecastScale * smooth(
|
|
102
|
+
const v = round(-D.forecastScale * smooth(at.comfortAt, at.spentAt, worst));
|
|
86
103
|
return v < 0 ? { source: 'forecast:body', value: v } : undefined;
|
|
87
104
|
}
|
|
88
|
-
/** A first-person line for the forecast (narration only). */
|
|
89
|
-
export function forecastLine(spans) {
|
|
105
|
+
/** A first-person line for the forecast (narration only); `at` is the offer's thresholds (2.8.0, default as before). */
|
|
106
|
+
export function forecastLine(spans, at = FORECAST_DEFAULTS) {
|
|
90
107
|
const after = spans.slice(1);
|
|
91
108
|
if (after.length === 0)
|
|
92
109
|
return '';
|
|
@@ -95,9 +112,9 @@ export function forecastLine(spans) {
|
|
|
95
112
|
if (s.worst > w.worst)
|
|
96
113
|
w = s;
|
|
97
114
|
const what = { fatigue: 'spent', sleepiness: 'too sleepy', hunger: 'too hungry', thirst: 'too thirsty' }[w.worstOf];
|
|
98
|
-
if (w.worst >=
|
|
115
|
+
if (w.worst >= at.spentAt)
|
|
99
116
|
return `I can do it, but I'll be ${what} for ${w.label}.`;
|
|
100
|
-
if (w.worst >=
|
|
117
|
+
if (w.worst >= at.comfortAt)
|
|
101
118
|
return `I can do it, though ${w.label} will be hard.`;
|
|
102
119
|
return `I'll still have enough for ${w.label}.`;
|
|
103
120
|
}
|
package/dist/restore.js
CHANGED
|
@@ -22,6 +22,7 @@ import { sanitizeBonds } from './partnering/index.js';
|
|
|
22
22
|
import { cleanRetention, createPerson, traceLimit } from './person.js';
|
|
23
23
|
import { sanitizePromptings } from './promptings/index.js';
|
|
24
24
|
import { sanitizeProspection } from './prospection/index.js';
|
|
25
|
+
import { sanitizeReview } from './review/index.js';
|
|
25
26
|
import { sanitizeSkillRetention } from './skills/index.js';
|
|
26
27
|
import { sanitizeGroups, sanitizeImpressions } from './social/index.js';
|
|
27
28
|
import { ENGINE_VERSION, MAX_MINUTE, PERSON_SCHEMA } from './types.js';
|
|
@@ -63,6 +64,7 @@ const OPTIONAL_KEYS = new Set([
|
|
|
63
64
|
'methods',
|
|
64
65
|
'prospection',
|
|
65
66
|
'promptings',
|
|
67
|
+
'review',
|
|
66
68
|
]);
|
|
67
69
|
/** Kind of a JSON value for the per-field type check in `restore`. */
|
|
68
70
|
const kindOf = (x) => (Array.isArray(x) ? 'array' : x === null ? 'null' : typeof x);
|
|
@@ -177,6 +179,7 @@ export function restore(input) {
|
|
|
177
179
|
keepValid(out, 'methods', sanitizeMethods);
|
|
178
180
|
keepValid(out, 'prospection', (x) => sanitizeProspection(x, out.now, out.id));
|
|
179
181
|
keepValid(out, 'promptings', (x) => sanitizePromptings(x, out.now));
|
|
182
|
+
keepValid(out, 'review', (x) => sanitizeReview(x, out.now));
|
|
180
183
|
boundLists(out);
|
|
181
184
|
return out;
|
|
182
185
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './review.ts';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './review.js';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { DayRecord, Minute, Person, ReviewRecord, ReviewSpec, ReviewState, ReviewStyle, Term } from '../types.ts';
|
|
2
|
+
export declare const REVIEW_DEFAULTS: {
|
|
3
|
+
/** Default evening review time, minute of day. */
|
|
4
|
+
at: number;
|
|
5
|
+
/** Largest `review:resolve` term at full strength. */
|
|
6
|
+
resolveScale: number;
|
|
7
|
+
/** Share of the remaining headroom a lapse adds, by style. */
|
|
8
|
+
gain: Record<ReviewStyle, number>;
|
|
9
|
+
/** Strength kept after the commitment is kept. */
|
|
10
|
+
keptKeep: number;
|
|
11
|
+
halfLifeDays: number;
|
|
12
|
+
/** Resolves weaker than this are dropped. */
|
|
13
|
+
dropBelow: number;
|
|
14
|
+
maxResolves: number;
|
|
15
|
+
maxSeen: number;
|
|
16
|
+
/** Distress felt per lapse by a ruminating reviewer, capped at `ruminateMax`. */
|
|
17
|
+
ruminatePer: number;
|
|
18
|
+
ruminateMax: number;
|
|
19
|
+
};
|
|
20
|
+
/** Turn on the evening review. Idempotent; a second call updates style, source and time but keeps resolves. */
|
|
21
|
+
export declare function enableReview(p: Person, spec?: ReviewSpec): ReviewState;
|
|
22
|
+
/** The key a commitment and its chronicle note share: kind, owed-to, and label (else first action). */
|
|
23
|
+
export declare const resolveKey: (c: {
|
|
24
|
+
label?: string | undefined;
|
|
25
|
+
kind: string;
|
|
26
|
+
action?: string | undefined;
|
|
27
|
+
actions?: readonly string[];
|
|
28
|
+
toId?: string | undefined;
|
|
29
|
+
}) => string;
|
|
30
|
+
/**
|
|
31
|
+
* The `review:resolve` terms for an option, read from its already-traced `commitment:*` and `abstain:*` terms.
|
|
32
|
+
* Pure; empty when review is off or no resolve matches.
|
|
33
|
+
*/
|
|
34
|
+
export declare function reviewTerms(p: Person, terms: readonly Term[]): Term[];
|
|
35
|
+
/**
|
|
36
|
+
* Review now if it is due: at or after `at` on a day not yet reviewed, or when `closed` (the day just closed)
|
|
37
|
+
* was never reviewed. Reads the notes of `recent` (the latest closed day records) and the open day. Returns the
|
|
38
|
+
* record, or undefined when nothing ran. Distress for a ruminating reviewer is returned for the composite to feel.
|
|
39
|
+
*/
|
|
40
|
+
export declare function reviewIfDue(p: Person, now: Minute, recent: readonly DayRecord[], closed?: DayRecord): {
|
|
41
|
+
record: ReviewRecord;
|
|
42
|
+
distress: number;
|
|
43
|
+
} | undefined;
|
|
44
|
+
/** The person's own words for their latest review, or undefined when they hold no review. */
|
|
45
|
+
export declare function reviewLine(p: Person): string | undefined;
|
|
46
|
+
/** Restore guard: drop malformed fields, keep valid resolves. */
|
|
47
|
+
export declare function sanitizeReview(x: unknown, now?: Minute): ReviewState | undefined;
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCOPE (evening self-review, muhasaba, 2.7.0): once a day, from `at` (default 21:00), a person looks back over
|
|
3
|
+
* the commitments the chronicle recorded as kept or broken since their last review, and for each one they broke
|
|
4
|
+
* forms or renews a *resolve*: an if–then intention for that commitment's next occasion (Gollwitzer & Sheeran
|
|
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`), with the sign of the commitment's own term (toward a duty, away from an abstention's breaking
|
|
7
|
+
* act), scaled by its strength. Strength grows with diminishing gains at each further lapse, relaxes once the
|
|
8
|
+
* commitment is kept, and fades over `halfLifeDays` of disuse. Released and excused commitments (exemptions,
|
|
9
|
+
* sleep, necessity) form no resolve: what was beyond capacity is not held against the person (2:286). The review
|
|
10
|
+
* also runs when the day closes, if the evening passed without it or a lapse came after it.
|
|
11
|
+
*
|
|
12
|
+
* Held readings (`ReviewStyle`): `accounting` is self-accounting as the tradition describes it, naming the lapse
|
|
13
|
+
* with a view to tomorrow (Q 59:18, whose "tomorrow" most exegetes read as the hereafter, so a daily review is an
|
|
14
|
+
* inference; ʿUmar's saying reported by Tirmidhi after 2459, an athar, not a prophetic
|
|
15
|
+
* hadith) and turning back without despair (3:135, 39:53); it forms full resolves and adds no emotion. Reading accounting as
|
|
16
|
+
* non-punishing, like self-compassion after failure raising the motivation to repair (Breines & Chen 2012, E3),
|
|
17
|
+
* is an engineering mapping, not a finding about muhasaba. `ruminating` goes over
|
|
18
|
+
* the lapse abstractly: weaker resolves and a small distress emotion (Watkins 2008, E4: abstract, unconstructive
|
|
19
|
+
* repetitive thought). `none` never reviews. The review reads only chronicle notes and changes no other module's
|
|
20
|
+
* state except that one emotion.
|
|
21
|
+
*
|
|
22
|
+
* Every number is an engineering default, not fitted. Does not claim: divine reckoning or acceptance, a measure of
|
|
23
|
+
* piety, repentance as a ruling (tawba conditions are not modelled), effects on sleep onset (Harvey's model, E6, is
|
|
24
|
+
* not evidence enough), or that review improves outcomes beyond the borrowed if–then shape. It draws no randomness;
|
|
25
|
+
* without `enableReview` nothing here runs and existing runs replay byte for byte.
|
|
26
|
+
*/
|
|
27
|
+
import { clamp01, dexp, isObj, minuteOfDay, round } from '../core/index.js';
|
|
28
|
+
export const REVIEW_DEFAULTS = {
|
|
29
|
+
/** Default evening review time, minute of day. */
|
|
30
|
+
at: 21 * 60,
|
|
31
|
+
/** Largest `review:resolve` term at full strength. */
|
|
32
|
+
resolveScale: 0.3,
|
|
33
|
+
/** Share of the remaining headroom a lapse adds, by style. */
|
|
34
|
+
gain: { accounting: 0.5, ruminating: 0.15, none: 0 },
|
|
35
|
+
/** Strength kept after the commitment is kept. */
|
|
36
|
+
keptKeep: 0.6,
|
|
37
|
+
halfLifeDays: 7,
|
|
38
|
+
/** Resolves weaker than this are dropped. */
|
|
39
|
+
dropBelow: 0.05,
|
|
40
|
+
maxResolves: 8,
|
|
41
|
+
maxSeen: 64,
|
|
42
|
+
/** Distress felt per lapse by a ruminating reviewer, capped at `ruminateMax`. */
|
|
43
|
+
ruminatePer: 0.15,
|
|
44
|
+
ruminateMax: 0.45,
|
|
45
|
+
};
|
|
46
|
+
const STYLES = ['accounting', 'ruminating', 'none'];
|
|
47
|
+
/** Turn on the evening review. Idempotent; a second call updates style, source and time but keeps resolves. */
|
|
48
|
+
export function enableReview(p, spec = {}) {
|
|
49
|
+
const at = spec.at !== undefined && Number.isInteger(spec.at) && spec.at >= 0 && spec.at < 1440
|
|
50
|
+
? spec.at
|
|
51
|
+
: undefined;
|
|
52
|
+
const cur = p.review;
|
|
53
|
+
const st = cur ?? { style: 'accounting', at: REVIEW_DEFAULTS.at, resolves: [] };
|
|
54
|
+
if (spec.style && STYLES.includes(spec.style))
|
|
55
|
+
st.style = spec.style;
|
|
56
|
+
if (spec.styleSource !== undefined)
|
|
57
|
+
st.styleSource = spec.styleSource.slice(0, 96);
|
|
58
|
+
if (at !== undefined)
|
|
59
|
+
st.at = at;
|
|
60
|
+
p.review = st;
|
|
61
|
+
return st;
|
|
62
|
+
}
|
|
63
|
+
/** The key a commitment and its chronicle note share: kind, owed-to, and label (else first action). */
|
|
64
|
+
export const resolveKey = (c) => `${c.kind}:${c.toId ?? ''}:${c.label ?? c.action ?? c.actions?.[0] ?? ''}`;
|
|
65
|
+
/**
|
|
66
|
+
* The `review:resolve` terms for an option, read from its already-traced `commitment:*` and `abstain:*` terms.
|
|
67
|
+
* Pure; empty when review is off or no resolve matches.
|
|
68
|
+
*/
|
|
69
|
+
export function reviewTerms(p, terms) {
|
|
70
|
+
const st = p.review;
|
|
71
|
+
if (!st || st.resolves.length === 0)
|
|
72
|
+
return [];
|
|
73
|
+
let value = 0;
|
|
74
|
+
const used = new Set();
|
|
75
|
+
for (const t of terms) {
|
|
76
|
+
const m = /^(commitment|abstain):(.+)$/.exec(t.source);
|
|
77
|
+
if (!m || t.value === 0)
|
|
78
|
+
continue;
|
|
79
|
+
const c = p.agenda.commitments.find((x) => x.id === m[2]);
|
|
80
|
+
if (!c)
|
|
81
|
+
continue;
|
|
82
|
+
const r = st.resolves.find((x) => x.key === resolveKey(c));
|
|
83
|
+
if (!r || used.has(r.key))
|
|
84
|
+
continue;
|
|
85
|
+
used.add(r.key);
|
|
86
|
+
value += Math.sign(t.value) * REVIEW_DEFAULTS.resolveScale * r.strength;
|
|
87
|
+
}
|
|
88
|
+
const max = REVIEW_DEFAULTS.resolveScale;
|
|
89
|
+
value = Math.max(-max, Math.min(max, value));
|
|
90
|
+
return value === 0 ? [] : [{ source: 'review:resolve', value: round(value) }];
|
|
91
|
+
}
|
|
92
|
+
const fade = (s, days) => days <= 0 ? s : s * dexp((-Math.LN2 * days) / REVIEW_DEFAULTS.halfLifeDays);
|
|
93
|
+
/**
|
|
94
|
+
* Review now if it is due: at or after `at` on a day not yet reviewed, or when `closed` (the day just closed)
|
|
95
|
+
* was never reviewed. Reads the notes of `recent` (the latest closed day records) and the open day. Returns the
|
|
96
|
+
* record, or undefined when nothing ran. Distress for a ruminating reviewer is returned for the composite to feel.
|
|
97
|
+
*/
|
|
98
|
+
export function reviewIfDue(p, now, recent, closed) {
|
|
99
|
+
const st = p.review;
|
|
100
|
+
if (!st || st.style === 'none')
|
|
101
|
+
return undefined;
|
|
102
|
+
const today = Math.floor(now / 1440);
|
|
103
|
+
const seenNow = new Set(st.seen ?? []);
|
|
104
|
+
const lateLapse = closed?.broken.some((n) => !seenNow.has(n.id)) ?? false;
|
|
105
|
+
let day;
|
|
106
|
+
if (closed && (st.lastDay === undefined || st.lastDay < closed.day || lateLapse))
|
|
107
|
+
day = Math.max(closed.day, st.lastDay ?? closed.day);
|
|
108
|
+
else if (minuteOfDay(now) >= st.at && (st.lastDay === undefined || st.lastDay < today))
|
|
109
|
+
day = today;
|
|
110
|
+
else
|
|
111
|
+
return undefined;
|
|
112
|
+
const seen = new Set(st.seen ?? []);
|
|
113
|
+
const notes = (kind) => {
|
|
114
|
+
const out = [];
|
|
115
|
+
for (const r of closed && !recent.includes(closed) ? [...recent, closed] : recent)
|
|
116
|
+
for (const n of r[kind])
|
|
117
|
+
if (!seen.has(n.id))
|
|
118
|
+
out.push(n);
|
|
119
|
+
const open = p.chronicleDay;
|
|
120
|
+
if (open && open.day <= day)
|
|
121
|
+
for (const n of open[kind])
|
|
122
|
+
if (!seen.has(n.id))
|
|
123
|
+
out.push(n);
|
|
124
|
+
return out;
|
|
125
|
+
};
|
|
126
|
+
const broken = notes('broken');
|
|
127
|
+
const kept = notes('kept');
|
|
128
|
+
const elapsed = st.lastDay === undefined ? 0 : day - st.lastDay;
|
|
129
|
+
for (const r of st.resolves)
|
|
130
|
+
r.strength = round(fade(r.strength, elapsed));
|
|
131
|
+
const gain = REVIEW_DEFAULTS.gain[st.style];
|
|
132
|
+
const missed = [];
|
|
133
|
+
const resolved = [];
|
|
134
|
+
for (const n of broken) {
|
|
135
|
+
const key = resolveKey(n);
|
|
136
|
+
const name = n.label ?? n.action ?? n.kind;
|
|
137
|
+
if (!missed.includes(name))
|
|
138
|
+
missed.push(name);
|
|
139
|
+
let r = st.resolves.find((x) => x.key === key);
|
|
140
|
+
if (!r) {
|
|
141
|
+
r = { key, kind: n.kind, strength: 0, day };
|
|
142
|
+
if (n.label !== undefined)
|
|
143
|
+
r.label = n.label;
|
|
144
|
+
st.resolves.push(r);
|
|
145
|
+
}
|
|
146
|
+
r.strength = round(clamp01(r.strength + gain * (1 - r.strength)));
|
|
147
|
+
r.day = day;
|
|
148
|
+
if (!resolved.includes(name))
|
|
149
|
+
resolved.push(name);
|
|
150
|
+
}
|
|
151
|
+
const keptNames = [];
|
|
152
|
+
for (const n of kept) {
|
|
153
|
+
const key = resolveKey(n);
|
|
154
|
+
const name = n.label ?? n.action ?? n.kind;
|
|
155
|
+
if (!keptNames.includes(name))
|
|
156
|
+
keptNames.push(name);
|
|
157
|
+
const r = st.resolves.find((x) => x.key === key);
|
|
158
|
+
if (r && !broken.some((b) => resolveKey(b) === key))
|
|
159
|
+
r.strength = round(r.strength * REVIEW_DEFAULTS.keptKeep);
|
|
160
|
+
}
|
|
161
|
+
st.resolves = st.resolves
|
|
162
|
+
.filter((r) => r.strength >= REVIEW_DEFAULTS.dropBelow)
|
|
163
|
+
.sort((a, b) => b.strength - a.strength || (a.key < b.key ? -1 : a.key > b.key ? 1 : 0))
|
|
164
|
+
.slice(0, REVIEW_DEFAULTS.maxResolves);
|
|
165
|
+
const ids = [...(st.seen ?? []), ...broken.map((n) => n.id), ...kept.map((n) => n.id)];
|
|
166
|
+
st.seen = ids.slice(-REVIEW_DEFAULTS.maxSeen);
|
|
167
|
+
st.lastDay = day;
|
|
168
|
+
const record = {
|
|
169
|
+
day,
|
|
170
|
+
missed,
|
|
171
|
+
kept: keptNames,
|
|
172
|
+
resolved: st.style === 'accounting' ? resolved : [],
|
|
173
|
+
};
|
|
174
|
+
st.last = record;
|
|
175
|
+
const distress = st.style === 'ruminating'
|
|
176
|
+
? Math.min(REVIEW_DEFAULTS.ruminateMax, REVIEW_DEFAULTS.ruminatePer * missed.length)
|
|
177
|
+
: 0;
|
|
178
|
+
return { record, distress };
|
|
179
|
+
}
|
|
180
|
+
/** The person's own words for their latest review, or undefined when they hold no review. */
|
|
181
|
+
export function reviewLine(p) {
|
|
182
|
+
const st = p.review;
|
|
183
|
+
const r = st?.last;
|
|
184
|
+
if (!st || st.style === 'none' || !r)
|
|
185
|
+
return undefined;
|
|
186
|
+
const list = (xs) => xs.length <= 1 ? (xs[0] ?? '') : `${xs.slice(0, -1).join(', ')} and ${xs.at(-1)}`;
|
|
187
|
+
if (r.missed.length === 0)
|
|
188
|
+
return r.kept.length > 0 ? `Today I kept ${list(r.kept)}.` : undefined;
|
|
189
|
+
if (st.style === 'ruminating')
|
|
190
|
+
return `I keep going back over ${list(r.missed)}.`;
|
|
191
|
+
return `I didn't keep ${list(r.missed)}. Next time I'll be ready when it comes.`;
|
|
192
|
+
}
|
|
193
|
+
/** Restore guard: drop malformed fields, keep valid resolves. */
|
|
194
|
+
export function sanitizeReview(x, now) {
|
|
195
|
+
if (!isObj(x))
|
|
196
|
+
return undefined;
|
|
197
|
+
const style = STYLES.includes(x.style) ? x.style : 'accounting';
|
|
198
|
+
const at = Number.isInteger(x.at) && x.at >= 0 && x.at < 1440
|
|
199
|
+
? x.at
|
|
200
|
+
: REVIEW_DEFAULTS.at;
|
|
201
|
+
const resolves = [];
|
|
202
|
+
if (Array.isArray(x.resolves))
|
|
203
|
+
for (const r of x.resolves) {
|
|
204
|
+
if (!isObj(r) || typeof r.key !== 'string' || typeof r.kind !== 'string')
|
|
205
|
+
continue;
|
|
206
|
+
if (typeof r.strength !== 'number' || !Number.isFinite(r.strength) || !Number.isInteger(r.day))
|
|
207
|
+
continue;
|
|
208
|
+
const out = {
|
|
209
|
+
key: r.key.slice(0, 96),
|
|
210
|
+
kind: r.kind.slice(0, 32),
|
|
211
|
+
strength: clamp01(r.strength),
|
|
212
|
+
day: r.day,
|
|
213
|
+
};
|
|
214
|
+
if (typeof r.label === 'string')
|
|
215
|
+
out.label = r.label.slice(0, 96);
|
|
216
|
+
if (!resolves.some((y) => y.key === out.key))
|
|
217
|
+
resolves.push(out);
|
|
218
|
+
if (resolves.length >= REVIEW_DEFAULTS.maxResolves)
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
const st = { style, at, resolves };
|
|
222
|
+
if (typeof x.styleSource === 'string')
|
|
223
|
+
st.styleSource = x.styleSource.slice(0, 96);
|
|
224
|
+
if (Number.isInteger(x.lastDay) && (now === undefined || x.lastDay <= Math.floor(now / 1440)))
|
|
225
|
+
st.lastDay = x.lastDay;
|
|
226
|
+
if (Array.isArray(x.seen))
|
|
227
|
+
st.seen = x.seen.filter((s) => typeof s === 'string').slice(-REVIEW_DEFAULTS.maxSeen);
|
|
228
|
+
const l = x.last;
|
|
229
|
+
if (isObj(l) && Number.isInteger(l.day)) {
|
|
230
|
+
const strs = (v) => Array.isArray(v) ? v.filter((s) => typeof s === 'string').slice(0, 24) : [];
|
|
231
|
+
st.last = {
|
|
232
|
+
day: l.day,
|
|
233
|
+
missed: strs(l.missed),
|
|
234
|
+
kept: strs(l.kept),
|
|
235
|
+
resolved: strs(l.resolved),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
return st;
|
|
239
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -91,6 +91,11 @@ export interface BodyState {
|
|
|
91
91
|
* recovering with rest and faster with sleep. Raises felt fatigue and lowers capacity. Absent = no store.
|
|
92
92
|
*/
|
|
93
93
|
depletion?: Unit;
|
|
94
|
+
/**
|
|
95
|
+
* Fullness (2.8.0, opt-in via `enableFullness`): when true, eating, drinking or taking relief for a need already
|
|
96
|
+
* past its comfort level costs a little (`full:<need>` term) instead of being free. Absent = no term.
|
|
97
|
+
*/
|
|
98
|
+
fullness?: boolean;
|
|
94
99
|
asleep: boolean;
|
|
95
100
|
/** Minute the current sleep or wake period began. */
|
|
96
101
|
since: Minute;
|
|
@@ -832,7 +837,7 @@ export declare const PERSON_SCHEMA = "human/person@1";
|
|
|
832
837
|
* and an activity that would cover a closing stretch is reviewed when the stretch begins (`isUnderWay`,
|
|
833
838
|
* `dutyReviewAt`). Person shape unchanged.
|
|
834
839
|
*/
|
|
835
|
-
export declare const ENGINE_VERSION = "2.
|
|
840
|
+
export declare const ENGINE_VERSION = "2.8.0";
|
|
836
841
|
/** A pregnancy carried by this person (host-driven; see `family/`). */
|
|
837
842
|
export interface Pregnancy {
|
|
838
843
|
fatherId: PersonId;
|
|
@@ -999,6 +1004,8 @@ export interface Person {
|
|
|
999
1004
|
prospection?: ProspectionState;
|
|
1000
1005
|
/** Noticed promptings and practised turnings-away (2.4.0, opt-in via `enablePromptings`). Owned by `promptings/`. */
|
|
1001
1006
|
promptings?: PromptingsState;
|
|
1007
|
+
/** Evening self-review and its resolves (2.7.0, opt-in via `enableReview`). Owned by `review/`. */
|
|
1008
|
+
review?: ReviewState;
|
|
1002
1009
|
/** Practice consolidation (1.8.0, opt-in via `enableSkillRetention`). Owned by `skills/`. */
|
|
1003
1010
|
skillRetention?: {
|
|
1004
1011
|
consolidationHours: number;
|
|
@@ -1065,6 +1072,12 @@ export interface Affordance {
|
|
|
1065
1072
|
placeId?: EntityId;
|
|
1066
1073
|
/** Other people taking part. */
|
|
1067
1074
|
with?: PersonId[];
|
|
1075
|
+
/**
|
|
1076
|
+
* People already taking this course (2.8.0): a partner moving on, a companion leaving for home. Choosing it keeps
|
|
1077
|
+
* the person with them, a `together:<id>` term from closeness (`cognition` SCOPE). Unlike `with`, it says who is
|
|
1078
|
+
* going this way, not who would take part. Absent = no term, exactly as before.
|
|
1079
|
+
*/
|
|
1080
|
+
alongside?: PersonId[];
|
|
1068
1081
|
/** Minutes the activity occupies (including travel if the host folds it in). */
|
|
1069
1082
|
duration: number;
|
|
1070
1083
|
/** Physical exertion 0..1. */
|
|
@@ -1103,6 +1116,16 @@ export interface Affordance {
|
|
|
1103
1116
|
* spent during them. Absent = no forecast, exactly as before.
|
|
1104
1117
|
*/
|
|
1105
1118
|
followedBy?: FollowOn[];
|
|
1119
|
+
/**
|
|
1120
|
+
* Readout thresholds for this offer's forecast (2.8.0), for hosts whose people live higher on a readout (a
|
|
1121
|
+
* night keeper at sleepiness 0.7). `comfortAt` is where the forecast starts to weigh and `spentAt` where it
|
|
1122
|
+
* weighs fully; either absent, or a pair that is not 0 ≤ comfortAt < spentAt ≤ 1, falls back to
|
|
1123
|
+
* `FORECAST_DEFAULTS`. Ignored without `followedBy`.
|
|
1124
|
+
*/
|
|
1125
|
+
forecastAt?: {
|
|
1126
|
+
comfortAt?: Unit;
|
|
1127
|
+
spentAt?: Unit;
|
|
1128
|
+
};
|
|
1106
1129
|
/** Set on a partner's mirror offer by the joint protocol in `sim/` (the proposal id). */
|
|
1107
1130
|
jointId?: string;
|
|
1108
1131
|
/** Material gain/cost in host units (e.g. coins). Valued via security/achievement/power values. */
|
|
@@ -1352,6 +1375,8 @@ export interface DecisionRecord {
|
|
|
1352
1375
|
review?: boolean;
|
|
1353
1376
|
/** Why this decision was brought forward, when `interrupt()` forced it (e.g. 'percept:injury'). */
|
|
1354
1377
|
interrupt?: string;
|
|
1378
|
+
/** Made with `DecideOptions.reconsider` (2.8.0): the running activity had no inertia bonus. */
|
|
1379
|
+
reconsider?: true;
|
|
1355
1380
|
/**
|
|
1356
1381
|
* One resolution per voice that suggested something in this decision, in voice-id order (N1). `suggestion` is
|
|
1357
1382
|
* the credited voice's resolution (same object), or the only one when a single voice spoke.
|
|
@@ -1400,8 +1425,12 @@ export interface PersonSpec {
|
|
|
1400
1425
|
skillRetention?: boolean;
|
|
1401
1426
|
/** Accumulated exertion (2.6.0): the same as `enableFatigueStore(p)` after creation. */
|
|
1402
1427
|
fatigueStore?: boolean;
|
|
1428
|
+
/** Fullness (2.8.0): the same as `enableFullness(p)` after creation. */
|
|
1429
|
+
fullness?: boolean;
|
|
1403
1430
|
/** Promptings and turning away (2.4.0): the same as `enablePromptings(p, spec)` after creation. */
|
|
1404
1431
|
promptings?: PromptingsSpec;
|
|
1432
|
+
/** Evening self-review (2.7.0): the same as `enableReview(p, spec)` after creation. */
|
|
1433
|
+
review?: ReviewSpec;
|
|
1405
1434
|
};
|
|
1406
1435
|
}
|
|
1407
1436
|
/** A known span of exertion after an act (see `Affordance.followedBy`). */
|
|
@@ -1454,6 +1483,47 @@ export interface PromptingsState {
|
|
|
1454
1483
|
/** Echo gained per prompt under `fault`. Default 0: the recurrence term is a host-set assumption (P3 contested). */
|
|
1455
1484
|
echoRate: number;
|
|
1456
1485
|
}
|
|
1486
|
+
/**
|
|
1487
|
+
* How a person reviews a day (2.7.0, a held reading): `accounting` (muhasaba: name the lapse and resolve for its
|
|
1488
|
+
* next occasion), `ruminating` (go over it abstractly: weaker resolve, some distress) or `none` (no review).
|
|
1489
|
+
*/
|
|
1490
|
+
export type ReviewStyle = 'accounting' | 'ruminating' | 'none';
|
|
1491
|
+
/** A resolve formed in review: an if-then intention for the next occasion of a missed commitment. */
|
|
1492
|
+
export interface Resolve {
|
|
1493
|
+
/** The commitment's label, else its first action, else its kind. */
|
|
1494
|
+
key: string;
|
|
1495
|
+
label?: string;
|
|
1496
|
+
kind: string;
|
|
1497
|
+
strength: Unit;
|
|
1498
|
+
/** Day the resolve was formed or last renewed. */
|
|
1499
|
+
day: number;
|
|
1500
|
+
}
|
|
1501
|
+
/** What the person concluded in their latest review. */
|
|
1502
|
+
export interface ReviewRecord {
|
|
1503
|
+
day: number;
|
|
1504
|
+
missed: string[];
|
|
1505
|
+
kept: string[];
|
|
1506
|
+
resolved: string[];
|
|
1507
|
+
}
|
|
1508
|
+
/** Owned by `review/`. */
|
|
1509
|
+
export interface ReviewState {
|
|
1510
|
+
style: ReviewStyle;
|
|
1511
|
+
styleSource?: string;
|
|
1512
|
+
/** Minute of the day from which the evening review can run (default 21:00). */
|
|
1513
|
+
at: number;
|
|
1514
|
+
/** Last day reviewed. */
|
|
1515
|
+
lastDay?: number;
|
|
1516
|
+
resolves: Resolve[];
|
|
1517
|
+
last?: ReviewRecord;
|
|
1518
|
+
/** Commitment ids already reviewed (bounded). */
|
|
1519
|
+
seen?: string[];
|
|
1520
|
+
}
|
|
1521
|
+
/** What a host hands `enableReview`. */
|
|
1522
|
+
export interface ReviewSpec {
|
|
1523
|
+
style?: ReviewStyle;
|
|
1524
|
+
styleSource?: string;
|
|
1525
|
+
at?: number;
|
|
1526
|
+
}
|
|
1457
1527
|
/** What a host hands `enablePromptings`. */
|
|
1458
1528
|
export interface PromptingsSpec {
|
|
1459
1529
|
reading?: PromptingReading;
|
package/dist/types.js
CHANGED
|
@@ -120,4 +120,4 @@ export const PERSON_SCHEMA = 'human/person@1';
|
|
|
120
120
|
* and an activity that would cover a closing stretch is reviewed when the stretch begins (`isUnderWay`,
|
|
121
121
|
* `dutyReviewAt`). Person shape unchanged.
|
|
122
122
|
*/
|
|
123
|
-
export const ENGINE_VERSION = '2.
|
|
123
|
+
export const ENGINE_VERSION = '2.8.0';
|
package/dist/will/will.d.ts
CHANGED
|
@@ -28,6 +28,9 @@
|
|
|
28
28
|
* when the answer leaves the person doing what they are doing. A host command is a separate input that yields the
|
|
29
29
|
* `commanded` verdict (see the commanded-control SCOPE on `commandOutcome`); `learnFromVoice` learns from assented,
|
|
30
30
|
* complied and commanded activities, and `adoptVoiceTrust` moves trust toward a target without a suggestion.
|
|
31
|
+
* 2.8.0: `seedVoice` sets an authored starting trust after creation; `noteVoicePromise` learns from whether the
|
|
32
|
+
* voice kept its own promise; a decision with `reconsider` (WillContext) gives the running activity no inertia
|
|
33
|
+
* bonus, for a review the host forces because something changed (promise inertia, a cognition term, stays).
|
|
31
34
|
*/
|
|
32
35
|
import type { Affordance, BodyReadout, Command, Considered, EntityId, Minute, Percept, Person, RefusalKind, Signed, StandingAdvice, Suggestion, SuggestionResolution, Unit, VoiceConflict, VoiceRelation, WillState } from '../types.ts';
|
|
33
36
|
export declare const WILL_DEFAULTS: {
|
|
@@ -108,6 +111,14 @@ export declare const WILL_DEFAULTS: {
|
|
|
108
111
|
maxCredited: number;
|
|
109
112
|
/** Share of a tie's inertia bonus that remains when the activity is about to end. */
|
|
110
113
|
inertiaFloor: number;
|
|
114
|
+
/**
|
|
115
|
+
* A voice's own promise (2.8.0, `noteVoicePromise`): a kept promise moves trust this share of the way toward 1, a
|
|
116
|
+
* broken one this share toward 0. Broken weighs more than kept (trust asymmetry, unaudited citations in research/empirical-models.md §6b: Slovic 1993; broken promises
|
|
117
|
+
* damage trust and kept ones restore it only gradually, Schweitzer, Hershey and Bradlow 2006: research/
|
|
118
|
+
* empirical-models.md §6b). Shape only; the numbers are engineering defaults equal to `trustGain`/`trustLoss`.
|
|
119
|
+
*/
|
|
120
|
+
promiseKeptGain: number;
|
|
121
|
+
promiseBrokenLoss: number;
|
|
111
122
|
/**
|
|
112
123
|
* Insisting cannot override survival: at this desperation (same scale as conscience's necessity threshold),
|
|
113
124
|
* or when the person's own choice is driven by a bodily need at `survivalNeedUrgency`, the verdict is
|
|
@@ -151,6 +162,8 @@ export interface WillContext {
|
|
|
151
162
|
}[];
|
|
152
163
|
/** A host command in force (1.6.0); see the command SCOPE on `commandOutcome`. */
|
|
153
164
|
command?: Command;
|
|
165
|
+
/** No inertia bonus for the running activity in this decision (2.8.0, `DecideOptions.reconsider`). */
|
|
166
|
+
reconsider?: boolean;
|
|
154
167
|
}
|
|
155
168
|
/**
|
|
156
169
|
* How a command fared in one decision. `holds`: the commanded option was chosen. Otherwise `ends` says whether
|
|
@@ -194,6 +207,32 @@ export declare function voiceOf(p: Person, voiceId: EntityId): VoiceRelation | u
|
|
|
194
207
|
* records no history event and no credit, since no suggestion of that voice was weighed.
|
|
195
208
|
*/
|
|
196
209
|
export declare function adoptVoiceTrust(p: Person, voiceId: EntityId, target: Unit, rate: Unit): VoiceRelation;
|
|
210
|
+
/**
|
|
211
|
+
* Give `p` an authored starting trust in a voice after creation (2.8.0), marked `seeded` as `PersonSpec.voices`
|
|
212
|
+
* would (a seeded voice is never evicted to make room). A no-op returning the existing relation when the voice is
|
|
213
|
+
* already known, unless `replace` is set; replacing keeps counters and history and marks the voice seeded. Records
|
|
214
|
+
* no history event: no suggestion was weighed. A non-finite trust falls back to `defaultVoiceTrust`.
|
|
215
|
+
*/
|
|
216
|
+
export declare function seedVoice(p: Person, voiceId: EntityId, trust: Unit, opts?: {
|
|
217
|
+
replace?: boolean;
|
|
218
|
+
}): VoiceRelation;
|
|
219
|
+
/**
|
|
220
|
+
* SCOPE (a voice's own promise, 2.8.0): the voice said it would do something ("I will carry food up by 18:00") and
|
|
221
|
+
* the host reports whether it did. This is evidence about the voice's word, separate from how following its advice
|
|
222
|
+
* felt (`learnFromVoice`), so it needs no suggestion resolution and works for a request that was only deferred.
|
|
223
|
+
* Kept moves trust `promiseKeptGain` of the way toward 1, divided by 1 + n where n is the decayed count of kept
|
|
224
|
+
* promises already credited for the same action (key `promise:<action>` in `credited`), so a string of trivial
|
|
225
|
+
* promises cannot farm trust (these keys share `credited` and its `maxCredited` bound with advice credits, so a
|
|
226
|
+
* host with many promise actions can evict older entries of either kind, which only shortens a discount);
|
|
227
|
+
* broken moves it `promiseBrokenLoss` toward 0, undiscounted. History reasons are
|
|
228
|
+
* `promise-kept` and `promise-broken`. The voice is added if absent (bounded like any new voice). Does not model
|
|
229
|
+
* excuses, partial keeping, apology, or the size of what was promised; the host judges kept or broken.
|
|
230
|
+
*/
|
|
231
|
+
export declare function noteVoicePromise(p: Person, voiceId: EntityId, event: {
|
|
232
|
+
kept: boolean;
|
|
233
|
+
at?: Minute;
|
|
234
|
+
action?: string;
|
|
235
|
+
}): VoiceRelation;
|
|
197
236
|
/**
|
|
198
237
|
* The next minute after `now` at which a pending commitment reaches `wakeCommitmentPressure`, the level at which a
|
|
199
238
|
* sleeper may wake to serve it (the asleep veto lifts). The composite caps a sleeper's review there so a duty whose
|
package/dist/will/will.js
CHANGED
|
@@ -28,6 +28,9 @@
|
|
|
28
28
|
* when the answer leaves the person doing what they are doing. A host command is a separate input that yields the
|
|
29
29
|
* `commanded` verdict (see the commanded-control SCOPE on `commandOutcome`); `learnFromVoice` learns from assented,
|
|
30
30
|
* complied and commanded activities, and `adoptVoiceTrust` moves trust toward a target without a suggestion.
|
|
31
|
+
* 2.8.0: `seedVoice` sets an authored starting trust after creation; `noteVoicePromise` learns from whether the
|
|
32
|
+
* voice kept its own promise; a decision with `reconsider` (WillContext) gives the running activity no inertia
|
|
33
|
+
* bonus, for a review the host forces because something changed (promise inertia, a cognition term, stays).
|
|
31
34
|
*/
|
|
32
35
|
import { breakAllows, inBreak } from '../affect/index.js';
|
|
33
36
|
import { commitmentPressure, isUnderWay, pressureReachedAt } from '../agenda/index.js';
|
|
@@ -114,6 +117,14 @@ export const WILL_DEFAULTS = {
|
|
|
114
117
|
maxCredited: 8,
|
|
115
118
|
/** Share of a tie's inertia bonus that remains when the activity is about to end. */
|
|
116
119
|
inertiaFloor: 0.5,
|
|
120
|
+
/**
|
|
121
|
+
* A voice's own promise (2.8.0, `noteVoicePromise`): a kept promise moves trust this share of the way toward 1, a
|
|
122
|
+
* broken one this share toward 0. Broken weighs more than kept (trust asymmetry, unaudited citations in research/empirical-models.md §6b: Slovic 1993; broken promises
|
|
123
|
+
* damage trust and kept ones restore it only gradually, Schweitzer, Hershey and Bradlow 2006: research/
|
|
124
|
+
* empirical-models.md §6b). Shape only; the numbers are engineering defaults equal to `trustGain`/`trustLoss`.
|
|
125
|
+
*/
|
|
126
|
+
promiseKeptGain: 0.1,
|
|
127
|
+
promiseBrokenLoss: 0.25,
|
|
117
128
|
/**
|
|
118
129
|
* Insisting cannot override survival: at this desperation (same scale as conscience's necessity threshold),
|
|
119
130
|
* or when the person's own choice is driven by a bodily need at `survivalNeedUrgency`, the verdict is
|
|
@@ -224,6 +235,61 @@ export function adoptVoiceTrust(p, voiceId, target, rate) {
|
|
|
224
235
|
v.trust = clamp01(v.trust + r * (t - v.trust));
|
|
225
236
|
return v;
|
|
226
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* Give `p` an authored starting trust in a voice after creation (2.8.0), marked `seeded` as `PersonSpec.voices`
|
|
240
|
+
* would (a seeded voice is never evicted to make room). A no-op returning the existing relation when the voice is
|
|
241
|
+
* already known, unless `replace` is set; replacing keeps counters and history and marks the voice seeded. Records
|
|
242
|
+
* no history event: no suggestion was weighed. A non-finite trust falls back to `defaultVoiceTrust`.
|
|
243
|
+
*/
|
|
244
|
+
export function seedVoice(p, voiceId, trust, opts = {}) {
|
|
245
|
+
const existing = voiceOf(p, voiceId);
|
|
246
|
+
if (existing && !opts.replace)
|
|
247
|
+
return existing;
|
|
248
|
+
const v = existing ?? ensureVoice(p, voiceId);
|
|
249
|
+
v.trust = clamp01(Number.isFinite(trust) ? trust : WILL_DEFAULTS.defaultVoiceTrust);
|
|
250
|
+
v.seeded = true;
|
|
251
|
+
return v;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* SCOPE (a voice's own promise, 2.8.0): the voice said it would do something ("I will carry food up by 18:00") and
|
|
255
|
+
* the host reports whether it did. This is evidence about the voice's word, separate from how following its advice
|
|
256
|
+
* felt (`learnFromVoice`), so it needs no suggestion resolution and works for a request that was only deferred.
|
|
257
|
+
* Kept moves trust `promiseKeptGain` of the way toward 1, divided by 1 + n where n is the decayed count of kept
|
|
258
|
+
* promises already credited for the same action (key `promise:<action>` in `credited`), so a string of trivial
|
|
259
|
+
* promises cannot farm trust (these keys share `credited` and its `maxCredited` bound with advice credits, so a
|
|
260
|
+
* host with many promise actions can evict older entries of either kind, which only shortens a discount);
|
|
261
|
+
* broken moves it `promiseBrokenLoss` toward 0, undiscounted. History reasons are
|
|
262
|
+
* `promise-kept` and `promise-broken`. The voice is added if absent (bounded like any new voice). Does not model
|
|
263
|
+
* excuses, partial keeping, apology, or the size of what was promised; the host judges kept or broken.
|
|
264
|
+
*/
|
|
265
|
+
export function noteVoicePromise(p, voiceId, event) {
|
|
266
|
+
const W = WILL_DEFAULTS;
|
|
267
|
+
const v = ensureVoice(p, voiceId);
|
|
268
|
+
const at = event.at !== undefined && Number.isFinite(event.at) ? event.at : p.now;
|
|
269
|
+
const before = v.trust;
|
|
270
|
+
if (event.kept) {
|
|
271
|
+
const key = `promise:${event.action ?? ''}`;
|
|
272
|
+
v.credited ??= [];
|
|
273
|
+
const list = v.credited;
|
|
274
|
+
const prior = list.find((c) => c.action === key);
|
|
275
|
+
const n = prior ? decay(prior.n, Math.max(0, at - prior.at), W.creditHalfLife) : 0;
|
|
276
|
+
v.trust = clamp01(v.trust + (W.promiseKeptGain * (1 - v.trust)) / (1 + n));
|
|
277
|
+
if (prior) {
|
|
278
|
+
prior.n = n + 1;
|
|
279
|
+
prior.at = at;
|
|
280
|
+
}
|
|
281
|
+
else
|
|
282
|
+
list.push({ action: key, n: 1, at });
|
|
283
|
+
if (list.length > W.maxCredited) {
|
|
284
|
+
list.sort((a, b) => b.at - a.at);
|
|
285
|
+
list.length = W.maxCredited;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
else
|
|
289
|
+
v.trust = clamp01(v.trust - W.promiseBrokenLoss * v.trust);
|
|
290
|
+
noteTrust(v, v.trust - before, event.kept ? 'promise-kept' : 'promise-broken', at, event.action);
|
|
291
|
+
return v;
|
|
292
|
+
}
|
|
227
293
|
const finite = (x) => x === undefined || Number.isFinite(x);
|
|
228
294
|
/** Host numbers the framework computes with must be finite; a NaN offer is vetoed, never chosen. */
|
|
229
295
|
function wellFormed(aff) {
|
|
@@ -547,7 +613,7 @@ function evaluate(p, input, ctx, given, draw) {
|
|
|
547
613
|
const live = considered.filter((c) => !c.vetoed);
|
|
548
614
|
const current = p.activity;
|
|
549
615
|
const score = (c) => {
|
|
550
|
-
if (!current || c.affordanceId !== current.affordanceId)
|
|
616
|
+
if (!current || c.affordanceId !== current.affordanceId || ctx.reconsider)
|
|
551
617
|
return safeUtility(c.utility);
|
|
552
618
|
const span = Math.max(1, current.endsAt - current.startedAt);
|
|
553
619
|
const remaining = clamp01((current.endsAt - ctx.now) / span);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adam0white/human-framework",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"description": "Deterministic, explainable simulated people: body, needs, affect, memory, beliefs, skills, habits, relationships, values and a will that can refuse a suggestion",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"simulation",
|