@adam0white/human-framework 3.0.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.
package/README.md CHANGED
@@ -4,13 +4,15 @@ HF is a TypeScript library for simulated people in games and headless simulation
4
4
  possible actions. Each person weighs needs, habits, duties, goals and suggestions, then chooses.
5
5
  A player can suggest an action; the person can defer or refuse it.
6
6
 
7
- HF 3.0.0 uses utility scoring and seeded randomness, with no LLM in the actor loop and no runtime
7
+ HF 3.1.0 uses utility scoring and seeded randomness, with no LLM in the actor loop and no runtime
8
8
  dependencies. Decision records expose scored terms and template narration. Person state is plain JSON.
9
9
  This is an authored simulation model, not a validated predictor of real people.
10
10
 
11
11
  ## Migrating to v3
12
12
 
13
- The API migration keeps engine `2.23.0`. Relative to HF 2.23.0, it does not change seeded draws, numeric rules or saved Person state. The package version is `3.0.0`; publication is recorded separately in the release history.
13
+ The HF 3.0 API migration kept engine `2.23.0`. Relative to HF 2.23.0, that release changed no seeded draws, numeric rules or saved Person state.
14
+
15
+ HF 3.1 uses engine `3.1.0` and migrates earlier supported snapshots by preserving admitted state. Community death handling, warmth-through-25 upbringing and conditioned bodily forecasts require explicit host options, pinned again after restore. Omitted options preserve HF 3.0 continuation apart from the engine stamp. These are authored simulation rules, not validated predictions of real people.
14
16
 
15
17
  | Before | v3 |
16
18
  | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- |
@@ -57,10 +59,10 @@ This is a bounded host convention, not transport, consensus or fairness. It read
57
59
 
58
60
  ## Quick start
59
61
 
60
- Use Node 24 or newer. In an empty directory, install the version this README describes:
62
+ Use Node 24 or newer and TypeScript 7 or newer. In an empty directory, install the version this README describes:
61
63
 
62
64
  ```sh
63
- npm install @adam0white/human-framework@3.0.0
65
+ npm install @adam0white/human-framework@3.1.0
64
66
  ```
65
67
 
66
68
  Save these 20 lines as `quickstart.mjs`, then run `node quickstart.mjs`:
@@ -265,7 +267,7 @@ reference host: `createVillage`.
265
267
 
266
268
  There are two version numbers, and they move independently.
267
269
 
268
- - **The release version** is the package version (`package.json`, currently `3.0.0`) and `FRAMEWORK_VERSION`, the same
270
+ - **The release version** is the package version (`package.json`, currently `3.1.0`) and `FRAMEWORK_VERSION`, the same
269
271
  string compiled into the build; a test keeps them equal. It versions the public API under
