@adam0white/human-framework 2.23.0 → 3.1.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.
Files changed (43) hide show
  1. package/README.md +169 -55
  2. package/dist/agenda/agenda.js +1 -1
  3. package/dist/beliefs/beliefs.js +2 -2
  4. package/dist/cognition/cognition.d.ts +6 -1
  5. package/dist/cognition/cognition.js +1 -1
  6. package/dist/conscience/conscience.d.ts +2 -4
  7. package/dist/conscience/conscience.js +3 -5
  8. package/dist/conversation/conversation.d.ts +0 -41
  9. package/dist/conversation/conversation.js +5 -5
  10. package/dist/core/index.d.ts +0 -1
  11. package/dist/core/index.js +0 -1
  12. package/dist/core/libm.js +20 -0
  13. package/dist/core/math.d.ts +0 -2
  14. package/dist/core/math.js +3 -3
  15. package/dist/family/family.d.ts +5 -6
  16. package/dist/family/family.js +34 -12
  17. package/dist/habits/habits.js +5 -5
  18. package/dist/impression.d.ts +6 -8
  19. package/dist/impression.js +7 -9
  20. package/dist/index.d.ts +2 -1
  21. package/dist/index.js +2 -1
  22. package/dist/longrun.d.ts +2 -2
  23. package/dist/longrun.js +1 -2
  24. package/dist/memory/memory.d.ts +0 -4
  25. package/dist/memory/memory.js +20 -6
  26. package/dist/migrate.js +5 -0
  27. package/dist/person.d.ts +11 -4
  28. package/dist/person.js +24 -7
  29. package/dist/persona.js +2 -1
  30. package/dist/prospection/forecast.d.ts +10 -5
  31. package/dist/prospection/forecast.js +11 -6
  32. package/dist/sim/index.d.ts +1 -1
  33. package/dist/sim/index.js +1 -1
  34. package/dist/sim/longrun.d.ts +24 -6
  35. package/dist/sim/longrun.js +43 -13
  36. package/dist/sim/sim.d.ts +28 -2
  37. package/dist/sim/sim.js +146 -36
  38. package/dist/social/impressions.d.ts +0 -12
  39. package/dist/social/impressions.js +13 -8
  40. package/dist/social/social.js +2 -2
  41. package/dist/types.d.ts +5 -2
  42. package/dist/types.js +5 -2
  43. package/package.json +1 -1
@@ -40,11 +40,6 @@ export declare function conceptionChance(mother: Person, father: Person, days: n
40
40
  * median (an engineering bound against extreme draws). Returns the pregnancy, or undefined if one is already open.
41
41
  */
42
42
  export declare function conceive(mother: Person, father: Person | PersonId, seed: number, at: Minute): Pregnancy | undefined;
43
- /** @deprecated since 2.1: pass `at` (and the optional knobs) positionally. Removed in 3.0. */
44
- export declare function conceive(mother: Person, father: Person | PersonId, opts: {
45
- at: Minute;
46
- seed: number;
47
- }): Pregnancy | undefined;
48
43
  /**
49
44
  * Weeks since conception (0 when not pregnant). Counted from conception, not from the last menstrual period, which
50
45
  * is where obstetric weeks and trimesters are conventionally counted from (about two weeks earlier). The second
@@ -79,6 +74,8 @@ export interface Household {
79
74
  caregivers: readonly Person[];
80
75
  }
81
76
  export interface RaiseOptions {
77
+ /** Explicit household influence through young adulthood. Omit for exact legacy behavior. Pin in host saves. */
78
+ model?: 'warmth-through-25';
82
79
  /** Also move the child's trust in voices the caregivers trust (default true). */
83
80
  voices?: boolean;
84
81
  }
