@adam0white/human-framework 2.4.0 → 2.5.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.
@@ -189,3 +189,20 @@ export interface BodyThresholds {
189
189
  * then refines minute by minute inside the crossing step. Does not mutate `p`.
190
190
  */
191
191
  export declare function nextBodyThreshold(p: Person, load: BodyLoad, mods: LifeModifiers, thresholds?: BodyThresholds, horizon?: number, params?: BodyParams, perception?: BodyPerceptionContext): number;
192
+ /** One forecast span: where it ends and the worst perceived readout during it. */
193
+ export interface BodyForecastSpan {
194
+ label: string;
195
+ endsAt: Minute;
196
+ /** Worst perceived fatigue, sleepiness, hunger or thirst during the span (0..1), and which. */
197
+ worst: Unit;
198
+ worstOf: 'fatigue' | 'sleepiness' | 'hunger' | 'thirst';
199
+ }
200
+ /**
201
+ * The person's own forecast of their body through consecutive spans of known exertion, read as they perceive it
202
+ * (2.5.0). Simulates a cloned body in steps of at most `step` minutes; pure (does not mutate `p`).
203
+ */
204
+ export declare function forecastBody(p: Person, spans: readonly {
205
+ label: string;
206
+ duration: number;
207
+ load: BodyLoad;
208
+ }[], mods: LifeModifiers, params?: BodyParams, step?: number): BodyForecastSpan[];
package/dist/body/body.js CHANGED
@@ -763,3 +763,40 @@ export function nextBodyThreshold(p, load, mods, thresholds = { hunger: 0.7, thi
763
763
  }
764
764
  return Number.POSITIVE_INFINITY;
765
765
  }
766
+ /**
767
+ * The person's own forecast of their body through consecutive spans of known exertion, read as they perceive it
768
+ * (2.5.0). Simulates a cloned body in steps of at most `step` minutes; pure (does not mutate `p`).
769
+ */
770
+ export function forecastBody(p, spans, mods, params = BODY_DEFAULTS, step = 30) {
771
+ const out = [];
772
+ const sim = { ...p, body: cloneBody(p.body) };
773
+ for (const s of spans) {
774
+ const span = { label: s.label, endsAt: sim.now, worst: 0, worstOf: 'fatigue' };
775
+ // Hard internal cap (a week) so a hostile span cannot stall a decision.
776
+ let left = Math.min(7 * MINUTES_PER_DAY, Math.max(0, Number.isFinite(s.duration) ? s.duration : 0));
777
+ advanceBody(sim, 0, s.load, mods, params);
778
+ const r0 = readBody(sim, params).perceived;
779
+ for (const k of ['fatigue', 'sleepiness', 'hunger', 'thirst'])
780
+ if (r0[k] > span.worst) {
781
+ span.worst = r0[k];
782
+ span.worstOf = k;
783
+ }
784
+ while (left > 0 && sim.body.alive) {
785
+ const dt = Math.min(step, left);
786
+ advanceBody(sim, dt, s.load, mods, params);
787
+ sim.now += dt;
788
+ left -= dt;
789
+ const r = readBody(sim, params).perceived;
790
+ for (const k of ['fatigue', 'sleepiness', 'hunger', 'thirst'])
791
+ if (r[k] > span.worst) {
792
+ span.worst = r[k];
793
+ span.worstOf = k;
794
+ }
795
+ }
796
+ if (!sim.body.alive)
797
+ span.worst = 1;
798
+ span.endsAt = sim.now;
799
+ out.push(span);
800
+ }
801
+ return out;
802
+ }
@@ -35,7 +35,7 @@ import { methodEvidence } from '../methods/index.js';
35
35
  import { urgencyOf } from '../needs/index.js';
36
36
  import { partneringTerms } from '../partnering/index.js';
37
37
  import { promptIn, promptingTerms } from '../promptings/index.js';
38
- import { receivedGoalRoute } from '../prospection/index.js';
38
+ import { bodyForecastTerm, receivedGoalRoute } from '../prospection/index.js';
39
39
  import { companionSteadiness, socialTerms } from '../social/index.js';
40
40
  import { PHYSIOLOGICAL_NEEDS, VALUE_KEYS } from '../types.js';
41
41
  import { adviceWeight, resolveChoice, safeUtility, standingAdvice, suggestionTargets, voicesIn, } from '../will/index.js';
@@ -349,6 +349,12 @@ export function consider(p, aff, ctx) {
349
349
  const effort = effortValue(p, aff, ctx);
350
350
  if (effort < 0)
351
351
  push('effort', effort);
352
+ // Bodily forecast (2.5.0): only offers that declare known follow-on exertion.
353
+ if (aff.followedBy?.length) {
354
+ const t = bodyForecastTerm(p, aff, ctx.mods);
355
+ if (t)
356
+ push(t.source, t.value);
357
+ }
352
358
  // Risk: chance × severity × (fear + emotionality).
353
359
  if (aff.risk && aff.risk.chance > 0 && aff.risk.severity > 0) {
354
360
  const fear = emotionLevel(p, 'fear');
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Human Framework v1 public surface. */
2
- export declare const FRAMEWORK_VERSION = "2.4.0";
2
+ export declare const FRAMEWORK_VERSION = "2.5.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,7 +30,7 @@ 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 { consequenceEvidence, PROSPECTION_DEFAULTS } from './prospection/index.ts';
33
+ export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, FORECAST_DEFAULTS, forecastLine, PROSPECTION_DEFAULTS, } from './prospection/index.ts';
34
34
  export * from './restore.ts';
35
35
  export * from './scenarios/index.ts';
36
36
  export * from './sim/index.ts';
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Human Framework v1 public surface. */
2
- export const FRAMEWORK_VERSION = '2.4.0';
2
+ export const FRAMEWORK_VERSION = '2.5.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,7 +26,7 @@ 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 { consequenceEvidence, PROSPECTION_DEFAULTS } from './prospection/index.js';
29
+ export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, FORECAST_DEFAULTS, forecastLine, PROSPECTION_DEFAULTS, } from './prospection/index.js';
30
30
  export * from './restore.js';
31
31
  export * from './scenarios/index.js';
32
32
  export * from './sim/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. 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`. 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,11 @@ 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.4.0': {
62
+ to: '2.5.0',
63
+ note: 'person shape unchanged; offers may declare followedBy spans for a bodily forecast; offers without it are unchanged',
64
+ apply: stamp,
65
+ },
61
66
  '2.3.0': {
62
67
  to: '2.4.0',
63
68
  note: 'promptings are optional and start absent; existing hosts retain exact behavior',
package/dist/person.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type BodyForecastSpan } from './body/index.ts';
1
2
  import { type NafsPattern } from './promptings/index.ts';
2
3
  import { type SkillTransfer } from './skills/index.ts';
3
4
  import type { Activity, Affordance, BodyLoad, BodyReadout, Command, Commitment, DecisionRecord, LearningDomain, Minute, NeedId, NeedReading, NormDefinition, Outcome, Percept, Person, PersonSpec, Retention, Suggestion, SuggestionResolution, Unit } from './types.ts';
@@ -236,3 +237,11 @@ export declare function setRetention(p: Person, retention: Retention): void;
236
237
  * 75:2, 89:27), each with the matching evidence in existing state. Descriptions, never a level or a rank.