270
272
  [semver](https://semver.org/): a breaking change to an exported name or signature bumps the major, a new
271
273
  faculty or export the minor, a fix the patch. Each release is a tag `vX.Y.Z` and a GitHub release whose notes
@@ -637,3 +639,32 @@ Use `gestureVoiceTrust(p, voiceId, amount, { reason, at?, action? })` for an exp
637
639
  `confirmObservedReputation(p, prop, truth, now, table?)` replaces the manual `credence → confirm → applyReputationBelief` sequence. Supply only a fact the person could observe. It does not run attention or invent a moral judgement.
638
640
 
639
641
  `qalbReading(p)` is a read-only, sourced narration lens over existing records. Its detached categorical rows are this person's recorded understanding and evidence. They are never a spiritual health score, a ruling, a rank, or a decision input. Do not present mood, low social trust or missing memories as evidence of poor faith.
642
+
643
+ ## fdlibm notices
644
+
645
+ `dist/core/libm.js` ports `e_exp.c`, `e_log.c`, `k_cos.c`, `k_sin.c` and `e_rem_pio2.c`
646
+ from Sun/FreeBSD fdlibm. The original notices are preserved below and in that module.
647
+ Sources: [Netlib fdlibm](https://netlib.org/fdlibm/) and
648
+ [FreeBSD e_exp.c](https://cgit.freebsd.org/src/tree/lib/msun/src/e_exp.c).
649
+
650
+ ```text
651
+ * fdlibm notices: e_exp.c (Sun 2004); e_log.c, k_cos.c, k_sin.c and e_rem_pio2.c (Sun 1993).
652
+ * Sources: https://netlib.org/fdlibm/ and FreeBSD lib/msun/src/e_exp.c.
653
+ * ====================================================
654
+ * Copyright (C) 2004 by Sun Microsystems, Inc. All rights reserved.
655
+ *
656
+ * Permission to use, copy, modify, and distribute this
657
+ * software is freely granted, provided that this notice
658
+ * is preserved.
659
+ * ====================================================
660
+ *
661
+ * ====================================================
662
+ * Copyright (C) 1993 by Sun Microsystems, Inc. All rights reserved.
663
+ *
664
+ * Developed at SunSoft, a Sun Microsystems, Inc. business.
665
+ * Permission to use, copy, modify, and distribute this
666
+ * software is freely granted, provided that this notice
667
+ * is preserved.
668
+ * ====================================================
669
+
670
+ ```
@@ -35,7 +35,7 @@
35
35
  * 1971), so idle choice stops cycling short consumption at full hydration (The Rope, owner Kestrel export
36
36
  * 2026-10-19). It models no bladder, sensory-specific satiety by food, or overeating for comfort.
37
37
  */
38
- import type { BodyPerceptionContext } from '../body/index.ts';
38
+ import type { BodyParams, BodyPerceptionContext } from '../body/index.ts';
39
39
  import { type HabitContext } from '../habits/index.ts';
40
40
  import { type PromptingSeen } from '../promptings/index.ts';
41
41
  import type { Affordance, BodyReadout, Command, Considered, DecisionRecord, LifeModifiers, Minute, NeedReading, Person, Suggestion, Term, Unit } from '../types.ts';
@@ -150,6 +150,11 @@ export interface ConsiderContext {
150
150
  scarcity?: Unit;
151
151
  /** The body's perception context now (an open fast), for the bodily forecast (2.6.0). */
152
152
  perception?: BodyPerceptionContext;
153
+ /** Q26: held current conditions, used only by the bodily forecast, never other decision terms. */
154
+ forecastContext?: {
155
+ mods: LifeModifiers;
156
+ params: BodyParams;
157
+ };
153
158
  }
154
159
  /**
155
160
  * Standing advice terms for one option: per source, the strongest matching entry's weight × trust in the source
@@ -409,7 +409,7 @@ export function consider(p, aff, ctx) {
409
409
  push('effort', effort);
410
410
  // Bodily forecast (2.5.0): only offers that declare known follow-on exertion.
411
411
  if (aff.followedBy?.length) {
412
- const t = bodyForecastTerm(p, aff, ctx.mods, ctx.perception);
412
+ const t = bodyForecastTerm(p, aff, ctx.forecastContext?.mods ?? ctx.mods, ctx.perception, ctx.forecastContext?.params);
413
413
  if (t)
414
414
  push(t.source, t.value);
415
415
  }
package/dist/core/libm.js CHANGED
@@ -1,4 +1,24 @@
1
1
  // biome-ignore-all lint/correctness/noPrecisionLoss: fdlibm constants are kept digit for digit as published; each parses to the intended double.
2
+ /*
3
+ * fdlibm notices: e_exp.c (Sun 2004); e_log.c, k_cos.c, k_sin.c and e_rem_pio2.c (Sun 1993).
4
+ * Sources: https://netlib.org/fdlibm/ and FreeBSD lib/msun/src/e_exp.c.
5
+ * ====================================================
6
+ * Copyright (C) 2004 by Sun Microsystems, Inc. All rights reserved.
7
+ *
8
+ * Permission to use, copy, modify, and distribute this
9
+ * software is freely granted, provided that this notice
10
+ * is preserved.
11
+ * ====================================================
12
+ *
13
+ * ====================================================
14
+ * Copyright (C) 1993 by Sun Microsystems, Inc. All rights reserved.
15
+ *
16
+ * Developed at SunSoft, a Sun Microsystems, Inc. business.
17
+ * Permission to use, copy, modify, and distribute this
18
+ * software is freely granted, provided that this notice
19
+ * is preserved.
20
+ * ====================================================
21
+ */
2
22
  /**
3
23
  * Platform-independent transcendental functions for the engine.
4
24
  *
@@ -74,6 +74,8 @@ export interface Household {
74
74
  caregivers: readonly Person[];
75
75
  }
76
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';
77
79
  /** Also move the child's trust in voices the caregivers trust (default true). */
78
80
  voices?: boolean;
79
81
  }
@@ -81,6 +83,8 @@ export interface RaiseOptions {
81
83
  * Apply `minutes` of upbringing in `household` to `child`, at `child.now`. Warmth-weighted: a caregiver with no
82
84
  * warmth toward the child teaches nothing. Values, norm understanding, attachment and (optionally) trust in the
83
85
  * household's voices move by the age plasticities in the SCOPE paragraph. Returns the warmth-weighted
84
- * 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.
85
89
  */
86
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
  */
@@ -246,13 +254,17 @@ const relax = (rate, years) => clamp01(1 - dpow(1 - clamp01(rate), years));
246
254
  * Apply `minutes` of upbringing in `household` to `child`, at `child.now`. Warmth-weighted: a caregiver with no
247
255
  * warmth toward the child teaches nothing. Values, norm understanding, attachment and (optionally) trust in the
248
256
  * household's voices move by the age plasticities in the SCOPE paragraph. Returns the warmth-weighted
249
- * 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.
250
260
  */
251
261
  export function raise(child, household, minutes, opts = {}) {
252
262
  if (!(minutes > 0) || !child.body.alive)
253
263
  return 0;
254
264
  const F = FAMILY_DEFAULTS;
255
- 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;
256
268
  const warmth = carers.map((c) => Math.max(0, relationshipWith(c, child.id).affection));
257
269
  const total = warmth.reduce((s, w) => s + w, 0);
258
270
  const fam = familyOf(child);
@@ -263,6 +275,8 @@ export function raise(child, household, minutes, opts = {}) {
263
275
  const age = ageYears(child);
264
276
  const meanWarmth = total / carers.length;
265
277
  const vp = valuePlasticity(age);
278
+ if (continuous && vp <= 0)
279
+ return 0;
266
280
  // Values.
267
281
  if (vp > 0) {
268
282
  const k = relax(F.valueRatePerYear * meanWarmth, years * vp);
@@ -279,7 +293,7 @@ export function raise(child, household, minutes, opts = {}) {
279
293
  if (vp > 0) {
280
294
  const since = child.now - MINUTES_PER_YEAR;
281
295
  const ids = [...new Set(carers.flatMap((c) => c.conscience.norms.map((n) => n.normId)))].sort();
282
- const k = relax(F.normRatePerYear, years * vp);
296
+ const k = relax(continuous ? F.normRatePerYear * meanWarmth : F.normRatePerYear, years * vp);
283
297
  for (const id of ids) {
284
298
  let target = 0;
285
299
  let standing;
@@ -314,7 +328,9 @@ export function raise(child, household, minutes, opts = {}) {
314
328
  responsiveness += w * clamp01(0.7 * w + 0.3 * ((c.affect.mood.valence + 1) / 2));
315
329
  });
316
330
  responsiveness /= total;
317
- 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));
318
334
  const att = fam.attachment ?? F.attachmentStart;
319
335
  fam.attachment = clamp01(att + ka * (responsiveness - att));
320
336
  // Trust in the household's voices (an elder, a teacher, a host's voice): not the caregivers themselves.
@@ -337,5 +353,5 @@ export function raise(child, household, minutes, opts = {}) {
337
353
  adoptVoiceTrust(child, id, target / wsum, kv);
338
354
  }
339
355
  }
340
- return clamp01(responsiveness);
356
+ return clamp01(continuous ? responsiveness * meanWarmth * vp : responsiveness);
341
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';
@@ -26,9 +26,12 @@ export declare function predictAs(observer: Person, target: Person, affordances:
26
26
  others?: readonly Suggestion[];
27
27
  scarcity?: Unit;
28
28
  now?: Minute;
29
+ forecastContext?: 'conditioned';
29
30
  }): ImpressionPrediction;
30
31
  /** Whether `observer` expects a command on `target` to hold and at what price (pure; see `imagine`). */
31
- 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
+ }): {
32
35
  outcome: CommandOutcome;
33
36
  confidence: Unit;
34
37
  };
@@ -158,10 +158,10 @@ export function predictAs(observer, target, affordances, suggestion, opts = {})
158
158
  };
159
159
  }
160
160
  /** Whether `observer` expects a command on `target` to hold and at what price (pure; see `imagine`). */
