@adam0white/human-framework 2.23.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +169 -55
  2. package/dist/agenda/agenda.js +1 -1
  3. package/dist/beliefs/beliefs.js +2 -2
  4. package/dist/cognition/cognition.d.ts +6 -1
  5. package/dist/cognition/cognition.js +1 -1
  6. package/dist/conscience/conscience.d.ts +2 -4
  7. package/dist/conscience/conscience.js +3 -5
  8. package/dist/conversation/conversation.d.ts +0 -41
  9. package/dist/conversation/conversation.js +5 -5
  10. package/dist/core/index.d.ts +0 -1
  11. package/dist/core/index.js +0 -1
  12. package/dist/core/libm.js +20 -0
  13. package/dist/core/math.d.ts +0 -2
  14. package/dist/core/math.js +3 -3
  15. package/dist/family/family.d.ts +5 -6
  16. package/dist/family/family.js +34 -12
  17. package/dist/habits/habits.js +5 -5
  18. package/dist/impression.d.ts +6 -8
  19. package/dist/impression.js +7 -9
  20. package/dist/index.d.ts +2 -1
  21. package/dist/index.js +2 -1
  22. package/dist/longrun.d.ts +2 -2
  23. package/dist/longrun.js +1 -2
  24. package/dist/memory/memory.d.ts +0 -4
  25. package/dist/memory/memory.js +20 -6
  26. package/dist/migrate.js +5 -0
  27. package/dist/person.d.ts +11 -4
  28. package/dist/person.js +24 -7
  29. package/dist/persona.js +2 -1
  30. package/dist/prospection/forecast.d.ts +10 -5
  31. package/dist/prospection/forecast.js +11 -6
  32. package/dist/sim/index.d.ts +1 -1
  33. package/dist/sim/index.js +1 -1
  34. package/dist/sim/longrun.d.ts +24 -6
  35. package/dist/sim/longrun.js +43 -13
  36. package/dist/sim/sim.d.ts +28 -2
  37. package/dist/sim/sim.js +146 -36
  38. package/dist/social/impressions.d.ts +0 -12
  39. package/dist/social/impressions.js +13 -8
  40. package/dist/social/social.js +2 -2
  41. package/dist/types.d.ts +5 -2
  42. package/dist/types.js +5 -2
  43. package/package.json +1 -1
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,31 +10,49 @@
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
- import { type Routine, type RoutineDay } from '../longrun.ts';
30
+ import { type RaiseOptions } from '../family/index.ts';
31
+ import { type RoutineDay, type RoutineSource } from '../longrun.ts';
19
32
  import type { SkillTransfer } from '../skills/index.ts';
20
- import type { Minute, Person, PersonId } from '../types.ts';
21
- import { type Community, type LifecourseOptions, type SimEvent } from './sim.ts';
33
+ import type { Minute, PersonId } from '../types.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;
25
38
  };