237
238
  */
238
239
  export declare function nafsReading(p: Person): NafsPattern[];
240
+ /**
241
+ * The person's own forecast of their body through an offer and its `followedBy` spans (2.5.0, read-only), with a
242
+ * first-person line ("I can do it, but I'll be spent for the descent."). Empty spans without `followedBy`.
243
+ */
244
+ export declare function bodyForecast(p: Person, aff: Affordance): {
245
+ spans: BodyForecastSpan[];
246
+ line: string;
247
+ };
package/dist/person.js CHANGED
@@ -37,7 +37,7 @@ 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 { consumeConsequences, pruneConsequences, receiveConsequences } from './prospection/index.js';
40
+ import { bodyForecastFor, consumeConsequences, forecastLine, pruneConsequences, receiveConsequences, } from './prospection/index.js';
41
41
  import { enableSkillRetention, learnByWatching, practise, seedSkills, } from './skills/index.js';
42
42
  import { advanceSocial, applyReputationBelief, careFor, closeness, judge, markDeceased, relationshipWith, seedRelationships, socialEvent, threatAppraisal, } from './social/index.js';
43
43
  import { ENGINE_VERSION, MAX_MINUTE, PERSON_SCHEMA, PHYSIOLOGICAL_NEEDS, PSYCHOLOGICAL_NEEDS, TRAIT_KEYS, VALUE_KEYS, } from './types.js';