161
- export function previewCommandAs(observer, target, affordances, cmd, now = target.now) {
161
+ export function previewCommandAs(observer, target, affordances, cmd, now = target.now, opts = {}) {
162
162
  const model = imagine(observer, target, now);
163
163
  return {
164
- outcome: previewCommand(model, affordances, cmd),
164
+ outcome: previewCommand(model, affordances, cmd, opts),
165
165
  confidence: impressionConfidence(observer, target.id, now),
166
166
  };
167
167
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Human Framework public surface. */
2
- export declare const FRAMEWORK_VERSION = "3.0.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';
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Human Framework public surface. */
2
- export const FRAMEWORK_VERSION = '3.0.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';
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
@@ -906,6 +906,15 @@ function currentOffer(p) {
906
906
  return undefined;
907
907
  return { ...act.affordance, duration: act.endsAt - p.now };
908
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
+ }
909
918
  /** Offers (plus the current activity) and the scoring context for a decision at `p.now`. Reads only. */
910
919
  function decisionInputs(p, affordances, id, opts) {
911
920
  const list = [...affordances];
@@ -934,6 +943,8 @@ function decisionInputs(p, affordances, id, opts) {
934
943
  const perception = fastingCtx(p);
935
944
  if (perception)
936
945
  ctx.perception = perception;
946
+ if (opts.forecastContext === 'conditioned')
947
+ ctx.forecastContext = bodyForecastContext(p, opts);
937
948
  if (opts.suggestion)
938
949
  ctx.suggestion = opts.suggestion;
939
950
  // Only set when voices were given this way, so single-voice records keep their shape.
@@ -966,6 +977,8 @@ function commandOf(c) {
966
977
  export function predict(p, affordances, suggestion, opts = {}) {
967
978
  const others = (opts.others ?? []).filter((s) => s.voiceId !== suggestion.voiceId);
968
979
  const decideOpts = { suggestion };
980
+ if (opts.forecastContext !== undefined)
981
+ decideOpts.forecastContext = opts.forecastContext;
969
982
  if (others.length > 0)
970
983
  decideOpts.suggestions = others;
971
984
  if (opts.necessity !== undefined)
@@ -990,6 +1003,8 @@ export function predict(p, affordances, suggestion, opts = {}) {
990
1003
  export function answerNow(p, affordances, suggestion, opts = {}) {
991
1004
  const others = (opts.others ?? []).filter((s) => s.voiceId !== suggestion.voiceId);
992
1005
  const decideOpts = { suggestion };
1006
+ if (opts.forecastContext !== undefined)
1007
+ decideOpts.forecastContext = opts.forecastContext;
993
1008
  if (others.length > 0)
994
1009
  decideOpts.suggestions = others;
995
1010
  if (opts.necessity !== undefined)
@@ -1162,8 +1177,8 @@ export function releaseCommand(p, reason = 'released') {
1162
1177
  * (a UI showing "they will do it, but would rather eat" before the host takes control). `margin` is the gap
1163
1178
  * the hourly autonomy and trust costs scale with.
1164
1179
  */
1165
- export function previewCommand(p, affordances, cmd) {
1166
- 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 });
1167
1182
  const { considered, willCtx } = scoreAll(p, list, ctx);
1168
1183
  return resolveCommand(p, considered, willCtx);
1169
1184
  }
@@ -1695,8 +1710,9 @@ export function nafsReading(p) {
1695
1710
  * The person's own forecast of their body through an offer and its `followedBy` spans (2.5.0, read-only), with a
1696
1711
  * first-person line ("I can do it, but I'll be spent for the descent."). Empty spans without `followedBy`.
1697
1712
  */
1698
- export function bodyForecast(p, aff) {
1699
- 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);
1700
1716
  return {
1701
1717
  spans,
1702
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';
package/dist/sim/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  export * from './longrun.js';
2
- export * from './sim.js';
2
+ export { acceptJoint, addPerson, birth, communityState, createCommunity, declineJoint, interruptPerson, interrupts, jointSuccessChance, mirrorAffordance, nextEventAt, preview, proposeJoint, runSilent, SIM_DEFAULTS, sharesPlace, standingHeard, stepCommunity, tellDeath, } from './sim.js';
@@ -10,15 +10,28 @@
10
10
  * back to `stepCommunity` at the stretch's end: a common pattern is routine for the years nobody watches, lived
11
11
  * minutes for the days someone is watching. Switching fidelity is the host's decision and happens on a day boundary.
12
12
  *
13
+ * With `deathHandling: 'community'`, entry and dawn catch-up deaths are discovered at the sync minute and
14
+ * skip routine work. Routine deaths retain their whole-day batch ordering; attended news and proposal cleanup
15
+ * run only after every person's routine. Suppressing news does not suppress cleanup. This adds no mortality,
16
+ * funeral or religious model. The host pins the option after restore; omission retains the old path.
17
+ *
13
18
  * It does not run decisions, conversation, joint activities, contagion or the world's percepts and affordances: those
14
19
  * belong to lived time. Companions are listed by the host on both sides (each person's routine names the other),
15
20
  * since a routine day writes only its own person. Births and arrivals are the host's (`birth`, `addPerson` in
16
21
  * `onDay`); a person added during the stretch starts on the next day.
22
+ *
23
+ * Opt-in `upbringing.model: 'warmth-through-25'` selects living parents or guardians from the young person's
24
+ * ties, counting each different person once, through young adulthood (under 25). Guardianship does not supply
25
+ * warmth: `raise` reads caregiver-to-child affection. Its continuous age taper and warmth-scaled rates are
26
+ * engineering assumptions; no abuse, schooling or adult culture diffusion. Hosts pin `upbringing` in saved-run
27
+ * configuration and supply it after restore. Marriage, parenthood and co-residence do not gate this model;
28
+ * a living carer in the community suffices. Omitted, the under-18, parents-only path stays exact.
17
29
  */
30
+ import { type RaiseOptions } from '../family/index.ts';
18
31
  import { type RoutineDay, type RoutineSource } from '../longrun.ts';
19
32
  import type { SkillTransfer } from '../skills/index.ts';
20
33
  import type { Minute, PersonId } from '../types.ts';
21
- import { type Community, type LifecourseOptions, type SimEvent } from './sim.ts';
34
+ import { type Community, type LifecourseOptions, type SimEvent, type StepOptions } from './sim.ts';
22
35
  export declare const LIVE_COMMUNITY_DEFAULTS: {
23
36
  /** Upbringing time a day with the parents (engineering assumption: a few waking hours together). */
24
37
  upbringingMinutes: number;
@@ -30,11 +43,16 @@ export interface LiveCommunityOptions {
30
43
  onDay?(c: Community, day: number, at: Minute): void;
31
44
  /** Tell each death to everyone with a tie (default true; see `tellDeath`). */
32
45
  tellDeaths?: boolean;
46
+ /** Opt into attended death receipts and scheduler cleanup; pin on every resumed call. */
47
+ deathHandling?: StepOptions['deathHandling'];
33
48
  /**
34
- * Minutes of upbringing a day for each living minor with a living parent in the community (`family.raise`; default
35
- * `LIVE_COMMUNITY_DEFAULTS.upbringingMinutes`, 0 turns it off).
49
+ * Minutes of upbringing a day for eligible young people with a living carer in the community (`family.raise`).
50
+ * Default eligibility is under 18 with a parent; warmth-through-25 includes guardians and ages under 25.
51
+ * Defaults to `LIVE_COMMUNITY_DEFAULTS.upbringingMinutes`; 0 turns it off.
36
52
  */
37
53
  upbringingMinutes?: number;
54
+ /** Opt-in household model and voice selection, passed to `raise`. Pin in host saved-run configuration. */
55
+ upbringing?: RaiseOptions;
38
56
  lifecourse?: LifecourseOptions;
39
57
  transfer?: SkillTransfer;
40
58
  }
@@ -10,19 +10,30 @@
10
10
  * back to `stepCommunity` at the stretch's end: a common pattern is routine for the years nobody watches, lived
11
11
  * minutes for the days someone is watching. Switching fidelity is the host's decision and happens on a day boundary.
12
12
  *
13
+ * With `deathHandling: 'community'`, entry and dawn catch-up deaths are discovered at the sync minute and
14
+ * skip routine work. Routine deaths retain their whole-day batch ordering; attended news and proposal cleanup
15
+ * run only after every person's routine. Suppressing news does not suppress cleanup. This adds no mortality,
16
+ * funeral or religious model. The host pins the option after restore; omission retains the old path.
17
+ *
13
18
  * It does not run decisions, conversation, joint activities, contagion or the world's percepts and affordances: those
14
19
  * belong to lived time. Companions are listed by the host on both sides (each person's routine names the other),
15
20
  * since a routine day writes only its own person. Births and arrivals are the host's (`birth`, `addPerson` in
16
21
  * `onDay`); a person added during the stretch starts on the next day.
22
+ *
23
+ * Opt-in `upbringing.model: 'warmth-through-25'` selects living parents or guardians from the young person's
24
+ * ties, counting each different person once, through young adulthood (under 25). Guardianship does not supply
25
+ * warmth: `raise` reads caregiver-to-child affection. Its continuous age taper and warmth-scaled rates are
26
+ * engineering assumptions; no abuse, schooling or adult culture diffusion. Hosts pin `upbringing` in saved-run
27
+ * configuration and supply it after restore. Marriage, parenthood and co-residence do not gate this model;
28
+ * a living carer in the community suffices. Omitted, the under-18, parents-only path stays exact.
17
29
  */
18
30
  import { endDay } from '../chronicle/index.js';
19
31
  import { dayOf } from '../core/index.js';
20
32
  import { raise } from '../family/index.js';
21
33
  import { ageYears } from '../lifecourse/index.js';
22
34
  import { routineDay } from '../longrun.js';
23
- import { tick } from '../person.js';
24
35
  import { MINUTES_PER_DAY } from '../types.js';
25
- import { tellDeath } from './sim.js';
36
+ import { advanceCommunityPerson, cleanupCommunityDeath, tellDeath, } from './sim.js';
26
37
  export const LIVE_COMMUNITY_DEFAULTS = {
27
38
  /** Upbringing time a day with the parents (engineering assumption: a few waking hours together). */
28
39
  upbringingMinutes: 240,
@@ -36,6 +47,15 @@ const UPBRINGING_UNTIL = 18;
36
47
  export function liveCommunity(c, until, opts) {
37
48
  const events = [];
38
49
  const days = {};
50
+ const log = (e) => {
51
+ const p = c.people.find((q) => q.id === e.personId);
52
+ events.push(p?.lexicon?.plainEnglish && e.kind === 'died' ? { ...e, detail: `${p.name} died.` } : e);
53
+ };
54
+ if (opts.deathHandling === 'community') {
55
+ // Older raw community JSON lacks these maps. Only the opt-in needs them here.
56
+ c.interrupts ??= {};
57
+ c.joint ??= { nextId: 0, proposals: [], excluded: {} };
58
+ }
39
59
  // Start at the first midnight at or after the community's clock: whoever is behind lives up to it minute by
40
60
  // minute, and open chronicle days are closed so none is carried through the stretch.
41
61
  let now = 0;
@@ -47,7 +67,7 @@ export function liveCommunity(c, until, opts) {
47
67
  return { events, days };
48
68
  for (const p of c.people) {
49
69
  if (p.body.alive && p.now < now)
50
- tick(p, now);
70
+ advanceCommunityPerson(c, p, now, opts, log);
51
71
  endDay(p);
52
72
  if (!p.body.alive && p.now < now)
53
73
  p.now = now;
@@ -67,7 +87,9 @@ export function liveCommunity(c, until, opts) {
67
87
  if (!p.body.alive)
68
88
  continue;
69
89
  if (p.now < now)
70
- tick(p, now);
90
+ advanceCommunityPerson(c, p, now, opts, log);
91
+ if (opts.deathHandling === 'community' && !p.body.alive)
92
+ continue;
71
93
  if (p.now !== now)
72
94
  continue; // added mid-stretch with a later clock: joins when the day catches up
73
95
  const r = routineDay(p, opts.routineFor(p, day), routineOpts);
@@ -106,23 +128,31 @@ export function liveCommunity(c, until, opts) {
106
128
  for (const p of c.people)
107
129
  if (!p.body.alive && p.now < now)
108
130
  p.now = now;
109
- if (tell)
110
- for (const d of died)
111
- tellDeath(c, d, now);
112
- // Upbringing (1.8.0): each living minor spends the day's upbringing time with their living parents.
131
+ // Keep routine deaths batched: recipients must run their own routine before end-of-day news/cleanup.
132
+ for (const d of died) {
133
+ if (tell)
134
+ tellDeath(c, d, now, opts.deathHandling === 'community' ? { receipt: 'attended' } : {});
135
+ if (opts.deathHandling === 'community')
136
+ cleanupCommunityDeath(c, d, now, opts, log);
137
+ }
138
+ // Legacy: minors with parents. Opt-in: household influence through young adulthood with parents/guardians.
113
139
  if (upbringing > 0) {
114
140
  const byId = new Map(c.people.map((q) => [q.id, q]));
115
141
  for (const child of c.people) {
116
- if (!child.body.alive || ageYears(child) >= UPBRINGING_UNTIL)
142
+ const continuous = opts.upbringing?.model === 'warmth-through-25';
143
+ if (!child.body.alive || ageYears(child) >= (continuous ? 25 : UPBRINGING_UNTIL))
117
144
  continue;
118
145
  const caregivers = child.social.relationships
119
- .filter((r) => r.roles.includes('parent'))
146
+ .filter((r) => r.roles.includes('parent') || (continuous && r.roles.includes('guardian')))
120
147
  .flatMap((r) => {
121
148
  const q = byId.get(r.otherId);
122
149
  return q?.body.alive ? [q] : [];
123
150
  });
124
- if (caregivers.length > 0)
125
- raise(child, { caregivers }, upbringing);
151
+ const selected = continuous
152
+ ? [...new Map(caregivers.filter((q) => q.id !== child.id).map((q) => [q.id, q])).values()]
153
+ : caregivers;
154
+ if (selected.length > 0)
155
+ raise(child, { caregivers: selected }, upbringing, opts.upbringing);
126
156
  }
127
157
  }
128
158
  }
package/dist/sim/sim.d.ts CHANGED
@@ -183,6 +183,8 @@ export interface LifecourseOptions {
183
183
  exposures?(p: Person): HealthExposures | undefined;
184
184
  }
185
185
  export interface StepOptions {
186
+ /** Hold current pregnancy and ambient conditions through body forecasts; pin in host run config. */
187
+ forecastContext?: 'conditioned';
186
188
  /** Standing suggestions, applied at every decision of that person (resolved quietly at reviews); one or several voices. */
187
189
  suggestions?: Record<PersonId, Suggestion | readonly Suggestion[]>;
188
190
  /** Contagion rolls between people who just shared an activity (default true). */
@@ -194,6 +196,15 @@ export interface StepOptions {
194
196
  * noticed (1.8.0, default true): they grieve, keep the tie as a memory, and a spouse is widowed (`partnering/`).
195
197
  */
196
198
  tellDeaths?: boolean;
199
+ /**
200
+ * Opt in to transition-based dispatch around driver-owned tick and finish advancement. Discovery is the advanced clock
201
+ * minute, not an inferred biological death time. Notices use attended receipts. Pin this option in
202
+ * the host run config on every call and restore; no community/save mark enables it implicitly.
203
+ * A future-dated host outcome advances discovery and tied recipients to that future clock, even past
204
+ * the step bound and their intervening events. Hosts needing bounded event order must resolve at
205
+ * the current event minute. Returned finish/death entries record scheduling and discovery clocks.
206
+ */
207
+ deathHandling?: 'community';
197
208
  /** Hosts may disable the necessity exception. */
198
209
  necessity?: boolean;
199
210
  /** Minutes an idle person waits when no option is chosen. */
@@ -240,11 +251,25 @@ export declare function birth(c: Community, a: Person, b: Person, spec: ChildSpe
240
251
  * percept through `perceive`, so they grieve, the tie is kept and marked, and a spouse is widowed. Each listener's
241
252
  * receipt time is at least their own `now`. `late: 'now'` explicitly permits listeners past `at`, as does the
242
253
  * existing default. `late: 'throw'` requires all eligible listeners to be at or before `at` and throws before
243
- * delivering any news if one is ahead. This changes no listener's clock.
254
+ * delivering any news if one is ahead. This changes no listener's clock. `receipt: 'attended'`
255
+ * opts into knowledge-consistent bereavement: only an attended percept marks the tie and widows;
256
+ * dropped news changes neither. Omitted receipt keeps the original unconditional widow call.
244
257
  */
245
258
  export declare function tellDeath(c: Community, dead: Person, at: Minute, opts?: {
246
259
  late?: 'throw' | 'now';
260
+ receipt?: 'attended';
247
261
  }): void;
262
+ /**
263
+ * SCOPE: opt-in community death discovery and scheduler cleanup, not a mortality model. Wrap one
264
+ * driver-owned advancement (tick by default); dispatch its alive-to-dead transition at the resulting clock.
265
+ * The optional advancement callback supports life-course rolls without another tick. It must be
266
+ * synchronous and advance only `p`; hosts own discovery of external deaths. No RNG or state mark
267
+ * is added. Pin `deathHandling` in the host run config, including after JSON restore. Event caps
268
+ * may omit returned log entries but never suppress cleanup or notices. No funeral is modeled.
269
+ */
270
+ export declare function advanceCommunityPerson(c: Community, p: Person, at: Minute, opts?: Pick<StepOptions, 'deathHandling' | 'tellDeaths'>, log?: (e: SimEvent) => void, advance?: (p: Person, at: Minute) => void): void;
271
+ /** Internal scheduler settlement. Routine callers wait until every person's day has run. */
272
+ export declare function cleanupCommunityDeath(c: Community, p: Person, at: Minute, opts: Pick<StepOptions, 'deathHandling' | 'tellDeaths'>, log: (e: SimEvent) => void): void;
248
273
  /** Pure scheduling query, including passive bodily checkpoints during joint waiting. */
249
274
  export declare function nextEventAt(c: Community, p: Person): Minute;
250
275
  /**
@@ -255,6 +280,7 @@ export declare function preview(p: Person, affordances: readonly Affordance[], s
255
280
  necessity?: boolean;
256
281
  others?: readonly Suggestion[];
257
282
  scarcity?: number;
283
+ forecastContext?: 'conditioned';
258
284
  }): SuggestionResolution;
259
285
  /**
260
286
  * Force `p` to re-decide at `at` (not earlier than their own clock): a running activity's review is brought
@@ -280,7 +306,7 @@ export declare function declineJoint(c: Community, proposal: JointProposal, prop
280
306
  * activities are resolved as interrupted first, then the proposer begins the offer and each partner their
281
307
  * mirror. Returns the activities begun (empty while other partners have yet to answer).
282
308
  */
283
- export declare function acceptJoint(c: Community, world: World, proposal: JointProposal, partner: Person, partnerRecord: DecisionRecord, writeLog?: (e: SimEvent) => void): Activity[];
309
+ export declare function acceptJoint(c: Community, world: World, proposal: JointProposal, partner: Person, partnerRecord: DecisionRecord, writeLog?: (e: SimEvent) => void, opts?: Pick<StepOptions, 'deathHandling' | 'tellDeaths'>): Activity[];
284
310
  /**
285
311
  * Chance that `lead` succeeds at a skilled task with `partners` helping: `successChance` with
286
312
  * support = Σ partner skill × partner capacity / max(difficulty, 0.1), clamped to 0..1.
package/dist/sim/sim.js CHANGED
@@ -232,7 +232,9 @@ export function birth(c, a, b, spec) {
232
232
  * percept through `perceive`, so they grieve, the tie is kept and marked, and a spouse is widowed. Each listener's
233
233
  * receipt time is at least their own `now`. `late: 'now'` explicitly permits listeners past `at`, as does the
234
234
  * existing default. `late: 'throw'` requires all eligible listeners to be at or before `at` and throws before
235
- * delivering any news if one is ahead. This changes no listener's clock.
235
+ * delivering any news if one is ahead. This changes no listener's clock. `receipt: 'attended'`
236
+ * opts into knowledge-consistent bereavement: only an attended percept marks the tie and widows;
237
+ * dropped news changes neither. Omitted receipt keeps the original unconditional widow call.
236
238
  */
237
239
  export function tellDeath(c, dead, at, opts = {}) {
238
240
  if (opts.late === 'throw') {
@@ -259,11 +261,74 @@ export function tellDeath(c, dead, at, opts = {}) {
259
261
  summary: q.lexicon?.plainEnglish ? `${dead.name} died.` : `${dead.name} died`,
260
262
  },
261
263
  ]);
262
- // A spouse is widowed even if the news went unattended.
263
- if (q.bonds)
264
+ // Legacy calls widow even when unattended; attended mode leaves all receipt effects to perceive.
265
+ if (opts.receipt !== 'attended' && q.bonds)
264
266
  widow(q, dead.id, Math.max(at, q.now));
265
267
  }
266
268
  }
269
+ /**
270
+ * SCOPE: opt-in community death discovery and scheduler cleanup, not a mortality model. Wrap one
271
+ * driver-owned advancement (tick by default); dispatch its alive-to-dead transition at the resulting clock.
272
+ * The optional advancement callback supports life-course rolls without another tick. It must be
273
+ * synchronous and advance only `p`; hosts own discovery of external deaths. No RNG or state mark
274
+ * is added. Pin `deathHandling` in the host run config, including after JSON restore. Event caps
275
+ * may omit returned log entries but never suppress cleanup or notices. No funeral is modeled.
276
+ */
277
+ export function advanceCommunityPerson(c, p, at, opts = {}, log = () => { }, advance = tick) {
278
+ const alive = p.body.alive;
279
+ advance(p, at);
280
+ // Composite finish may advance beyond the scheduled event. Discovery uses that actual clock.
281
+ at = p.now;
282
+ if (opts.deathHandling !== 'community' || !alive || p.body.alive)
283
+ return;
284
+ log({ at, personId: p.id, kind: 'died', detail: `${p.name} died` });
285
+ if (opts.tellDeaths ?? true) {
286
+ // Perception clamps faculty time to the listener's clock. Synchronize eligible recipients first,
287
+ // discovering any deaths there too, so a notice is never encoded before its discovery minute.
288
+ for (const q of c.people) {
289
+ if (q === p || !q.body.alive || q.now >= at)
290
+ continue;
291
+ if (q.social.relationships.some((r) => r.otherId === p.id))
292
+ advanceCommunityPerson(c, q, at, opts, log);
293
+ }
294
+ tellDeath(c, p, at, { receipt: 'attended' });
295
+ }
296
+ cleanupCommunityDeath(c, p, at, opts, log);
297
+ }
298
+ /** Preserve finish's host-declared timing and return value; discover any internal advancement death. */
299
+ function finishCommunityPerson(c, world, p, outcome, opts, log) {
300
+ let report = null;
301
+ advanceCommunityPerson(c, p, p.now, opts, log, () => {
302
+ report = finish(p, outcome, finishOpts(world));
303
+ });
304
+ return report;
305
+ }
306
+ /** Internal scheduler settlement. Routine callers wait until every person's day has run. */
307
+ export function cleanupCommunityDeath(c, p, at, opts, log) {
308
+ for (const proposal of pendingProposals(c)) {
309
+ if (proposal.proposerId !== p.id && !proposal.partnerIds.includes(p.id))
310
+ continue;
311
+ // Settle before synchronization: a second discovered death cannot cancel this proposal twice.
312
+ settle(c, proposal, 'declined');
313
+ const proposer = personById(c, proposal.proposerId);
314
+ if (proposer?.body.alive) {
315
+ const when = Math.max(at, proposer.now);
316
+ const ex = c.joint.excluded[proposer.id];
317
+ if (ex?.at === when)
318
+ ex.ids.push(proposal.offer.id);
319
+ else
320
+ c.joint.excluded[proposer.id] = { at: when, ids: [proposal.offer.id] };
321
+ }
322
+ for (const id of [proposal.proposerId, ...proposal.partnerIds]) {
323
+ const q = personById(c, id);
324
+ if (!q?.body.alive)
325
+ continue;
326
+ if (q.now < at)
327
+ advanceCommunityPerson(c, q, at, opts, log);
328
+ interruptPerson(c, q, Math.max(at, q.now), `declined:${proposal.id}`);
329
+ }
330
+ }
331
+ }
267
332
  /** Settled proposals kept for inspection. */
268
333
  const MAX_SETTLED_PROPOSALS = 16;
269
334
  const pendingProposals = (c) => c.joint.proposals.filter((x) => x.status === 'pending');
@@ -409,7 +474,7 @@ export function declineJoint(c, proposal, proposer) {
409
474
  * activities are resolved as interrupted first, then the proposer begins the offer and each partner their
410
475
  * mirror. Returns the activities begun (empty while other partners have yet to answer).
411
476
  */
412
- export function acceptJoint(c, world, proposal, partner, partnerRecord, writeLog = () => { }) {
477
+ export function acceptJoint(c, world, proposal, partner, partnerRecord, writeLog = () => { }, opts = {}) {
413
478
  const log = (e) => {
414
479
  const person = personById(c, e.personId);
415
480
  writeLog(person?.lexicon?.plainEnglish ? { ...e, detail: plainEventDetail(person, e) } : e);
@@ -431,12 +496,41 @@ export function acceptJoint(c, world, proposal, partner, partnerRecord, writeLog
431
496
  if (q && rec)
432
497
  participants.push({ person: q, offer: mirrorFor(world, q, proposal), record: rec });
433
498
  }
434
- const at = Math.max(...participants.map((x) => x.person.now));
499
+ let at = Math.max(...participants.map((x) => x.person.now));
500
+ const finishExisting = (person) => {
501
+ const act = person.activity;
502
+ if (act) {
503
+ const finishAt = opts.deathHandling === 'community' ? Math.max(at, person.now) : at;
504
+ const outcome = world.resolve(person, act, finishAt >= act.endsAt ? 'ended' : 'interrupted');
505
+ finishCommunityPerson(c, world, person, outcome, opts, log);
506
+ log({
507
+ at: finishAt,
508
+ personId: person.id,
509
+ kind: 'finish',
510
+ detail: person.lexicon?.plainEnglish
511
+ ? taskEndLine(person, act.affordance.label, outcome.status)
512
+ : `${act.action} ${outcome.status}`,
513
+ decisionId: act.decisionId,
514
+ affordanceId: act.affordanceId,
515
+ action: act.action,
516
+ status: outcome.status,
517
+ });
518
+ }
519
+ };
520
+ if (opts.deathHandling === 'community') {
521
+ for (const { person } of participants)
522
+ if (person.now < at)
523
+ advanceCommunityPerson(c, person, at, opts, log);
524
+ if (proposal.status !== 'pending' || participants.some(({ person }) => !person.body.alive)) {
525
+ declineJoint(c, proposal);
526
+ return [];
527
+ }
528
+ }
435
529
  // Opted-in catch-up can physically stop a participant. Validate everyone before any begin.
436
530
  if (participants.some(({ person }) => person.body.doze !== undefined)) {
437
531
  for (const { person } of participants)
438
532
  if (person.now < at)
439
- tick(person, at);
533
+ advanceCommunityPerson(c, person, at, opts, log);
440
534
  if (participants.some(({ person }) => person.body.doze?.since !== undefined)) {
441
535
  declineJoint(c, proposal);
442
536
  for (const { person } of participants)
@@ -444,28 +538,30 @@ export function acceptJoint(c, world, proposal, partner, partnerRecord, writeLog
444
538
  return [];
445
539
  }
446
540
  }
541
+ if (opts.deathHandling === 'community') {
542
+ // Host outcomes may advance beyond `at`. Resolve all old work before anyone starts shared work.
543
+ for (const { person } of participants)
544
+ finishExisting(person);
545
+ at = Math.max(...participants.map(({ person }) => person.now));
546
+ for (const { person } of participants)
547
+ if (person.now < at)
548
+ advanceCommunityPerson(c, person, at, opts, log);
549
+ if (proposal.status !== 'pending' ||
550
+ participants.some(({ person }) => !person.body.alive || person.body.doze?.since !== undefined)) {
551
+ declineJoint(c, proposal);
552
+ for (const { person } of participants)
553
+ if (person.body.alive)
554
+ interruptPerson(c, person, at, `declined:${proposal.id}`);
555
+ return [];
556
+ }
557
+ }
447
558
  settle(c, proposal, 'accepted');
448
559
  const begun = [];
449
560
  for (const { person, offer, record } of participants) {
450
561
  if (person.now < at)
451
- tick(person, at);
452
- const act = person.activity;
453
- if (act) {
454
- const outcome = world.resolve(person, act, at >= act.endsAt ? 'ended' : 'interrupted');
455
- finish(person, outcome, finishOpts(world));
456
- log({
457
- at,
458
- personId: person.id,
459
- kind: 'finish',
460
- detail: person.lexicon?.plainEnglish
461
- ? taskEndLine(person, act.affordance.label, outcome.status)
462
- : `${act.action} ${outcome.status}`,
463
- decisionId: act.decisionId,
464
- affordanceId: act.affordanceId,
465
- action: act.action,
466
- status: outcome.status,
467
- });
468
- }
562
+ advanceCommunityPerson(c, person, at, opts, log);
563
+ if (opts.deathHandling !== 'community')
564
+ finishExisting(person);
469
565
  const started = begin(person, offer, record, {
470
566
  ...world.beginOptions?.(person, offer, record),
471
567
  jointId: proposal.id,
@@ -614,9 +710,9 @@ function converseOneWay(c, speaker, listener, ctx, log) {
614
710
  });
615
711
  }
616
712
  }
617
- function converseBoth(c, world, p, q, act, t, log) {
713
+ function converseBoth(c, world, p, q, act, t, log, opts) {
618
714
  if (q.now < t)
619
- tick(q, t);
715
+ advanceCommunityPerson(c, q, t, opts, log);
620
716
  // Non-joint conversation offers assume the partner is available; nobody talks with someone asleep.
621
717
  if (!q.body.alive || q.body.asleep)
622
718
  return;
@@ -717,6 +813,8 @@ function bookkeepDay(p, world, t, dayDone, opts) {
717
813
  /** Merge heard advice and the host's decision options in their existing precedence order. */
718
814
  function decisionOptions(c, world, p, opts, reason, affordances) {
719
815
  const o = {};
816
+ if (opts.forecastContext !== undefined)
817
+ o.forecastContext = opts.forecastContext;
720
818
  // Voices: queued conversation advice first (fresher), then the host's standing suggestions that are heard now
721
819
  // (standing-advice SCOPE); one per voice. At reviews they still weigh in, but `decide` resolves them quietly
722
820
  // (no counters, pressure or autonomy cost), so one request is not re-counted as many verdicts.
@@ -798,7 +896,9 @@ export function stepCommunity(c, world, until, opts = {}) {
798
896
  .filter((q) => q !== undefined);
799
897
  for (const q of waiters)
800
898
  if (q.now < t)
801
- tick(q, t);
899
+ advanceCommunityPerson(c, q, t, opts, log);
900
+ if (opts.deathHandling === 'community' && x.status !== 'pending')
901
+ continue;
802
902
  const reasons = new Map(waiters.map((q) => [q.id, q.activity?.interrupt?.reason ?? c.interrupts[q.id]?.reason]));
803
903
  if (x.proposerId === p.id)
804
904
  settle(c, x, 'declined');
@@ -826,7 +926,9 @@ export function stepCommunity(c, world, until, opts = {}) {
826
926
  break;
827
927
  const p = next;
828
928
  const ordinaryDue = ordinaryEventAt(c, p) <= t;
829
- tick(p, t);
929
+ advanceCommunityPerson(c, p, t, opts, log);
930
+ if (opts.deathHandling === 'community' && !p.body.alive)
931
+ continue;
830
932
  const dozed = p.activity?.dozedAt !== undefined || p.body.doze?.since !== undefined;
831
933
  if (dozed) {
832
934
  cancelDozeWaiting(p, t);
@@ -835,7 +937,7 @@ export function stepCommunity(c, world, until, opts = {}) {
835
937
  if (act.interrupt)
836
938
  c.interrupts[p.id] ??= { ...act.interrupt };
837
939
  const outcome = world.resolve(p, act, 'interrupted');
838
- finish(p, outcome, finishOpts(world));
940
+ finishCommunityPerson(c, world, p, outcome, opts, log);
839
941
  log({
840
942
  at: t,
841
943
  personId: p.id,
@@ -850,13 +952,17 @@ export function stepCommunity(c, world, until, opts = {}) {
850
952
  status: outcome.status,
851
953
  }, outcome.summary !== undefined);
852
954
  }
955
+ if (opts.deathHandling === 'community' && !p.body.alive)
956
+ continue;
853
957
  c.idleUntil[p.id] = t + quantum;
854
958
  if (!ordinaryDue)
855
959
  continue;
856
960
  }
857
961
  if (!ordinaryDue && p.body.alive)
858
962
  continue;
859
- bookkeepDay(p, world, t, c.dayDone, opts);
963
+ advanceCommunityPerson(c, p, t, opts, log, () => bookkeepDay(p, world, t, c.dayDone ?? {}, opts));
964
+ if (opts.deathHandling === 'community' && !p.body.alive)
965
+ continue;
860
966
  if (!p.body.alive) {
861
967
  log({ at: t, personId: p.id, kind: 'died', detail: `${p.name} died` });
862
968
  if (opts.tellDeaths ?? true)
@@ -899,7 +1005,7 @@ export function stepCommunity(c, world, until, opts = {}) {
899
1005
  perceptReason ??= act?.interrupt?.reason;
900
1006
  if (act && t >= act.endsAt) {
901
1007
  const outcome = world.resolve(p, act, 'ended');
902
- const report = finish(p, outcome, finishOpts(world));
1008
+ const report = finishCommunityPerson(c, world, p, outcome, opts, log);
903
1009
  const ev = {
904
1010
  at: t,
905
1011
  personId: p.id,
@@ -915,6 +1021,8 @@ export function stepCommunity(c, world, until, opts = {}) {
915
1021
  if (report)
916
1022
  ev.status = report.status;
917
1023
  log(ev, outcome.summary !== undefined);
1024
+ if (opts.deathHandling === 'community' && !p.body.alive)
1025
+ continue;
918
1026
  if (outcome.status === 'completed') {
919
1027
  noteStandingDone(c, p, act, t);
920
1028
  if (pairMinute !== t) {
@@ -927,8 +1035,8 @@ export function stepCommunity(c, world, until, opts = {}) {
927
1035
  continue;
928
1036
  pairsDone.add(key);
929
1037
  if (isConversation(act))
930
- converseBoth(c, world, p, q, act, t, log);
931
- if (contagion && sharesPlace(q, act))
1038
+ converseBoth(c, world, p, q, act, t, log, opts);
1039
+ if (contagion && (opts.deathHandling !== 'community' || q.body.alive) && sharesPlace(q, act))
932
1040
  contagionBetween(p, q, act, t, log);
933
1041
  }
934
1042
  }
@@ -972,7 +1080,7 @@ export function stepCommunity(c, world, until, opts = {}) {
972
1080
  detail: `joins ${acceptedProposal.offer.label}`,
973
1081
  jointId: acceptedProposal.id,
974
1082
  });
975
- acceptJoint(c, world, acceptedProposal, p, record, (event) => log(event, true));
1083
+ acceptJoint(c, world, acceptedProposal, p, record, (event) => log(event, true), opts);
976
1084
  continue;
977
1085
  }
978
1086
  const chosen = affordances.find((a) => a.id === record.chosenAffordanceId);
@@ -1010,7 +1118,7 @@ export function stepCommunity(c, world, until, opts = {}) {
1010
1118
  }
1011
1119
  if (act) {
1012
1120
  const outcome = world.resolve(p, act, 'interrupted');
1013
- finish(p, outcome, finishOpts(world));
1121
+ finishCommunityPerson(c, world, p, outcome, opts, log);
1014
1122
  log({
1015
1123
  at: t,
1016
1124
  personId: p.id,
@@ -1024,6 +1132,8 @@ export function stepCommunity(c, world, until, opts = {}) {
1024
1132
  status: outcome.status,
1025
1133
  });
1026
1134
  }
1135
+ if (opts.deathHandling === 'community' && !p.body.alive)
1136
+ continue;
1027
1137
  if (!chosen) {
1028
1138
  c.idleUntil[p.id] = t + quantum;
1029
1139
  log({ at: t, personId: p.id, kind: 'idle', detail: 'nothing chosen' });
@@ -1048,7 +1158,7 @@ export function stepCommunity(c, world, until, opts = {}) {
1048
1158
  }
1049
1159
  for (const p of c.people)
1050
1160
  if (p.now < until)
1051
- tick(p, until);
1161
+ advanceCommunityPerson(c, p, until, opts, log);
1052
1162
  return events;
1053
1163
  }
1054
1164
  /**
package/dist/types.d.ts CHANGED
@@ -879,7 +879,10 @@ export interface Activity {
879
879
  }
880
880
  export declare const PERSON_SCHEMA = "human/person@1";
881
881
  /**
882
- * Current: 2.3.0 (2026-10-17): optional temporal goals and received consequences.
882
+ * Engine compatibility stamp. Package versions and engine versions may differ.
883
+ * See CHANGELOG.md for current changes and migrate.ts for supported saved versions.
884
+ *
885
+ * 2.3.0 (2026-10-17): optional temporal goals and received consequences.
883
886
  * 2.2.0 (2026-10-14): method evidence grades each action independently.
884
887
  * Successful alternatives no longer count as failures of another action. Person state shape is unchanged.
885
888
  * 2.1.0 saves migrate by stamping the version.
@@ -915,7 +918,7 @@ export declare const PERSON_SCHEMA = "human/person@1";
915
918
  * and an activity that would cover a closing stretch is reviewed when the stretch begins (`isUnderWay`,
916
919
  * `dutyReviewAt`). Person shape unchanged.
917
920
  */
918
- export declare const ENGINE_VERSION = "2.23.0";
921
+ export declare const ENGINE_VERSION = "3.1.0";
919
922
  /** A pregnancy carried by this person (host-driven; see `family/`). */
920
923
  export interface Pregnancy {
921
924
  fatherId: PersonId;
package/dist/types.js CHANGED
@@ -84,7 +84,10 @@ export const EMOTIONS = [
84
84
  // ---------------------------------------------------------------------------------------------
85
85
  export const PERSON_SCHEMA = 'human/person@1';
86
86
  /**
87
- * Current: 2.3.0 (2026-10-17): optional temporal goals and received consequences.
87
+ * Engine compatibility stamp. Package versions and engine versions may differ.
88
+ * See CHANGELOG.md for current changes and migrate.ts for supported saved versions.
89
+ *
90
+ * 2.3.0 (2026-10-17): optional temporal goals and received consequences.
88
91
  * 2.2.0 (2026-10-14): method evidence grades each action independently.
89
92
  * Successful alternatives no longer count as failures of another action. Person state shape is unchanged.
90
93
  * 2.1.0 saves migrate by stamping the version.
@@ -120,4 +123,4 @@ export const PERSON_SCHEMA = 'human/person@1';
120
123
  * and an activity that would cover a closing stretch is reviewed when the stretch begins (`isUnderWay`,
121
124
  * `dutyReviewAt`). Person shape unchanged.
122
125
  */
123
- export const ENGINE_VERSION = '2.23.0';
126
+ export const ENGINE_VERSION = '3.1.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adam0white/human-framework",
3
- "version": "3.0.0",
3
+ "version": "3.1.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",