@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 +36 -5
- package/dist/cognition/cognition.d.ts +6 -1
- package/dist/cognition/cognition.js +1 -1
- package/dist/core/libm.js +20 -0
- package/dist/family/family.d.ts +5 -1
- package/dist/family/family.js +22 -6
- package/dist/habits/habits.js +5 -5
- package/dist/impression.d.ts +4 -1
- package/dist/impression.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/migrate.js +5 -0
- package/dist/person.d.ts +11 -4
- package/dist/person.js +20 -4
- package/dist/persona.js +2 -1
- package/dist/prospection/forecast.d.ts +10 -5
- package/dist/prospection/forecast.js +11 -6
- package/dist/sim/index.d.ts +1 -1
- package/dist/sim/index.js +1 -1
- package/dist/sim/longrun.d.ts +21 -3
- package/dist/sim/longrun.js +42 -12
- package/dist/sim/sim.d.ts +28 -2
- package/dist/sim/sim.js +146 -36
- package/dist/types.d.ts +5 -2
- package/dist/types.js +5 -2
- package/package.json +1 -1
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
*
|
package/dist/family/family.d.ts
CHANGED
|
@@ -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;
|
package/dist/family/family.js
CHANGED
|
@@ -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`
|
|
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
|
|
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 =
|
|
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
|
}
|
package/dist/habits/habits.js
CHANGED
|
@@ -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.
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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';
|
package/dist/impression.d.ts
CHANGED
|
@@ -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
|
};
|
package/dist/impression.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
15
|
-
*
|
|
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
|
|
15
|
-
*
|
|
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,
|
|
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;
|
package/dist/sim/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export * from './longrun.ts';
|
|
2
|
-
export
|
|
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
|
|
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';
|
package/dist/sim/longrun.d.ts
CHANGED
|
@@ -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
|
|
35
|
-
*
|
|
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
|
}
|
package/dist/sim/longrun.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
-
|
|
125
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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 = "
|
|
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
|
-
*
|
|
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 = '
|
|
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.
|
|
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",
|