@@ -1410,3 +1410,11 @@ export function nafsReading(p) {
1410
1410
  guilt = Math.max(guilt, e.intensity);
1411
1411
  return nafsPatterns(p, { guiltLoad: guiltLoad(p), guilt });
1412
1412
  }
1413
+ /**
1414
+ * The person's own forecast of their body through an offer and its `followedBy` spans (2.5.0, read-only), with a
1415
+ * first-person line ("I can do it, but I'll be spent for the descent."). Empty spans without `followedBy`.
1416
+ */
1417
+ export function bodyForecast(p, aff) {
1418
+ const spans = bodyForecastFor(p, aff, lifeModifiers(p));
1419
+ return { spans, line: forecastLine(spans) };
1420
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * SCOPE (bodily forecast, 2.5.0): a person anticipates their own body during exertion they know must follow an
3
+ * act ("I can reach the top, but I'll be spent for the descent"). The host declares the follow-on spans on the
4
+ * offer (`Affordance.followedBy`); the person simulates a copy of their body through the act and those spans with
5
+ * the ordinary body model, reads it as they perceive it, and weighs the worst perceived fatigue, sleepiness,
6
+ * hunger or thirst during the follow-on spans. One term, `forecast:body` = -forecastScale × smoothstep(comfortAt,
7
+ * spentAt, worst), joins the offer's utility; it is pure and draws no randomness. Offers without `followedBy`
8
+ * get no forecast, so existing hosts continue exactly.
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
15
+ * modifiers are, but not `ambient` parameters). Existing bleeding, illness and healing run in the copy, so an
16
+ * open wound can read as being spent, and death in the forecast reads as fully spent. The term is absolute
17
+ * (see `bodyForecastTerm`). It does not plan, search routes, schedule rests on the way or
18
+ * predict injuries, and it is not a calibrated estimate of human anticipation. Every number is an engineering
19
+ * default.
20
+ */
21
+ import { type BodyForecastSpan } from '../body/index.ts';
22
+ import type { Affordance, LifeModifiers, Person, Term } from '../types.ts';
23
+ export declare const FORECAST_DEFAULTS: {
24
+ /** Worst perceived readout during a follow-on from which the forecast starts to weigh. */
25
+ readonly comfortAt: 0.8;
26
+ /** Worst perceived readout at which the person expects to be spent (full weight). */
27
+ readonly spentAt: 0.97;
28
+ /** Weight at full expected spending (utility units; the effort term is on the order of 0.1–1). */
29
+ readonly forecastScale: 1.2;
30
+ /** At most this many follow-on spans are read, and at most this many minutes in total. */
31
+ readonly maxSpans: 4;
32
+ readonly maxMinutes: number;
33
+ };
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[];
36
+ /**
37
+ * The `forecast:body` term for an offer, or undefined (no follow-on, or not expected to weigh). Absolute by
38
+ * design: it weighs being in that state *while doing* the follow-on, so a person already sleepy or thirsty is
39
+ * charged for exerting so, while an offer with no declared follow-on is not (a night hike with a declared descent
40
+ * costs more than staying home). A resting baseline was tried and rejected: thirst and sleepiness rise at rest
41
+ * too, which cancelled the very case the forecast exists for. Pure.
42
+ */
43
+ export declare function bodyForecastTerm(p: Person, aff: Affordance, mods: LifeModifiers): Term | undefined;
44
+ /** A first-person line for the forecast (narration only). */
45
+ export declare function forecastLine(spans: readonly BodyForecastSpan[]): string;
@@ -0,0 +1,103 @@
1
+ /**
2
+ * SCOPE (bodily forecast, 2.5.0): a person anticipates their own body during exertion they know must follow an
3
+ * act ("I can reach the top, but I'll be spent for the descent"). The host declares the follow-on spans on the
4
+ * offer (`Affordance.followedBy`); the person simulates a copy of their body through the act and those spans with
5
+ * the ordinary body model, reads it as they perceive it, and weighs the worst perceived fatigue, sleepiness,
6
+ * hunger or thirst during the follow-on spans. One term, `forecast:body` = -forecastScale × smoothstep(comfortAt,
7
+ * spentAt, worst), joins the offer's utility; it is pure and draws no randomness. Offers without `followedBy`
8
+ * get no forecast, so existing hosts continue exactly.
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
15
+ * modifiers are, but not `ambient` parameters). Existing bleeding, illness and healing run in the copy, so an
16
+ * open wound can read as being spent, and death in the forecast reads as fully spent. The term is absolute
17
+ * (see `bodyForecastTerm`). It does not plan, search routes, schedule rests on the way or
18
+ * predict injuries, and it is not a calibrated estimate of human anticipation. Every number is an engineering
19
+ * default.
20
+ */
21
+ import { forecastBody } from '../body/index.js';
22
+ import { clamp01, round } from '../core/index.js';
23
+ export const FORECAST_DEFAULTS = {
24
+ /** Worst perceived readout during a follow-on from which the forecast starts to weigh. */
25
+ comfortAt: 0.8,
26
+ /** Worst perceived readout at which the person expects to be spent (full weight). */
27
+ spentAt: 0.97,
28
+ /** Weight at full expected spending (utility units; the effort term is on the order of 0.1–1). */
29
+ forecastScale: 1.2,
30
+ /** At most this many follow-on spans are read, and at most this many minutes in total. */
31
+ maxSpans: 4,
32
+ maxMinutes: 24 * 60,
33
+ };
34
+ const smooth = (a, b, x) => {
35
+ const t = clamp01((x - a) / (b - a));
36
+ return t * t * (3 - 2 * t);
37
+ };
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) {
40
+ const follow = aff.followedBy;
41
+ if (!Array.isArray(follow) || follow.length === 0 || !p.body.alive)
42
+ return [];
43
+ const D = FORECAST_DEFAULTS;
44
+ const d0 = Number.isFinite(aff.duration) ? Math.min(D.maxMinutes, Math.max(0, aff.duration)) : 0;
45
+ const spans = [
46
+ {
47
+ label: aff.label,
48
+ duration: d0,
49
+ load: {
50
+ effort: clamp01(aff.effort),
51
+ focus: clamp01(aff.focus ?? 0),
52
+ mode: aff.mode ?? 'awake',
53
+ },
54
+ },
55
+ ];
56
+ let total = spans[0]?.duration ?? 0;
57
+ for (const f of follow.slice(0, D.maxSpans)) {
58
+ if (!f || typeof f !== 'object')
59
+ continue;
60
+ const d = Number.isFinite(f.duration) ? Math.max(0, f.duration) : 0;
61
+ if (total + d > D.maxMinutes)
62
+ break;
63
+ total += d;
64
+ spans.push({
65
+ label: f.label ?? 'after',
66
+ duration: d,
67
+ load: { effort: clamp01(f.effort), focus: clamp01(f.focus ?? 0), mode: 'awake' },
68
+ });
69
+ }
70
+ return forecastBody(p, spans, mods);
71
+ }
72
+ /**
73
+ * The `forecast:body` term for an offer, or undefined (no follow-on, or not expected to weigh). Absolute by
74
+ * design: it weighs being in that state *while doing* the follow-on, so a person already sleepy or thirsty is
75
+ * charged for exerting so, while an offer with no declared follow-on is not (a night hike with a declared descent
76
+ * costs more than staying home). A resting baseline was tried and rejected: thirst and sleepiness rise at rest
77
+ * too, which cancelled the very case the forecast exists for. Pure.
78
+ */
79
+ export function bodyForecastTerm(p, aff, mods) {
80
+ const spans = bodyForecastFor(p, aff, mods).slice(1);
81
+ if (spans.length === 0)
82
+ return undefined;
83
+ const D = FORECAST_DEFAULTS;
84
+ const worst = Math.max(...spans.map((s) => s.worst));
85
+ const v = round(-D.forecastScale * smooth(D.comfortAt, D.spentAt, worst));
86
+ return v < 0 ? { source: 'forecast:body', value: v } : undefined;
87
+ }
88
+ /** A first-person line for the forecast (narration only). */
89
+ export function forecastLine(spans) {
90
+ const after = spans.slice(1);
91
+ if (after.length === 0)
92
+ return '';
93
+ let w = after[0];
94
+ for (const s of after)
95
+ if (s.worst > w.worst)
96
+ w = s;
97
+ const what = { fatigue: 'spent', sleepiness: 'too sleepy', hunger: 'too hungry', thirst: 'too thirsty' }[w.worstOf];
98
+ if (w.worst >= FORECAST_DEFAULTS.spentAt)
99
+ return `I can do it, but I'll be ${what} for ${w.label}.`;
100
+ if (w.worst >= FORECAST_DEFAULTS.comfortAt)
101
+ return `I can do it, though ${w.label} will be hard.`;
102
+ return `I'll still have enough for ${w.label}.`;
103
+ }
@@ -1 +1,2 @@
1
+ export * from './forecast.ts';
1
2
  export * from './prospection.ts';