@@ -86,6 +83,8 @@ export interface RaiseOptions {
86
83
  * Apply `minutes` of upbringing in `household` to `child`, at `child.now`. Warmth-weighted: a caregiver with no
87
84
  * warmth toward the child teaches nothing. Values, norm understanding, attachment and (optionally) trust in the
88
85
  * household's voices move by the age plasticities in the SCOPE paragraph. Returns the warmth-weighted
89
- * responsiveness the child experienced (0..1).
86
+ * responsiveness the child experienced (0..1) in legacy mode. With `warmth-through-25`, returns effective
87
+ * responsiveness weighted by mean warmth and age plasticity, approaching zero with either. The host supplies
88
+ * this model again after restoring a save. Raised minutes count even when warmth or plasticity is zero.
90
89
  */
91
90
  export declare function raise(child: Person, household: Household, minutes: number, opts?: RaiseOptions): Unit;
@@ -29,12 +29,20 @@
29
29
  * security starts at 0.6 (58% of nonclinical mothers are secure-autonomous, F12) and moves toward the caregivers'
30
30
  * responsiveness (warmth and mood) at 50% per year, most in the first three years, with a floor of 0.1 of that
31
31
  * plasticity by age 12 (responsiveness and intergenerational correspondence, F10, F11; rates assumed). Optionally a
32
- * child comes to trust the voices the household trusts (`adoptVoiceTrust`). This file writes `values` for minors: one of the
32
+ * child comes to trust the voices the household trusts (`adoptVoiceTrust`). This file writes `values` through young adulthood: one of the
33
33
  * documented exceptions to values being fixed at creation (docs/framework.md); the opt-in `character/` drift is
34
34
  * the other. It does not model siblings'
35
35
  * influence, peers, schooling, abuse, divorce or temperament-by-parenting interactions, and it never assigns a
36
36
  * religious obligation by age: a child holds only the understanding the household lived.
37
37
  *
38
+ * Opt-in `RaiseOptions.model: 'warmth-through-25'` scales norm and attachment relaxation rates by mean warmth,
39
+ * as values and voice trust already do. All influence tapers continuously to zero at 25 using `valuePlasticity`;
40
+ * attachment also keeps its early-life plasticity. The return is responsiveness × mean warmth × value plasticity.
41
+ * These coefficients and the age horizon are engineering assumptions, not empirical calibration or rulings.
42
+ * F7–F11 in research/family-environment-sources.md support the shapes, not these rates. No abuse, schooling or
43
+ * adult culture diffusion is modelled. Without this option, arithmetic and the normalized return stay unchanged.
44
+ * Hosts must pin this option in saved-run configuration and supply it again after restore; no state infers it.
45
+ *
38
46
  * Aptitudes are read by the composite as a multiplier on learning (`aptitudeOf`); they are drawn at birth by
39
47
  * `createChild` (`ChildSpec.aptitudes`).
40
48
  */
@@ -161,19 +169,25 @@ export function conceptionChance(mother, father, days) {
161
169
  return 0;
162
170
  return clamp01(1 - dpow(1 - c, days / FAMILY_DEFAULTS.cycleDays));
163
171
  }
164
- export function conceive(mother, father, seedOrOpts, when) {
165
- const opts = typeof seedOrOpts === 'number' ? { seed: seedOrOpts, at: when } : seedOrOpts;
172
+ /**
173
+ * Record that `mother` has conceived by `father` at `at`. The due date is drawn from a stream derived from `seed`;
174
+ * `seed` also becomes the child's seed at delivery. The drawn length is clamped to 4 sd below and 3 sd above the
175
+ * median (an engineering bound against extreme draws). Returns the pregnancy, or undefined if one is already open.
176
+ */
177
+ export function conceive(mother, father, seed, at) {
178
+ if (!Number.isFinite(at))
179
+ throw new TypeError('conceive requires a finite positional time');
166
180
  const fam = familyOf(mother);
167
181
  if (fam.pregnancy)
168
182
  return undefined;
169
- const rng = createRng((opts.seed ^ 0x9e3779b9) >>> 0);
183
+ const rng = createRng((seed ^ 0x9e3779b9) >>> 0);
170
184
  const F = FAMILY_DEFAULTS;
171
185
  const days = clamp(normal(rng, F.gestationDays, F.gestationSdDays), F.gestationDays - 4 * F.gestationSdDays, F.gestationDays + 3 * F.gestationSdDays);
172
186
  const pregnancy = {
173
187
  fatherId: typeof father === 'string' ? father : father.id,
174
- conceivedAt: opts.at,
175
- dueAt: opts.at + Math.round(days * MINUTES_PER_DAY),
176
- seed: opts.seed,
188
+ conceivedAt: at,
189
+ dueAt: at + Math.round(days * MINUTES_PER_DAY),
190
+ seed,
177
191
  };
178
192
  fam.pregnancy = pregnancy;
179
193
  return { ...pregnancy };
@@ -240,13 +254,17 @@ const relax = (rate, years) => clamp01(1 - dpow(1 - clamp01(rate), years));
240
254
  * Apply `minutes` of upbringing in `household` to `child`, at `child.now`. Warmth-weighted: a caregiver with no
241
255
  * warmth toward the child teaches nothing. Values, norm understanding, attachment and (optionally) trust in the
242
256
  * household's voices move by the age plasticities in the SCOPE paragraph. Returns the warmth-weighted
243
- * responsiveness the child experienced (0..1).
257
+ * responsiveness the child experienced (0..1) in legacy mode. With `warmth-through-25`, returns effective
258
+ * responsiveness weighted by mean warmth and age plasticity, approaching zero with either. The host supplies
259
+ * this model again after restoring a save. Raised minutes count even when warmth or plasticity is zero.
244
260
  */
245
261
  export function raise(child, household, minutes, opts = {}) {
246
262
  if (!(minutes > 0) || !child.body.alive)
247
263
  return 0;
248
264
  const F = FAMILY_DEFAULTS;
249
- const carers = household.caregivers.filter((c) => c.id !== child.id && c.body.alive);
265
+ const continuous = opts.model === 'warmth-through-25';
266
+ const eligible = household.caregivers.filter((c) => c.id !== child.id && c.body.alive);
267
+ const carers = continuous ? [...new Map(eligible.map((c) => [c.id, c])).values()] : eligible;
250
268
  const warmth = carers.map((c) => Math.max(0, relationshipWith(c, child.id).affection));
251
269
  const total = warmth.reduce((s, w) => s + w, 0);
252
270
  const fam = familyOf(child);
@@ -257,6 +275,8 @@ export function raise(child, household, minutes, opts = {}) {
257
275
  const age = ageYears(child);
258
276
  const meanWarmth = total / carers.length;
259
277
  const vp = valuePlasticity(age);
278
+ if (continuous && vp <= 0)
279
+ return 0;
260
280
  // Values.
261
281
  if (vp > 0) {
262
282
  const k = relax(F.valueRatePerYear * meanWarmth, years * vp);
@@ -273,7 +293,7 @@ export function raise(child, household, minutes, opts = {}) {
273
293
  if (vp > 0) {
274
294
  const since = child.now - MINUTES_PER_YEAR;
275
295
  const ids = [...new Set(carers.flatMap((c) => c.conscience.norms.map((n) => n.normId)))].sort();
276
- const k = relax(F.normRatePerYear, years * vp);
296
+ const k = relax(continuous ? F.normRatePerYear * meanWarmth : F.normRatePerYear, years * vp);
277
297
  for (const id of ids) {
278
298
  let target = 0;
279
299
  let standing;
@@ -308,7 +328,9 @@ export function raise(child, household, minutes, opts = {}) {
308
328
  responsiveness += w * clamp01(0.7 * w + 0.3 * ((c.affect.mood.valence + 1) / 2));
309
329
  });
310
330
  responsiveness /= total;
311
- const ka = relax(F.attachmentRatePerYear, years * attachmentPlasticity(age));
331
+ const ka = continuous
332
+ ? relax(F.attachmentRatePerYear * meanWarmth, years * attachmentPlasticity(age) * vp)
333
+ : relax(F.attachmentRatePerYear, years * attachmentPlasticity(age));
312
334
  const att = fam.attachment ?? F.attachmentStart;
313
335
  fam.attachment = clamp01(att + ka * (responsiveness - att));
314
336
  // Trust in the household's voices (an elder, a teacher, a host's voice): not the caregivers themselves.
@@ -331,5 +353,5 @@ export function raise(child, household, minutes, opts = {}) {
331
353
  adoptVoiceTrust(child, id, target / wsum, kv);
332
354
  }
333
355
  }
334
- return clamp01(responsiveness);
356
+ return clamp01(continuous ? responsiveness * meanWarmth * vp : responsiveness);
335
357
  }
@@ -28,11 +28,11 @@
28
28
  * assumption. Does not claim: pharmacology, dependence severity or relapse probabilities.
29
29
  *
30
30
  * SCOPE (habit ease, N4): `habitEase` is a multiplier < 1 on the initiation and attentional parts of an
31
- * option's effort cost for a cued habitual action. Chosen by the discriminating experiment in
32
- * `test/habits.test.ts`: depletion increases habit performance relative to deliberate alternatives (Neal,
33
- * Wood & Drolet 2013, JPSP 104:959); an additive habit term cannot produce that (fatigue costs habitual and
34
- * deliberate options alike), an ease multiplier can. Physical exertion is not eased (habit does not make a
35
- * walk lighter). Does not claim: a measured size of the effect, or that automaticity removes intention.
31
+ * option's effort cost for a cued habitual action. `test/habits-lane.test.ts` compares candidate effort
32
+ * formulas under authored fatigue and sleepiness. That engineering comparison is not a restraint-depletion
33
+ * manipulation, participant-data replication, or full choice experiment; it does not validate a fungible
34
+ * willpower fuel. Physical exertion is not eased (habit does not make a walk lighter). Does not claim:
35
+ * a measured effect size, an empirically established explanation, or that automaticity removes intention.
36
36
  */
37
37
  import { clamp01, decay, dexp, hourOf, minuteOfDay } from '../core/index.js';
38
38
  import { MINUTES_PER_DAY, } from '../types.js';
@@ -6,14 +6,9 @@ export declare function outwardSigns(target: Person): OutwardSigns;
6
6
  /** What `target` says of themself: words hide more than faces (`wordsReserve`), and a limp is not mentioned. */
7
7
  export declare function selfReport(target: Person): OutwardSigns;
8
8
  /** `observer` looks at `target` at `at` (clarity 0..1: a clear look near by, or a distant figure; default 1). */
9
- export declare function glimpseOf(observer: Person, target: Person, at: Minute, clarity?: Unit): void;
10
- /** @deprecated since 2.1: pass `at` (and the optional knobs) positionally. Removed in 3.0. */
11
- export declare function glimpseOf(observer: Person, target: Person, opts: {
12
- at: Minute;
13
- clarity?: Unit;
14
- }): void;
9
+ export declare function glimpsePerson(observer: Person, target: Person, at: Minute, clarity?: Unit): void;
15
10
  /** `observer` has known `target` for a long time (familiarity 0..1): traits and family ties, condensed. */
16
- export declare function acquaintWith(observer: Person, target: Person, familiarity: Unit, at: Minute): void;
11
+ export declare function acquaintPerson(observer: Person, target: Person, familiarity: Unit, at: Minute): void;
17
12
  /** How sure the observer is of the things that drive an answer: traits and the state cues, 0..1. */
18
13
  export declare function impressionConfidence(observer: Person, targetId: PersonId, now: Minute): Unit;
19
14
  /**
@@ -31,9 +26,12 @@ export declare function predictAs(observer: Person, target: Person, affordances:
31
26
  others?: readonly Suggestion[];
32
27
  scarcity?: Unit;
33
28
  now?: Minute;
29
+ forecastContext?: 'conditioned';
34
30
  }): ImpressionPrediction;
35
31
  /** Whether `observer` expects a command on `target` to hold and at what price (pure; see `imagine`). */
36
- export declare function previewCommandAs(observer: Person, target: Person, affordances: readonly Affordance[], cmd: Command, now?: Minute): {
32
+ export declare function previewCommandAs(observer: Person, target: Person, affordances: readonly Affordance[], cmd: Command, now?: Minute, opts?: {
33
+ forecastContext?: 'conditioned';
34
+ }): {
37
35
  outcome: CommandOutcome;
38
36
  confidence: Unit;
39
37
  };
@@ -4,7 +4,7 @@
4
4
  * - `outwardSigns(target)`: perceived fatigue, pain and fear and the felt mood, each reduced by the target's reserve;
5
5
  * a limp (lost moving capacity) shows through any reserve. `selfReport(target)` is the same in words, which hide
6
6
  * more (`IMPRESSION_DEFAULTS.wordsReserve`): "I'm fine".
7
- * - `glimpseOf(observer, target, at, clarity?)` = `glimpse` of the target's outward signs; `acquaintWith` hands the target's
7
+ * - `glimpsePerson(observer, target, at, clarity?)` = `glimpse` of the target's outward signs; `acquaintPerson` hands the target's
8
8
  * traits and family ties (public roles) to `acquaint`.
9
9
  * - `imagine(observer, target, now)`: the target as the observer pictures them — a copy whose traits, fear, pain,
10
10
  * fatigue, mood, ties and trust in voices are the observer's beliefs (each pulled to the prior by its uncertainty)
@@ -61,14 +61,12 @@ export function selfReport(target) {
61
61
  s.pain = round(body.perceived.pain * (1 - clamp01(reserveOf(target, 'pain') * IMPRESSION_DEFAULTS.wordsReserve)));
62
62
  return s;
63
63
  }
64
- export function glimpseOf(observer, target, when, clarity) {
65
- if (typeof when === 'number')
66
- glimpse(observer, target.id, outwardSigns(target), when, clarity);
67
- else
68
- glimpse(observer, target.id, outwardSigns(target), when.at, when.clarity);
64
+ /** `observer` looks at `target` at `at` (clarity 0..1: a clear look near by, or a distant figure; default 1). */
65
+ export function glimpsePerson(observer, target, at, clarity) {
66
+ glimpse(observer, target.id, outwardSigns(target), at, clarity);
69
67
  }
70
68
  /** `observer` has known `target` for a long time (familiarity 0..1): traits and family ties, condensed. */
71
- export function acquaintWith(observer, target, familiarity, at) {
69
+ export function acquaintPerson(observer, target, familiarity, at) {
72
70
  const ties = {};
73
71
  for (const r of target.social.relationships)
74
72
  if (r.roles.some((x) => PUBLIC_ROLES.includes(x)))
@@ -160,10 +158,10 @@ export function predictAs(observer, target, affordances, suggestion, opts = {})
160
158
  };
161
159
  }
162
160
  /** Whether `observer` expects a command on `target` to hold and at what price (pure; see `imagine`). */
163
- export function previewCommandAs(observer, target, affordances, cmd, now = target.now) {
161
+ export function previewCommandAs(observer, target, affordances, cmd, now = target.now, opts = {}) {
164
162
  const model = imagine(observer, target, now);
165
163
  return {
166
- outcome: previewCommand(model, affordances, cmd),
164
+ outcome: previewCommand(model, affordances, cmd, opts),
167
165
  confidence: impressionConfidence(observer, target.id, now),
168
166
  };
169
167
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Human Framework public surface. */
2
- export declare const FRAMEWORK_VERSION = "2.23.0";
2
+ export declare const FRAMEWORK_VERSION = "3.1.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, MakeUpReceipt, PrayerCalendar, PrayerSchedule, PrayerTimes, Retimer, } from './agenda/index.ts';
@@ -46,5 +46,6 @@ export * from './scenarios/index.ts';
46
46
  export * from './sim/index.ts';
47
47
  export * from './skills/index.ts';
48
48
  export * from './social/index.ts';
49
+ export { canonicalTickInputs, type TickInput, type TickInputKey } from './tick-input.ts';
49
50
  export * from './types.ts';
50
51
  export * from './will/index.ts';
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Human Framework public surface. */
2
- export const FRAMEWORK_VERSION = '2.23.0';
2
+ export const FRAMEWORK_VERSION = '3.1.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, isContinuingCommitment, isUnderWay, MAKRUH_DEFAULTS, makruhWindows, missedExcuse, onFinished, owedMakeUps, PRAYER_IMPORTANCE, PURPOSE_DEFAULTS, prayerWindows, pressureReachedAt, promise, proposeGoals, RAMADAN_DEFAULTS, ramadanFast, ramadanMeals, release as releaseCommitment, retimeCommitments, retirePaidMakeUps, revisePurposes, scheduleMakeUp, spanMeetsWindow, suhoorWindow, timesFor, violatesAbstention, } from './agenda/index.js';
@@ -42,5 +42,6 @@ export * from './scenarios/index.js';
42
42
  export * from './sim/index.js';
43
43
  export * from './skills/index.js';
44
44
  export * from './social/index.js';
45
+ export { canonicalTickInputs } from './tick-input.js';
45
46
  export * from './types.js';
46
47
  export * from './will/index.js';
package/dist/longrun.d.ts CHANGED
@@ -87,8 +87,8 @@ export interface RoutineDay {
87
87
  * Live one routine day from `p.now` (see SCOPE). A dead person only moves the clock. Returns what happened.
88
88
  */
89
89
  export declare function routineDay(p: Person, routine: Routine, opts?: RoutineOptions): RoutineDay;
90
- /** A routine, or one chosen per day (by season, age, role). */
91
- export type RoutineSource = Routine | ((p: Person, day: number) => Routine);
90
+ /** Choose a routine for a person at the start of an absolute day. Capture host context in a closure. */
91
+ export type RoutineSource = (p: Person, day: number) => Routine;
92
92
  /**
93
93
  * Live from `p.now` to `to` by routine (see SCOPE): the running activity is dropped and the open chronicle day closed
94
94
  * first, whole days are lived with `routineDay`, a remainder shorter than a day is skipped (`skip`), and past
package/dist/longrun.js CHANGED
@@ -181,7 +181,6 @@ export function routineDay(p, routine, opts = {}) {
181
181
  report.stage = stageAfter;
182
182
  return report;
183
183
  }
184
- const routineOf = (src, p) => typeof src === 'function' ? src(p, dayOf(p.now)) : src;
185
184
  /**
186
185
  * Live from `p.now` to `to` by routine (see SCOPE): the running activity is dropped and the open chronicle day closed
187
186
  * first, whole days are lived with `routineDay`, a remainder shorter than a day is skipped (`skip`), and past
@@ -199,7 +198,7 @@ export function liveRoutine(p, to, routine, opts = {}) {
199
198
  endDay(p);
200
199
  p.activity = null;
201
200
  while (p.now + MINUTES_PER_DAY <= to && p.body.alive) {
202
- const r = routineDay(p, routineOf(routine, p), opts);
201
+ const r = routineDay(p, routine(p, dayOf(p.now)), opts);
203
202
  if (r.episodes.length > 0 || r.onsets.length > 0 || r.stage || r.died)
204
203
  out.push(r);
205
204
  }
@@ -139,10 +139,6 @@ export interface RetellOptions {
139
139
  * from the listener to others (a host may call it again with the listener as teller).
140
140
  */
141
141
  export declare function retell(teller: Person, listener: Person, at: Minute, opts?: RetellOptions): Gist[];
142
- /** @deprecated since 2.1: pass `at` (and the optional knobs) positionally. Removed in 3.0. */
143
- export declare function retell(teller: Person, listener: Person, opts: RetellOptions & {
144
- at: Minute;
145
- }): Gist[];
146
142
  /** Encode an episode. Evicts the weakest (salience × recency) episode beyond the bound. */
147
143
  export declare function remember(p: Person, e: Omit<Episode, 'id' | 'salience'> & {
148
144
  salience?: Unit;
@@ -287,8 +287,22 @@ export const RETELL_DEFAULTS = {
287
287
  /** Gists weaker than this (teller's salience) are not worth telling. */
288
288
  minSalience: 0.1,
289
289
  };
290
- export function retell(teller, listener, when, knobs = {}) {
291
- const opts = typeof when === 'number' ? { ...knobs, at: when } : when;
290
+ /**
291
+ * `teller` tells `listener` at `at` the gist of what mattered to them (2.0, L2: stories passed in a household or around a
292
+ * fire). The listener must have gists on (`enableGists`); otherwise nothing happens. The teller's strongest gists
293
+ * (up to `limit`, optionally only those about `placeIds`) become the listener's own gists with the tag `told`, the
294
+ * teller's valence and summary, and a salience of the teller's × `trust` × `RETELL_DEFAULTS.share`. A listener who
295
+ * already holds a gist about the same thing (kind, action, target and place, whoever was there) from their own
296
+ * experience keeps it unchanged; one told before keeps the
297
+ * stronger telling (retelling the same story does not keep raising it). So a child of parents who fear a place
298
+ * weighs that fear (cognition's `memory` term, by place at half weight) before ever standing there, and their own
299
+ * later visits outweigh it as they would any gist. No randomness; returns the gists written or strengthened.
300
+ * It does not model distortion in the retelling, the listener doubting the teller beyond `trust`, or retelling on
301
+ * from the listener to others (a host may call it again with the listener as teller).
302
+ */
303
+ export function retell(teller, listener, at, opts = {}) {
304
+ if (!Number.isFinite(at))
305
+ throw new TypeError('retell requires a finite positional time');
292
306
  const told = teller.memory.gists;
293
307
  const list = listener.memory.gists;
294
308
  if (!told || !list || teller.id === listener.id)
@@ -312,7 +326,7 @@ export function retell(teller, listener, when, knobs = {}) {
312
326
  if (salience > mine.salience) {
313
327
  mine.salience = salience;
314
328
  mine.weight = salience;
315
- mine.lastAt = opts.at;
329
+ mine.lastAt = at;
316
330
  out.push(mine);
317
331
  }
318
332
  continue;
@@ -321,15 +335,15 @@ export function retell(teller, listener, when, knobs = {}) {
321
335
  listener.memory.nextGist = (listener.memory.nextGist ?? 0) + 1;
322
336
  const g = {
323
337
  id,
324
- at: opts.at,
338
+ at,
325
339
  kind: src.kind,
326
340
  valence: src.valence,
327
341
  salience,
328
342
  summary: src.summary,
329
343
  tags: ['told', ...src.tags.filter((t) => t !== 'told')].slice(0, GIST_DEFAULTS.maxTags),
330
344
  count: 0,
331
- firstAt: opts.at,
332
- lastAt: opts.at,
345
+ firstAt: at,
346
+ lastAt: at,
333
347
  weight: salience,
334
348
  peak: salience,
335
349
  };
package/dist/migrate.js CHANGED
@@ -41,6 +41,11 @@ function renameAmbient(json) {
41
41
  }
42
42
  /** The chain, keyed by the version each step upgrades from. */
43
43
  export const MIGRATIONS = {
44
+ '2.23.0': {
45
+ to: '3.1.0',
46
+ note: 'version stamp only; existing state and RNG are unchanged, and new behaviour requires explicit opt-in',
47
+ apply: stamp,
48
+ },
44
49
  '2.21.0': {
45
50
  to: '2.23.0',
46
51
  note: 'shape unchanged; doze delivery and plain narration fixes apply to opted-in runs',
package/dist/person.d.ts CHANGED
@@ -102,6 +102,13 @@ export interface PerceiveOptions {
102
102
  */
103
103
  export declare function perceive(p: Person, percepts: readonly Percept[], options?: PerceiveOptions): Percept[];
104
104
  export interface DecideOptions {
105
+ /**
106
+ * Q26: forecast with current pregnancy and surroundings held through the bounded horizon.
107
+ * Approximation, not future conditions. Absent preserves legacy. Pin in host saved-run config;
108
+ * restore never infers this choice from Person state. Changes only the bodily forecast term.
109
+ * planRoute and begin pursuit fallback have no bodily forecast term and keep their existing inputs.
110
+ */
111
+ forecastContext?: 'conditioned';
105
112
  suggestion?: Suggestion;
106
113
  /** Several voices in one decision (N1); merged with `suggestion`, one per voice id. */
107
114
  suggestions?: readonly Suggestion[];
@@ -124,7 +131,7 @@ export interface DecideOptions {
124
131
  * How this person would answer a suggestion right now, without changing them or consuming randomness
125
132
  * (hover telegraph). Does not advance time. With `temperature > 0`, read `likelihood` rather than the verdict.
126
133
  */
127
- export declare function predict(p: Person, affordances: readonly Affordance[], suggestion: Suggestion, opts?: {
134
+ export declare function predict(p: Person, affordances: readonly Affordance[], suggestion: Suggestion, opts?: Pick<DecideOptions, 'forecastContext'> & {
128
135
  necessity?: boolean;
129
136
  others?: readonly Suggestion[];
130
137
  scarcity?: Unit;
@@ -138,7 +145,7 @@ export declare function predict(p: Person, affordances: readonly Affordance[], s
138
145
  * or record a trace entry; a later decision on the same standing suggestion counts again (hosts that answer
139
146
  * now should drop the suggestion once it is refused).
140
147
  */
141
- export declare function answerNow(p: Person, affordances: readonly Affordance[], suggestion: Suggestion, opts?: {
148
+ export declare function answerNow(p: Person, affordances: readonly Affordance[], suggestion: Suggestion, opts?: Pick<DecideOptions, 'forecastContext'> & {
142
149
  necessity?: boolean;
143
150
  others?: readonly Suggestion[];
144
151
  scarcity?: Unit;
@@ -178,7 +185,7 @@ export declare function releaseCommand(p: Person, reason?: string): boolean;
178
185
  * (a UI showing "they will do it, but would rather eat" before the host takes control). `margin` is the gap
179
186
  * the hourly autonomy and trust costs scale with.
180
187
  */
181
- export declare function previewCommand(p: Person, affordances: readonly Affordance[], cmd: Command): CommandOutcome;
188
+ export declare function previewCommand(p: Person, affordances: readonly Affordance[], cmd: Command, opts?: Pick<DecideOptions, 'forecastContext'>): CommandOutcome;
182
189
  export interface BeginOptions {
183
190
  /**
184
191
  * Make a commitment at begin (e.g. carrying the injured): a pending commitment for this action and target,
@@ -271,7 +278,7 @@ export declare function nafsReading(p: Person): NafsPattern[];
271
278
  * The person's own forecast of their body through an offer and its `followedBy` spans (2.5.0, read-only), with a
272
279
  * first-person line ("I can do it, but I'll be spent for the descent."). Empty spans without `followedBy`.
273
280
  */
274
- export declare function bodyForecast(p: Person, aff: Affordance): {
281
+ export declare function bodyForecast(p: Person, aff: Affordance, opts?: Pick<DecideOptions, 'forecastContext'>): {
275
282
  spans: BodyForecastSpan[];
276
283
  line: string;
277
284
  };
package/dist/person.js CHANGED
@@ -29,7 +29,8 @@ import { ageCharacter, enableCharacterChange, noteCharacterDay, noteCharacterSoc
29
29
  import { closeDay, enableYearbook, endDay, noteAnswer, noteCommitments, noteDecision, noteMood, noteOutcome, trimChronicle, } from './chronicle/index.js';
30
30
  import { decide as cognitionDecide, desperationOf, prospectiveRoute, scoreAll } from './cognition/index.js';
31
31
  import { createConscience, guiltLoad, heldNorms, normVeto, recordDeed, recordRepair, repent, } from './conscience/index.js';
32
- import { clamp01, clampSigned, createRng, dayOf, isObj, keysOf, minuteOfDay, round } from './core/index.js';
32
+ import { clamp01, clampSigned, createRng, dayOf, isObj, minuteOfDay, round } from './core/index.js';
33
+ import { keysOf } from './core/keys.js';
33
34
  import { enableCuriosity, noteCuriosity } from './curiosity/index.js';
34
35
  import { ambientBodyParams, ambientModifiers, ambientMood, ambientNeeds } from './environment/index.js';
35
36
  import { aptitudeOf, createFamily, pregnancyModifiers } from './family/index.js';
@@ -905,6 +906,15 @@ function currentOffer(p) {
905
906
  return undefined;
906
907
  return { ...act.affordance, duration: act.endsAt - p.now };
907
908
  }
909
+ /** Q26: compose one held snapshot for all public forecast paths; does not alter Person or ordinary scoring. */
910
+ function bodyForecastContext(p, opts) {
911
+ return opts.forecastContext === 'conditioned'
912
+ ? {
913
+ mods: segmentModifiers(p),
914
+ params: p.ambient ? ambientBodyParams(p, BODY_DEFAULTS) : BODY_DEFAULTS,
915
+ }
916
+ : { mods: lifeModifiers(p), params: BODY_DEFAULTS };
917
+ }
908
918
  /** Offers (plus the current activity) and the scoring context for a decision at `p.now`. Reads only. */
909
919
  function decisionInputs(p, affordances, id, opts) {
910
920
  const list = [...affordances];
@@ -933,6 +943,8 @@ function decisionInputs(p, affordances, id, opts) {
933
943
  const perception = fastingCtx(p);
934
944
  if (perception)
935
945
  ctx.perception = perception;
946
+ if (opts.forecastContext === 'conditioned')
947
+ ctx.forecastContext = bodyForecastContext(p, opts);
936
948
  if (opts.suggestion)
937
949
  ctx.suggestion = opts.suggestion;
938
950
  // Only set when voices were given this way, so single-voice records keep their shape.
@@ -965,6 +977,8 @@ function commandOf(c) {
965
977
  export function predict(p, affordances, suggestion, opts = {}) {
966
978
  const others = (opts.others ?? []).filter((s) => s.voiceId !== suggestion.voiceId);
967
979
  const decideOpts = { suggestion };
980
+ if (opts.forecastContext !== undefined)
981
+ decideOpts.forecastContext = opts.forecastContext;
968
982
  if (others.length > 0)
969
983
  decideOpts.suggestions = others;
970
984
  if (opts.necessity !== undefined)
@@ -989,6 +1003,8 @@ export function predict(p, affordances, suggestion, opts = {}) {
989
1003
  export function answerNow(p, affordances, suggestion, opts = {}) {
990
1004
  const others = (opts.others ?? []).filter((s) => s.voiceId !== suggestion.voiceId);
991
1005
  const decideOpts = { suggestion };
1006
+ if (opts.forecastContext !== undefined)
1007
+ decideOpts.forecastContext = opts.forecastContext;
992
1008
  if (others.length > 0)
993
1009
  decideOpts.suggestions = others;
994
1010
  if (opts.necessity !== undefined)
@@ -1161,8 +1177,8 @@ export function releaseCommand(p, reason = 'released') {
1161
1177
  * (a UI showing "they will do it, but would rather eat" before the host takes control). `margin` is the gap
1162
1178
  * the hourly autonomy and trust costs scale with.
1163
1179
  */
1164
- export function previewCommand(p, affordances, cmd) {
1165
- const { list, ctx } = decisionInputs(p, affordances, 'preview', { command: cmd });
1180
+ export function previewCommand(p, affordances, cmd, opts = {}) {
1181
+ const { list, ctx } = decisionInputs(p, affordances, 'preview', { ...opts, command: cmd });
1166
1182
  const { considered, willCtx } = scoreAll(p, list, ctx);
1167
1183
  return resolveCommand(p, considered, willCtx);
1168
1184
  }
@@ -1390,7 +1406,7 @@ function respondToConscience(p, act, now, completed, minutes) {
1390
1406
  // Worship closes breaches against God alone; guilt toward a wronged person waits for repair.
1391
1407
  const repented = new Set();
1392
1408
  for (const b of p.conscience.breaches) {
1393
- if (!b.repaired && b.victimId === undefined && repent(p, b.id, now))
1409
+ if (!b.repaired && b.victimId === undefined && repent(p, b.id))
1394
1410
  repented.add(b.normId);
1395
1411
  }
1396
1412
  for (const b of p.conscience.breaches)
@@ -1401,7 +1417,7 @@ function respondToConscience(p, act, now, completed, minutes) {
1401
1417
  regulate(p, minutes, 'worship');
1402
1418
  }
1403
1419
  if (completed && (tags.includes('repair') || tags.includes('apologize')) && aff.targetId !== undefined) {
1404
- recordRepair(p, aff.targetId, now);
1420
+ recordRepair(p, aff.targetId);
1405
1421
  release(p, { id: 'guilt', targetId: aff.targetId });
1406
1422
  socialEvent(p, { at: now, kind: 'apology', otherId: aff.targetId, byMe: true, magnitude: 0.7 });
1407
1423
  }
@@ -1694,8 +1710,9 @@ export function nafsReading(p) {
1694
1710
  * The person's own forecast of their body through an offer and its `followedBy` spans (2.5.0, read-only), with a
1695
1711
  * first-person line ("I can do it, but I'll be spent for the descent."). Empty spans without `followedBy`.
1696
1712
  */
1697
- export function bodyForecast(p, aff) {
1698
- const spans = bodyForecastFor(p, aff, lifeModifiers(p), fastingCtx(p));
1713
+ export function bodyForecast(p, aff, opts = {}) {
1714
+ const { mods, params } = bodyForecastContext(p, opts);
1715
+ const spans = bodyForecastFor(p, aff, mods, fastingCtx(p), params);
1699
1716
  return {
1700
1717
  spans,
1701
1718
  line: forecastLine(spans, forecastThresholds(aff), { plainEnglish: p.lexicon?.plainEnglish }),
package/dist/persona.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * identity, age, family and health compatibility before returning a new Person. Neither input is mutated.
5
5
  * This is not a world migration, identity registry, transport, hash, authentication or general calendar mapper.
6
6
  * v1 admits zero-duration travel and healthy compatible bodies only; family/bonds remain host-authored and must
7
- * match source provenance. Portable v1 accepts its published 2.14.1 and 2.17.0 stamps and the current engine;
7
+ * match source provenance. Portable v1 accepts its published 2.14.1, 2.17.0, 2.21.0 and 2.23.0 stamps and the current engine;
8
8
  * the bodily opt-ins change no portable field semantics. Pregnancy and nonempty temporal bonds are unsupported in v1. All other world-dependent stores stay with the source and are disclosed as omitted.
9
9
  */
10
10
  import { hasUnresolvedMakeUps, sameChain } from './agenda/agenda.js';
@@ -188,6 +188,7 @@ export function validatePersonaProjection(input) {
188
188
  keys(input.source, ['schema', 'engine', 'id', 'now']);
189
189
  // The 2.14.1 and 2.17.0 portable fields retain their semantics across the bodily opt-ins.
190
190
  const compatibleEngine = input.source.engine === ENGINE_VERSION ||
191
+ input.source.engine === '2.23.0' ||
191
192
  input.source.engine === '2.21.0' ||
192
193
  input.source.engine === '2.17.0' ||
193
194
  input.source.engine === '2.14.1';
@@ -11,15 +11,20 @@
11
11
  * current effort; only a person with the opt-in fatigue store (2.6.0, `enableFatigueStore`) carries accumulated
12
12
  * exertion, so only then does a long climb by itself leave them more spent for the descent; unfelt
13
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
- * modifiers are, but not `ambient` parameters). Existing bleeding, illness and healing run in the copy, so an
14
+ * now, held for the whole forecast even if the fast would end during it. By default host surroundings are not
15
+ * applied. Q26: explicit per-call conditioned forecasts instead hold current age, pregnancy and ambient
16
+ * modifiers AND ambient body parameters through the bounded horizon (four follow-on spans, 1440 minutes).
17
+ * This is a frozen current-conditions approximation: no future weather, pregnancy progression or world
18
+ * lookahead. Readouts remain perceived; integration and interpolation retain the existing body model's limits.
19
+ * Direct helpers retain caller-supplied modifiers and default body parameters unless given other parameters.
20
+ * Existing bleeding, illness and healing run in the copy, so an
16
21
  * open wound can read as being spent, and death in the forecast reads as fully spent. The term is absolute
17
22
  * (see `bodyForecastTerm`). It does not plan, search routes, schedule rests on the way or
18
23
  * predict injuries, and it is not a calibrated estimate of human anticipation. Every number is an engineering
19
24
  * default. 2.8.0: an offer may carry its own thresholds (`Affordance.forecastAt`, from The Glance's night
20
25
  * keepers); offers without it read exactly as before.
21
26
  */
22
- import { type BodyForecastSpan, type BodyPerceptionContext } from '../body/index.ts';
27
+ import { type BodyForecastSpan, type BodyParams, type BodyPerceptionContext } from '../body/index.ts';
23
28
  import type { Affordance, LifeModifiers, Person, Term } from '../types.ts';
24
29
  export declare const FORECAST_DEFAULTS: {
25
30
  /** Worst perceived readout during a follow-on from which the forecast starts to weigh. */
@@ -41,7 +46,7 @@ export declare function forecastThresholds(aff: Pick<Affordance, 'forecastAt'>):
41
46
  spentAt: number;
42
47
  };
43
48
  /** The person's forecast for an offer: the act, then its follow-on spans. Empty without `followedBy`. Pure. */
44
- export declare function bodyForecastFor(p: Person, aff: Affordance, mods: LifeModifiers, perception?: BodyPerceptionContext): BodyForecastSpan[];
49
+ export declare function bodyForecastFor(p: Person, aff: Affordance, mods: LifeModifiers, perception?: BodyPerceptionContext, params?: BodyParams): BodyForecastSpan[];
45
50
  /**
46
51
  * The `forecast:body` term for an offer, or undefined (no follow-on, or not expected to weigh). Absolute by
47
52
  * design: it weighs being in that state *while doing* the follow-on, so a person already sleepy or thirsty is
@@ -49,7 +54,7 @@ export declare function bodyForecastFor(p: Person, aff: Affordance, mods: LifeMo
49
54
  * costs more than staying home). A resting baseline was tried and rejected: thirst and sleepiness rise at rest
50
55
  * too, which cancelled the very case the forecast exists for. Pure.
51
56
  */
52
- export declare function bodyForecastTerm(p: Person, aff: Affordance, mods: LifeModifiers, perception?: BodyPerceptionContext): Term | undefined;
57
+ export declare function bodyForecastTerm(p: Person, aff: Affordance, mods: LifeModifiers, perception?: BodyPerceptionContext, params?: BodyParams): Term | undefined;
53
58
  /** A first-person line for the forecast (narration only); `at` is the offer's thresholds (2.8.0, default as before). */
54
59
  export declare function forecastLine(spans: readonly BodyForecastSpan[], at?: {
55
60
  comfortAt: number;
@@ -11,8 +11,13 @@
11
11
  * current effort; only a person with the opt-in fatigue store (2.6.0, `enableFatigueStore`) carries accumulated
12
12
  * exertion, so only then does a long climb by itself leave them more spent for the descent; unfelt
13
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
- * modifiers are, but not `ambient` parameters). Existing bleeding, illness and healing run in the copy, so an
14
+ * now, held for the whole forecast even if the fast would end during it. By default host surroundings are not
15
+ * applied. Q26: explicit per-call conditioned forecasts instead hold current age, pregnancy and ambient
16
+ * modifiers AND ambient body parameters through the bounded horizon (four follow-on spans, 1440 minutes).
17
+ * This is a frozen current-conditions approximation: no future weather, pregnancy progression or world
18
+ * lookahead. Readouts remain perceived; integration and interpolation retain the existing body model's limits.
19
+ * Direct helpers retain caller-supplied modifiers and default body parameters unless given other parameters.
20
+ * Existing bleeding, illness and healing run in the copy, so an
16
21
  * open wound can read as being spent, and death in the forecast reads as fully spent. The term is absolute
17
22
  * (see `bodyForecastTerm`). It does not plan, search routes, schedule rests on the way or
18
23
  * predict injuries, and it is not a calibrated estimate of human anticipation. Every number is an engineering
@@ -52,7 +57,7 @@ const smooth = (a, b, x) => {
52
57
  return t * t * (3 - 2 * t);
53
58
  };
54
59
  /** The person's forecast for an offer: the act, then its follow-on spans. Empty without `followedBy`. Pure. */
55
- export function bodyForecastFor(p, aff, mods, perception) {
60
+ export function bodyForecastFor(p, aff, mods, perception, params = BODY_DEFAULTS) {
56
61
  const follow = aff.followedBy;
57
62
  if (!Array.isArray(follow) || follow.length === 0 || !p.body.alive)
58
63
  return [];
@@ -83,7 +88,7 @@ export function bodyForecastFor(p, aff, mods, perception) {
83
88
  load: { effort: clamp01(f.effort), focus: clamp01(f.focus ?? 0), mode: 'awake' },
84
89
  });
85
90
  }
86
- return forecastBody(p, spans, mods, BODY_DEFAULTS, 30, perception);
91
+ return forecastBody(p, spans, mods, params, 30, perception);
87
92
  }
88
93
  /**
89
94
  * The `forecast:body` term for an offer, or undefined (no follow-on, or not expected to weigh). Absolute by
@@ -92,8 +97,8 @@ export function bodyForecastFor(p, aff, mods, perception) {
92
97
  * costs more than staying home). A resting baseline was tried and rejected: thirst and sleepiness rise at rest
93
98
  * too, which cancelled the very case the forecast exists for. Pure.
94
99
  */
95
- export function bodyForecastTerm(p, aff, mods, perception) {
96
- const spans = bodyForecastFor(p, aff, mods, perception).slice(1);
100
+ export function bodyForecastTerm(p, aff, mods, perception, params = BODY_DEFAULTS) {
101
+ const spans = bodyForecastFor(p, aff, mods, perception, params).slice(1);
97
102
  if (spans.length === 0)
98
103
  return undefined;
99
104
  const D = FORECAST_DEFAULTS;
@@ -1,2 +1,2 @@
1
1
  export * from './longrun.ts';
2
- export * from './sim.ts';
2
+ export { acceptJoint, addPerson, birth, type Community, type CommunityState, communityState, createCommunity, declineJoint, interruptPerson, interrupts, type JointProposal, jointSuccessChance, type LifecourseOptions, mirrorAffordance, nextEventAt, preview, proposeJoint, runSilent, SIM_DEFAULTS, type SilentRun, type SimEvent, type SimEventKind, type StepOptions, sharesPlace, standingHeard, stepCommunity, tellDeath, type World, } from './sim.ts';