26
39
  export interface LiveCommunityOptions {
27
40
  /** Each living person's routine for a day (`day` is the day number being lived). */
28
- routineFor(p: Person, day: number, c: Community): Routine;
41
+ routineFor: RoutineSource;
29
42
  /** At each dawn, before anyone's day: births, arrivals, routine changes. */
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,10 +87,12 @@ 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
- const r = routineDay(p, opts.routineFor(p, day, c), routineOpts);
95
+ const r = routineDay(p, opts.routineFor(p, day), routineOpts);
74
96
  if (r.episodes.length > 0 || r.onsets.length > 0 || r.stage || r.died) {
75
97
  const list = days[p.id] ?? [];
76
98
  list.push(r);
@@ -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
  /**
@@ -1,8 +1,6 @@
1
1
  import type { EntityId, Minute, NormTag, Person, PersonId, Signed, StateCue, Traits, Unit } from '../types.ts';
2
2
  export type CueKind = 'state' | 'place' | 'trait' | 'tie' | 'trust' | 'skill' | 'regard';
3
3
  export declare const STATE_CUES: readonly StateCue[];
4
- /** @deprecated since 2.1: use `TRAIT_KEYS` (types.ts), the same list. Removed in 3.0. */
5
- export declare const TRAIT_NAMES: readonly (keyof Traits)[];
6
4
  export declare const IMPRESSION_DEFAULTS: {
7
5
  /** Weight half-life by kind, minutes (0 = no decay). */
8
6
  halfLife: Record<CueKind, number>;
@@ -88,20 +86,10 @@ export declare function setReserve(p: Person, reserve: Partial<Record<StateCue,
88
86
  * the evidence and how true it is (a figure in the dark tells little and tells it wrong).
89
87
  */
90
88
  export declare function glimpse(observer: Person, targetId: PersonId, signs: OutwardSigns, at: Minute, clarity?: Unit): void;
91
- /** @deprecated since 2.1: pass `at` (and the optional knobs) positionally. Removed in 3.0. */
92
- export declare function glimpse(observer: Person, targetId: PersonId, signs: OutwardSigns, opts: {
93
- at: Minute;
94
- clarity?: Unit;
95
- }): void;
96
89
  /** What someone was seen to do: place fear, ties, trust in a voice and trait evidence (see `ObservedAct`). */
97
90
  export declare function observeAct(observer: Person, targetId: PersonId, act: ObservedAct, table?: Record<string, Partial<Record<keyof Traits, Unit>>>): void;
98
91
  /** Testimony about someone (their own words, or another's): `value` on `key`, weighted (e.g. by trust in the teller). */
99
92
  export declare function hear(observer: Person, targetId: PersonId, key: string, value: number, at: Minute, weight?: Unit): void;
100
- /** @deprecated since 2.1: pass `at` (and the optional knobs) positionally. Removed in 3.0. */
101
- export declare function hear(observer: Person, targetId: PersonId, key: string, value: number, opts: {
102
- at: Minute;
103
- weight?: Unit;
104
- }): void;
105
93
  /**
106
94
  * Years of history condensed (host call, e.g. at the start of a run for people who already know each other): one
107
95
  * sample per trait from `traits` with an error that shrinks with familiarity, at weight
@@ -30,8 +30,6 @@ import { clamp, clamp01, cmpStr, decay, latestPerId, newestN, round } from '../c
30
30
  import { MINUTES_PER_DAY, MINUTES_PER_YEAR, TRAIT_KEYS } from '../types.js';
31
31
  import { judge, SOCIAL_DEFAULTS } from './social.js';
32
32
  export const STATE_CUES = ['fatigue', 'pain', 'fear', 'mood'];
33
- /** @deprecated since 2.1: use `TRAIT_KEYS` (types.ts), the same list. Removed in 3.0. */
34
- export const TRAIT_NAMES = TRAIT_KEYS;
35
33
  export const IMPRESSION_DEFAULTS = {
36
34
  /** Weight half-life by kind, minutes (0 = no decay). */
37
35
  halfLife: {
@@ -208,11 +206,16 @@ function erred(observer, targetId, key, at, value, clarity) {
208
206
  const e = (hash01(`${observer.id}|${targetId}|${key}|${at}`) - 0.5) * IMPRESSION_DEFAULTS.noise;
209
207
  return value + e * (1 - clamp01(clarity));
210
208
  }
211
- export function glimpse(observer, targetId, signs, when, clarity) {
209
+ /**
210
+ * A look at someone's outward signs (from the composite's `outwardSigns`). `clarity` 0..1 scales both the weight of
211
+ * the evidence and how true it is (a figure in the dark tells little and tells it wrong).
212
+ */
213
+ export function glimpse(observer, targetId, signs, at, clarity) {
214
+ if (!Number.isFinite(at))
215
+ throw new TypeError('glimpse requires a finite positional time');
212
216
  if (targetId === observer.id)
213
217
  return;
214
- const at = typeof when === 'number' ? when : when.at;
215
- const clear = clamp01((typeof when === 'number' ? clarity : when.clarity) ?? 1);
218
+ const clear = clamp01(clarity ?? 1);
216
219
  if (clear <= 0)
217
220
  return;
218
221
  const imp = impressionFor(observer, targetId, true, at);
@@ -252,11 +255,13 @@ export function observeAct(observer, targetId, act, table = IMPRESSION_DEFAULTS.
252
255
  }
253
256
  }
254
257
  }
255
- export function hear(observer, targetId, key, value, when, weight) {
258
+ /** Testimony about someone (their own words, or another's): `value` on `key`, weighted (e.g. by trust in the teller). */
259
+ export function hear(observer, targetId, key, value, at, weight) {
260
+ if (!Number.isFinite(at))
261
+ throw new TypeError('hear requires a finite positional time');
256
262
  if (targetId === observer.id)
257
263
  return;
258
- const at = typeof when === 'number' ? when : when.at;
259
- const w = clamp01((typeof when === 'number' ? weight : when.weight) ?? 0.5);
264
+ const w = clamp01(weight ?? 0.5);
260
265
  if (w <= 0)
261
266
  return;
262
267
  const imp = impressionFor(observer, targetId, true, at);