@@ -1 +1,2 @@
1
+ export * from './forecast.js';
1
2
  export * from './prospection.js';
package/dist/types.d.ts CHANGED
@@ -827,7 +827,7 @@ export declare const PERSON_SCHEMA = "human/person@1";
827
827
  * and an activity that would cover a closing stretch is reviewed when the stretch begins (`isUnderWay`,
828
828
  * `dutyReviewAt`). Person shape unchanged.
829
829
  */
830
- export declare const ENGINE_VERSION = "2.4.0";
830
+ export declare const ENGINE_VERSION = "2.5.0";
831
831
  /** A pregnancy carried by this person (host-driven; see `family/`). */
832
832
  export interface Pregnancy {
833
833
  fatherId: PersonId;
@@ -1092,6 +1092,12 @@ export interface Affordance {
1092
1092
  severity: Unit;
1093
1093
  kind: string;
1094
1094
  };
1095
+ /**
1096
+ * Exertion the person knows must follow this act (2.5.0): the way back down, carrying the load home. The
1097
+ * person forecasts their own body through the act and these spans (`prospection/` forecast) and weighs being
1098
+ * spent during them. Absent = no forecast, exactly as before.
1099
+ */
1100
+ followedBy?: FollowOn[];
1095
1101
  /** Set on a partner's mirror offer by the joint protocol in `sim/` (the proposal id). */
1096
1102
  jointId?: string;
1097
1103
  /** Material gain/cost in host units (e.g. coins). Valued via security/achievement/power values. */
@@ -1298,7 +1304,8 @@ export interface Term {
1298
1304
  /** 'need:food', 'norm:salah', 'commitment:c3', 'commitment' (promise inertia of the running activity),
1299
1305
  * 'goal:g1', 'habit', 'emotion:fear:risky' (emotion id, then the tag it acts through), 'effort', 'risk',
1300
1306
  * 'suggestion:player', 'social:p2', 'expectation', 'autonomy' (reactance to a pushing voice),
1301
- * 'promptings:turn' (a practised turning-away), 'promptings:echo' (recurrence under the `fault` reading) */
1307
+ * 'promptings:turn' (a practised turning-away), 'forecast:body' (expected
1308
+ * bodily state during `followedBy` spans), 'promptings:echo' (recurrence under the `fault` reading) */
1302
1309
  source: string;
1303
1310
  value: number;
1304
1311
  }
@@ -1390,6 +1397,14 @@ export interface PersonSpec {
1390
1397
  promptings?: PromptingsSpec;
1391
1398
  };
1392
1399
  }
1400
+ /** A known span of exertion after an act (see `Affordance.followedBy`). */
1401
+ export interface FollowOn {
1402
+ label?: string;
1403
+ /** Minutes. */
1404
+ duration: number;
1405
+ effort: Unit;
1406
+ focus?: Unit;
1407
+ }
1393
1408
  /**
1394
1409
  * How a person understands their own unwanted inner pulls (a held reading, set by the host, with provenance).
1395
1410
  * `whisper`, `inclination` and `intrusion` share mechanics and differ only in vocabulary; `fault` (the scrupulous
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.4.0';
123
+ export const ENGINE_VERSION = '2.5.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adam0white/human-framework",
3
- "version": "2.4.0",
3
+ "version": "2.5.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",