@adam0white/human-framework 2.5.0 → 2.7.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.
@@ -30,6 +30,15 @@ export declare const BODY_DEFAULTS: {
30
30
  exertionRiseHalfLife: number;
31
31
  exertionRecoverHalfLife: number;
32
32
  exertionRecoverHalfLifeAsleep: number;
33
+ /** Fatigue store (2.6.0, opt-in): depletion per hour at full effort (× (1 - 0.5 fitness) × (1 - depletion)). */
34
+ depletionPerHour: number;
35
+ /** Effort below which the store does not fill and recovers while awake. */
36
+ depletionFloor: number;
37
+ /** Half-life of depletion at rest awake and asleep (minutes, divided by the life-course recovery factor). */
38
+ depletionRestHalfLife: number;
39
+ depletionSleepHalfLife: number;
40
+ /** How much a full store adds to felt fatigue: fatigue = 1 - (1 - exertion)(1 - 0.5 s)(1 - weight × depletion). */
41
+ depletionFatigueWeight: number;
33
42
  /** Effort above which fitness trains. */
34
43
  trainingEffort: number;
35
44
  /** Detraining half-life (min) toward fitnessFloor. */
@@ -116,6 +125,8 @@ export interface BodyPerceptionContext {
116
125
  fastingDays: number;
117
126
  }
118
127
  export type BodyParams = typeof BODY_DEFAULTS;
128
+ /** Turn on the fatigue store (2.6.0): accumulated exertion that recovers with rest and sleep. Idempotent. */
129
+ export declare function enableFatigueStore(p: Person): void;
119
130
  export declare function createBody(init: PersonSpec['body'] | undefined, now: Minute): BodyState;
120
131
  /**
121
132
  * Advance `p.body` over `dt` minutes under `load`. Call with `p.now` at the start of the interval: a mode
@@ -205,4 +216,4 @@ export declare function forecastBody(p: Person, spans: readonly {
205
216
  label: string;
206
217
  duration: number;
207
218
  load: BodyLoad;
208
- }[], mods: LifeModifiers, params?: BodyParams, step?: number): BodyForecastSpan[];
219
+ }[], mods: LifeModifiers, params?: BodyParams, step?: number, perception?: BodyPerceptionContext): BodyForecastSpan[];
package/dist/body/body.js CHANGED
@@ -76,6 +76,15 @@ export const BODY_DEFAULTS = {
76
76
  exertionRiseHalfLife: 40,
77
77
  exertionRecoverHalfLife: 45,
78
78
  exertionRecoverHalfLifeAsleep: 25,
79
+ /** Fatigue store (2.6.0, opt-in): depletion per hour at full effort (× (1 - 0.5 fitness) × (1 - depletion)). */
80
+ depletionPerHour: 0.3,
81
+ /** Effort below which the store does not fill and recovers while awake. */
82
+ depletionFloor: 0.3,
83
+ /** Half-life of depletion at rest awake and asleep (minutes, divided by the life-course recovery factor). */
84
+ depletionRestHalfLife: 8 * 60,
85
+ depletionSleepHalfLife: 3 * 60,
86
+ /** How much a full store adds to felt fatigue: fatigue = 1 - (1 - exertion)(1 - 0.5 s)(1 - weight × depletion). */
87
+ depletionFatigueWeight: 1,
79
88
  /** Effort above which fitness trains. */
80
89
  trainingEffort: 0.4,
81
90
  /** Detraining half-life (min) toward fitnessFloor. */
@@ -229,6 +238,13 @@ export function sanitizeBody(b, now) {
229
238
  delete b.lastSleep;
230
239
  if (b.lastDowned !== undefined && !span(b.lastDowned))
231
240
  delete b.lastDowned;
241
+ if (b.depletion !== undefined)
242
+ b.depletion = typeof b.depletion === 'number' && Number.isFinite(b.depletion) ? clamp01(b.depletion) : 0;
243
+ }
244
+ /** Turn on the fatigue store (2.6.0): accumulated exertion that recovers with rest and sleep. Idempotent. */
245
+ export function enableFatigueStore(p) {
246
+ if (p.body.depletion === undefined)
247
+ p.body.depletion = 0;
232
248
  }
233
249
  /**
234
250
  * Repair the optional chronic fields of saved illnesses in place: a non-finite or out-of-range `baseline` is
@@ -367,6 +383,18 @@ function subStep(b, h, load, mods, P) {
367
383
  ? P.exertionRiseHalfLife
368
384
  : (asleep ? P.exertionRecoverHalfLifeAsleep : P.exertionRecoverHalfLife) / recovery;
369
385
  b.exertion = clamp01(decay(b.exertion, h, hl, target));
386
+ // Fatigue store (2.6.0, opt-in): fills with sustained effort above the floor, empties with rest and sleep.
387
+ if (b.depletion !== undefined) {
388
+ const d = clamp01(Number.isFinite(b.depletion) ? b.depletion : 0);
389
+ if (effortUsed > P.depletionFloor) {
390
+ const rate = (P.depletionPerHour * (effortUsed - P.depletionFloor)) / (1 - P.depletionFloor);
391
+ b.depletion = clamp01(d + (rate * (1 - 0.5 * b.fitness) * (1 - d) * h) / 60);
392
+ }
393
+ else {
394
+ const half = (asleep ? P.depletionSleepHalfLife : P.depletionRestHalfLife) / recovery;
395
+ b.depletion = clamp01(decay(d, h, half, 0));
396
+ }
397
+ }
370
398
  // Fitness: dF/dt = -kd (F - floor) + kt (max - F), exact for constant load.
371
399
  const maxFit = clamp01(mods.maxFitness);
372
400
  const kd = LN2 / P.detrainHalfLife;
@@ -501,7 +529,8 @@ export function readBody(p, params = BODY_DEFAULTS, ctx) {
501
529
  const sleepiness = clamp01(params.sleepinessPressureWeight * b.sleepPressure +
502
530
  params.sleepinessCircadianWeight * (1 - alertness) +
503
531
  debtTerm);
504
- const fatigueOf = (s) => clamp01(1 - (1 - b.exertion) * (1 - 0.5 * s));
532
+ const store = b.depletion === undefined ? 1 : 1 - params.depletionFatigueWeight * clamp01(b.depletion);
533
+ const fatigueOf = (s) => clamp01(1 - (1 - b.exertion) * (1 - 0.5 * s) * store);
505
534
  const fatigue = fatigueOf(sleepiness);
506
535
  const hunger = clamp01(1 - b.satiety);
507
536
  const thirst = clamp01(1 - b.hydration);
@@ -767,7 +796,7 @@ export function nextBodyThreshold(p, load, mods, thresholds = { hunger: 0.7, thi
767
796
  * The person's own forecast of their body through consecutive spans of known exertion, read as they perceive it
768
797
  * (2.5.0). Simulates a cloned body in steps of at most `step` minutes; pure (does not mutate `p`).
769
798
  */
770
- export function forecastBody(p, spans, mods, params = BODY_DEFAULTS, step = 30) {
799
+ export function forecastBody(p, spans, mods, params = BODY_DEFAULTS, step = 30, perception) {
771
800
  const out = [];
772
801
  const sim = { ...p, body: cloneBody(p.body) };
773
802
  for (const s of spans) {
@@ -775,7 +804,7 @@ export function forecastBody(p, spans, mods, params = BODY_DEFAULTS, step = 30)
775
804
  // Hard internal cap (a week) so a hostile span cannot stall a decision.
776
805
  let left = Math.min(7 * MINUTES_PER_DAY, Math.max(0, Number.isFinite(s.duration) ? s.duration : 0));
777
806
  advanceBody(sim, 0, s.load, mods, params);
778
- const r0 = readBody(sim, params).perceived;
807
+ const r0 = readBody(sim, params, perception).perceived;
779
808
  for (const k of ['fatigue', 'sleepiness', 'hunger', 'thirst'])
780
809
  if (r0[k] > span.worst) {
781
810
  span.worst = r0[k];
@@ -786,7 +815,7 @@ export function forecastBody(p, spans, mods, params = BODY_DEFAULTS, step = 30)
786
815
  advanceBody(sim, dt, s.load, mods, params);
787
816
  sim.now += dt;
788
817
  left -= dt;
789
- const r = readBody(sim, params).perceived;
818
+ const r = readBody(sim, params, perception).perceived;
790
819
  for (const k of ['fatigue', 'sleepiness', 'hunger', 'thirst'])
791
820
  if (r[k] > span.worst) {
792
821
  span.worst = r[k];
@@ -1,3 +1,4 @@
1
+ import type { BodyPerceptionContext } from '../body/index.ts';
1
2
  import { type HabitContext } from '../habits/index.ts';
2
3
  import { type PromptingSeen } from '../promptings/index.ts';
3
4
  import type { Affordance, BodyReadout, Command, Considered, DecisionRecord, LifeModifiers, Minute, NeedReading, Person, Suggestion, Term, Unit } from '../types.ts';
@@ -100,6 +101,8 @@ export interface ConsiderContext {
100
101
  * leaves the material and safety terms exactly as without it.
101
102
  */
102
103
  scarcity?: Unit;
104
+ /** The body's perception context now (an open fast), for the bodily forecast (2.6.0). */
105
+ perception?: BodyPerceptionContext;
103
106
  }
104
107
  /**
105
108
  * Standing advice terms for one option: per source, the strongest matching entry's weight × trust in the source
@@ -36,6 +36,7 @@ import { urgencyOf } from '../needs/index.js';
36
36
  import { partneringTerms } from '../partnering/index.js';
37
37
  import { promptIn, promptingTerms } from '../promptings/index.js';
38
38
  import { bodyForecastTerm, receivedGoalRoute } from '../prospection/index.js';
39
+ import { reviewTerms } from '../review/index.js';
39
40
  import { companionSteadiness, socialTerms } from '../social/index.js';
40
41
  import { PHYSIOLOGICAL_NEEDS, VALUE_KEYS } from '../types.js';
41
42
  import { adviceWeight, resolveChoice, safeUtility, standingAdvice, suggestionTargets, voicesIn, } from '../will/index.js';
@@ -351,7 +352,7 @@ export function consider(p, aff, ctx) {
351
352
  push('effort', effort);
352
353
  // Bodily forecast (2.5.0): only offers that declare known follow-on exertion.
353
354
  if (aff.followedBy?.length) {
354
- const t = bodyForecastTerm(p, aff, ctx.mods);
355
+ const t = bodyForecastTerm(p, aff, ctx.mods, ctx.perception);
355
356
  if (t)
356
357
  push(t.source, t.value);
357
358
  }
@@ -409,6 +410,10 @@ export function consider(p, aff, ctx) {
409
410
  if (p.promptings)
410
411
  for (const t of promptingTerms(p, aff.action, terms, aff.tags, ctx.now, ctx.desperation))
411
412
  push(t.source, t.value);
413
+ // Evening review (2.7.0, opt-in): a resolve formed after a lapse leans on that commitment's own term.
414
+ if (p.review)
415
+ for (const t of reviewTerms(p, terms))
416
+ push(t.source, t.value);
412
417
  let utility = 0;
413
418
  for (const t of terms)
414
419
  utility += t.value;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Human Framework v1 public surface. */
2
- export declare const FRAMEWORK_VERSION = "2.5.0";
2
+ export declare const FRAMEWORK_VERSION = "2.7.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';
@@ -32,6 +32,7 @@ export * from './promptings/index.ts';
32
32
  export type { ConsequenceEvidence } from './prospection/index.ts';
33
33
  export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, FORECAST_DEFAULTS, forecastLine, 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.5.0';
2
+ export const FRAMEWORK_VERSION = '2.7.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';
@@ -28,6 +28,7 @@ export * from './person.js';
28
28
  export * from './promptings/index.js';
29
29
  export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, FORECAST_DEFAULTS, forecastLine, 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`. 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.6.0': {
62
+ to: '2.7.0',
63
+ note: 'the evening review (review) is optional and starts absent; existing hosts retain exact behavior',
64
+ apply: stamp,
65
+ },
66
+ '2.5.0': {
67
+ to: '2.6.0',
68
+ 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",
69
+ apply: stamp,
70
+ },
61
71
  '2.4.0': {
62
72
  to: '2.5.0',
63
73
  note: 'person shape unchanged; offers may declare followedBy spans for a bodily forecast; offers without it are unchanged',
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, expose, injure, nextBodyThreshold, readBody, setDowned, sicken, skipBody, } from './body/index.js';
25
+ import { advanceBody, BODY_DEFAULTS, checkDowned, clearDowned, consume, createBody, downedAllows, enableFatigueStore, 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';
@@ -38,6 +38,7 @@ import { advanceNeeds, createNeeds, levelOf, meanSatisfaction, readNeeds, satisf
38
38
  import { widow } from './partnering/index.js';
39
39
  import { enablePromptings, nafsPatterns, notePromptings } from './promptings/index.js';
40
40
  import { bodyForecastFor, consumeConsequences, forecastLine, 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';
@@ -154,8 +155,12 @@ export function createPerson(spec) {
154
155
  enableCharacterChange(p);
155
156
  if (on?.skillRetention)
156
157
  enableSkillRetention(p);
158
+ if (on?.fatigueStore)
159
+ enableFatigueStore(p);
157
160
  if (on?.promptings)
158
161
  enablePromptings(p, on.promptings);
162
+ if (on?.review)
163
+ enableReview(p, on.review);
159
164
  return p;
160
165
  }
161
166
  /**
@@ -416,6 +421,12 @@ export function tick(p, now) {
416
421
  closed = closeDay(p);
417
422
  applyRecall(p, recallByCue(p, { at: end }));
418
423
  }
424
+ // Evening review (2.7.0, opt-in): from its hour, or at the close of a day that passed without one.
425
+ if (p.review) {
426
+ const done = reviewIfDue(p, end, (p.chronicle ?? []).slice(-2), closed);
427
+ if (done && done.distress > 0)
428
+ feel(p, 'distress', done.distress, 'review', end);
429
+ }
419
430
  if (minuteOfDay(end) === 0) {
420
431
  // Lasting gists (1.8.0, opt-in): at midnight, episodes past the horizon fold into gists.
421
432
  if (p.memory.gists)
@@ -732,6 +743,9 @@ function decisionInputs(p, affordances, id, opts) {
732
743
  necessity: opts.necessity ?? true,
733
744
  tendencyEmotions: tendencyEmotions(p),
734
745
  };
746
+ const perception = fastingCtx(p);
747
+ if (perception)
748
+ ctx.perception = perception;
735
749
  if (opts.suggestion)
736
750
  ctx.suggestion = opts.suggestion;
737
751
  // Only set when voices were given this way, so single-voice records keep their shape.
@@ -1415,6 +1429,6 @@ export function nafsReading(p) {
1415
1429
  * first-person line ("I can do it, but I'll be spent for the descent."). Empty spans without `followedBy`.
1416
1430
  */
1417
1431
  export function bodyForecast(p, aff) {
1418
- const spans = bodyForecastFor(p, aff, lifeModifiers(p));
1432
+ const spans = bodyForecastFor(p, aff, lifeModifiers(p), fastingCtx(p));
1419
1433
  return { spans, line: forecastLine(spans) };
1420
1434
  }
@@ -7,18 +7,18 @@
7
7
  * spentAt, worst), joins the offer's utility; it is pure and draws no randomness. Offers without `followedBy`
8
8
  * get no forecast, so existing hosts continue exactly.
9
9
  *
10
- * The forecast inherits the body model's limits and the person's misperceptions: HF exertion relaxes toward the
11
- * current effort (no cumulative glycogen or muscle-damage store), so a long climb does not by itself leave a
12
- * person more spent for the descent unless sleepiness, hunger, thirst or the descent's own effort do; unfelt
13
- * sleep debt stays unfelt in the forecast; a fast's damped hunger and thirst are not applied (so a fasting
14
- * person forecasts them undamped); host surroundings are not applied (the person's own body rates and life
10
+ * The forecast inherits the body model's limits and the person's misperceptions. Exertion relaxes toward the
11
+ * current effort; only a person with the opt-in fatigue store (2.6.0, `enableFatigueStore`) carries accumulated
12
+ * exertion, so only then does a long climb by itself leave them more spent for the descent; unfelt
13
+ * sleep debt stays unfelt in the forecast; an open fast's damping of hunger and thirst applies (2.6.0) as it is
14
+ * now, held for the whole forecast even if the fast would end during it; host surroundings are not applied (the person's own body rates and life
15
15
  * modifiers are, but not `ambient` parameters). Existing bleeding, illness and healing run in the copy, so an
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
19
  * default.
20
20
  */
21
- import { type BodyForecastSpan } from '../body/index.ts';
21
+ import { type BodyForecastSpan, type BodyPerceptionContext } from '../body/index.ts';
22
22
  import type { Affordance, LifeModifiers, Person, Term } from '../types.ts';
23
23
  export declare const FORECAST_DEFAULTS: {
24
24
  /** Worst perceived readout during a follow-on from which the forecast starts to weigh. */
@@ -32,7 +32,7 @@ export declare const FORECAST_DEFAULTS: {
32
32
  readonly maxMinutes: number;
33
33
  };
34
34
  /** The person's forecast for an offer: the act, then its follow-on spans. Empty without `followedBy`. Pure. */
35
- export declare function bodyForecastFor(p: Person, aff: Affordance, mods: LifeModifiers): BodyForecastSpan[];
35
+ export declare function bodyForecastFor(p: Person, aff: Affordance, mods: LifeModifiers, perception?: BodyPerceptionContext): BodyForecastSpan[];
36
36
  /**
37
37
  * The `forecast:body` term for an offer, or undefined (no follow-on, or not expected to weigh). Absolute by
38
38
  * design: it weighs being in that state *while doing* the follow-on, so a person already sleepy or thirsty is
@@ -40,6 +40,6 @@ export declare function bodyForecastFor(p: Person, aff: Affordance, mods: LifeMo
40
40
  * costs more than staying home). A resting baseline was tried and rejected: thirst and sleepiness rise at rest
41
41
  * too, which cancelled the very case the forecast exists for. Pure.
42
42
  */
43
- export declare function bodyForecastTerm(p: Person, aff: Affordance, mods: LifeModifiers): Term | undefined;
43
+ export declare function bodyForecastTerm(p: Person, aff: Affordance, mods: LifeModifiers, perception?: BodyPerceptionContext): Term | undefined;
44
44
  /** A first-person line for the forecast (narration only). */
45
45
  export declare function forecastLine(spans: readonly BodyForecastSpan[]): string;
@@ -7,18 +7,18 @@
7
7
  * spentAt, worst), joins the offer's utility; it is pure and draws no randomness. Offers without `followedBy`
8
8
  * get no forecast, so existing hosts continue exactly.
9
9
  *
10
- * The forecast inherits the body model's limits and the person's misperceptions: HF exertion relaxes toward the
11
- * current effort (no cumulative glycogen or muscle-damage store), so a long climb does not by itself leave a
12
- * person more spent for the descent unless sleepiness, hunger, thirst or the descent's own effort do; unfelt
13
- * sleep debt stays unfelt in the forecast; a fast's damped hunger and thirst are not applied (so a fasting
14
- * person forecasts them undamped); host surroundings are not applied (the person's own body rates and life
10
+ * The forecast inherits the body model's limits and the person's misperceptions. Exertion relaxes toward the
11
+ * current effort; only a person with the opt-in fatigue store (2.6.0, `enableFatigueStore`) carries accumulated
12
+ * exertion, so only then does a long climb by itself leave them more spent for the descent; unfelt
13
+ * sleep debt stays unfelt in the forecast; an open fast's damping of hunger and thirst applies (2.6.0) as it is
14
+ * now, held for the whole forecast even if the fast would end during it; host surroundings are not applied (the person's own body rates and life
15
15
  * modifiers are, but not `ambient` parameters). Existing bleeding, illness and healing run in the copy, so an
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
19
  * default.
20
20
  */
21
- import { forecastBody } from '../body/index.js';
21
+ import { BODY_DEFAULTS, forecastBody, } from '../body/index.js';
22
22
  import { clamp01, round } from '../core/index.js';
23
23
  export const FORECAST_DEFAULTS = {
24
24
  /** Worst perceived readout during a follow-on from which the forecast starts to weigh. */
@@ -36,7 +36,7 @@ const smooth = (a, b, x) => {
36
36
  return t * t * (3 - 2 * t);
37
37
  };
38
38
  /** The person's forecast for an offer: the act, then its follow-on spans. Empty without `followedBy`. Pure. */
39
- export function bodyForecastFor(p, aff, mods) {
39
+ export function bodyForecastFor(p, aff, mods, perception) {
40
40
  const follow = aff.followedBy;
41
41
  if (!Array.isArray(follow) || follow.length === 0 || !p.body.alive)
42
42
  return [];
@@ -67,7 +67,7 @@ export function bodyForecastFor(p, aff, mods) {
67
67
  load: { effort: clamp01(f.effort), focus: clamp01(f.focus ?? 0), mode: 'awake' },
68
68
  });
69
69
  }
70
- return forecastBody(p, spans, mods);
70
+ return forecastBody(p, spans, mods, BODY_DEFAULTS, 30, perception);
71
71
  }
72
72
  /**
73
73
  * The `forecast:body` term for an offer, or undefined (no follow-on, or not expected to weigh). Absolute by
@@ -76,8 +76,8 @@ export function bodyForecastFor(p, aff, mods) {
76
76
  * costs more than staying home). A resting baseline was tried and rejected: thirst and sleepiness rise at rest
77
77
  * too, which cancelled the very case the forecast exists for. Pure.
78
78
  */
79
- export function bodyForecastTerm(p, aff, mods) {
80
- const spans = bodyForecastFor(p, aff, mods).slice(1);
79
+ export function bodyForecastTerm(p, aff, mods, perception) {
80
+ const spans = bodyForecastFor(p, aff, mods, perception).slice(1);
81
81
  if (spans.length === 0)
82
82
  return undefined;
83
83
  const D = FORECAST_DEFAULTS;
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
@@ -86,6 +86,11 @@ export interface BodyState {
86
86
  illnesses: Illness[];
87
87
  /** Accumulated sleep debt in hours (chronic restriction); under-perceived by the person. */
88
88
  sleepDebt: number;
89
+ /**
90
+ * Accumulated exertion (2.6.0, opt-in via `enableFatigueStore`): 0..1, rising slowly with sustained effort and
91
+ * recovering with rest and faster with sleep. Raises felt fatigue and lowers capacity. Absent = no store.
92
+ */
93
+ depletion?: Unit;
89
94
  asleep: boolean;
90
95
  /** Minute the current sleep or wake period began. */
91
96
  since: Minute;
@@ -827,7 +832,7 @@ export declare const PERSON_SCHEMA = "human/person@1";
827
832
  * and an activity that would cover a closing stretch is reviewed when the stretch begins (`isUnderWay`,
828
833
  * `dutyReviewAt`). Person shape unchanged.
829
834
  */
830
- export declare const ENGINE_VERSION = "2.5.0";
835
+ export declare const ENGINE_VERSION = "2.7.0";
831
836
  /** A pregnancy carried by this person (host-driven; see `family/`). */
832
837
  export interface Pregnancy {
833
838
  fatherId: PersonId;
@@ -994,6 +999,8 @@ export interface Person {
994
999
  prospection?: ProspectionState;
995
1000
  /** Noticed promptings and practised turnings-away (2.4.0, opt-in via `enablePromptings`). Owned by `promptings/`. */
996
1001
  promptings?: PromptingsState;
1002
+ /** Evening self-review and its resolves (2.7.0, opt-in via `enableReview`). Owned by `review/`. */
1003
+ review?: ReviewState;
997
1004
  /** Practice consolidation (1.8.0, opt-in via `enableSkillRetention`). Owned by `skills/`. */
998
1005
  skillRetention?: {
999
1006
  consolidationHours: number;
@@ -1393,8 +1400,12 @@ export interface PersonSpec {
1393
1400
  yearbook?: boolean;
1394
1401
  character?: boolean;
1395
1402
  skillRetention?: boolean;
1403
+ /** Accumulated exertion (2.6.0): the same as `enableFatigueStore(p)` after creation. */
1404
+ fatigueStore?: boolean;
1396
1405
  /** Promptings and turning away (2.4.0): the same as `enablePromptings(p, spec)` after creation. */
1397
1406
  promptings?: PromptingsSpec;
1407
+ /** Evening self-review (2.7.0): the same as `enableReview(p, spec)` after creation. */
1408
+ review?: ReviewSpec;
1398
1409
  };
1399
1410
  }
1400
1411
  /** A known span of exertion after an act (see `Affordance.followedBy`). */
@@ -1447,6 +1458,47 @@ export interface PromptingsState {
1447
1458
  /** Echo gained per prompt under `fault`. Default 0: the recurrence term is a host-set assumption (P3 contested). */
1448
1459
  echoRate: number;
1449
1460
  }
1461
+ /**
1462
+ * How a person reviews a day (2.7.0, a held reading): `accounting` (muhasaba: name the lapse and resolve for its
1463
+ * next occasion), `ruminating` (go over it abstractly: weaker resolve, some distress) or `none` (no review).
1464
+ */
1465
+ export type ReviewStyle = 'accounting' | 'ruminating' | 'none';
1466
+ /** A resolve formed in review: an if-then intention for the next occasion of a missed commitment. */
1467
+ export interface Resolve {
1468
+ /** The commitment's label, else its first action, else its kind. */
1469
+ key: string;
1470
+ label?: string;
1471
+ kind: string;
1472
+ strength: Unit;
1473
+ /** Day the resolve was formed or last renewed. */
1474
+ day: number;
1475
+ }
1476
+ /** What the person concluded in their latest review. */
1477
+ export interface ReviewRecord {
1478
+ day: number;
1479
+ missed: string[];
1480
+ kept: string[];
1481
+ resolved: string[];
1482
+ }
1483
+ /** Owned by `review/`. */
1484
+ export interface ReviewState {
1485
+ style: ReviewStyle;
1486
+ styleSource?: string;
1487
+ /** Minute of the day from which the evening review can run (default 21:00). */
1488
+ at: number;
1489
+ /** Last day reviewed. */
1490
+ lastDay?: number;
1491
+ resolves: Resolve[];
1492
+ last?: ReviewRecord;
1493
+ /** Commitment ids already reviewed (bounded). */
1494
+ seen?: string[];
1495
+ }
1496
+ /** What a host hands `enableReview`. */
1497
+ export interface ReviewSpec {
1498
+ style?: ReviewStyle;
1499
+ styleSource?: string;
1500
+ at?: number;
1501
+ }
1450
1502
  /** What a host hands `enablePromptings`. */
1451
1503
  export interface PromptingsSpec {
1452
1504
  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.5.0';
123
+ export const ENGINE_VERSION = '2.7.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adam0white/human-framework",
3
- "version": "2.5.0",
3
+ "version": "2.7.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",