@adam0white/human-framework 2.18.0 → 2.20.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 (49) hide show
  1. package/README.md +51 -0
  2. package/dist/agenda/agenda.d.ts +47 -0
  3. package/dist/agenda/agenda.js +4 -1
  4. package/dist/body/body.d.ts +10 -0
  5. package/dist/body/body.js +15 -0
  6. package/dist/chronicle/story.js +3 -2
  7. package/dist/cognition/cognition.d.ts +37 -0
  8. package/dist/cognition/cognition.js +3 -2
  9. package/dist/conversation/conversation.js +10 -4
  10. package/dist/core/index.d.ts +1 -0
  11. package/dist/core/index.js +1 -0
  12. package/dist/core/keys.d.ts +6 -0
  13. package/dist/core/keys.js +8 -0
  14. package/dist/family/family.js +5 -5
  15. package/dist/index.d.ts +4 -1
  16. package/dist/index.js +4 -1
  17. package/dist/longrun.js +1 -1
  18. package/dist/memory/memory.d.ts +26 -0
  19. package/dist/memory/memory.js +3 -9
  20. package/dist/narrate/lexicon.d.ts +2 -1
  21. package/dist/narrate/lexicon.js +2 -1
  22. package/dist/narrate/narrate.d.ts +14 -1
  23. package/dist/narrate/narrate.js +81 -39
  24. package/dist/narrate/plain.d.ts +7 -0
  25. package/dist/narrate/plain.js +293 -0
  26. package/dist/observed-reputation.d.ts +4 -0
  27. package/dist/observed-reputation.js +16 -0
  28. package/dist/partnering/partnering.js +1 -1
  29. package/dist/person.d.ts +5 -0
  30. package/dist/person.js +119 -11
  31. package/dist/presentation.d.ts +10 -0
  32. package/dist/presentation.js +88 -0
  33. package/dist/promptings/promptings.d.ts +1 -0
  34. package/dist/promptings/promptings.js +40 -4
  35. package/dist/prospection/forecast.d.ts +2 -0
  36. package/dist/prospection/forecast.js +15 -1
  37. package/dist/qalb.d.ts +28 -0
  38. package/dist/qalb.js +67 -0
  39. package/dist/review/review.d.ts +32 -0
  40. package/dist/review/review.js +6 -2
  41. package/dist/scenarios/village.js +63 -26
  42. package/dist/sim/longrun.js +20 -3
  43. package/dist/sim/sim.d.ts +1 -1
  44. package/dist/sim/sim.js +93 -15
  45. package/dist/social/social.js +12 -2
  46. package/dist/types.d.ts +6 -0
  47. package/dist/will/will.d.ts +12 -0
  48. package/dist/will/will.js +43 -4
  49. package/package.json +1 -1
@@ -257,6 +257,7 @@ export function createVillage(people, opts) {
257
257
  return out;
258
258
  };
259
259
  const resolve = (p, act, reason) => {
260
+ const say = (legacy, plain) => (p.lexicon?.plainEnglish ? plain : legacy);
260
261
  const now = p.now;
261
262
  state.now = Math.max(state.now, now);
262
263
  const aff = act.affordance;
@@ -272,12 +273,16 @@ export function createVillage(people, opts) {
272
273
  // Partial work counts: a field shift left early yields and pays pro rata for the time put in.
273
274
  const span = Math.max(1, act.endsAt - act.startedAt);
274
275
  const progress = clamp01((now - act.startedAt) / span);
275
- const out = { ...base, status: 'interrupted', summary: `${aff.label}: interrupted` };
276
+ const out = {
277
+ ...base,
278
+ status: 'interrupted',
279
+ summary: say(`${aff.label}: interrupted`, `I was interrupted during the task “${aff.label}”.`),
280
+ };
276
281
  if (act.action === 'work-field' && progress > 0) {
277
282
  state.food += V.fieldYield * progress;
278
283
  out.material = V.wage * progress;
279
284
  out.needs = { competence: 0.1 * progress };
280
- out.summary = `${aff.label}: stopped ${Math.round(progress * 100)}% of the way through`;
285
+ out.summary = say(`${aff.label}: stopped ${Math.round(progress * 100)}% of the way through`, `I stopped the task “${aff.label}” after completing ${Math.round(progress * 100)}% of it.`);
281
286
  }
282
287
  return out;
283
288
  }
@@ -291,17 +296,29 @@ export function createVillage(people, opts) {
291
296
  case 'eat':
292
297
  if (state.food >= 1) {
293
298
  state.food -= 1;
294
- return done({ ...base, needs: { food: 0.6 }, summary: 'ate at the kitchen' });
299
+ return done({
300
+ ...base,
301
+ needs: { food: 0.6 },
302
+ summary: say('ate at the kitchen', 'I ate at the kitchen.'),
303
+ });
295
304
  }
296
- return { ...base, status: 'failed', summary: 'found the kitchen empty' };
305
+ return {
306
+ ...base,
307
+ status: 'failed',
308
+ summary: say('found the kitchen empty', 'I found the kitchen empty.'),
309
+ };
297
310
  case 'drink':
298
- return done({ ...base, needs: { water: 0.6 }, summary: 'drank at the well' });
311
+ return done({
312
+ ...base,
313
+ needs: { water: 0.6 },
314
+ summary: say('drank at the well', 'I drank at the well.'),
315
+ });
299
316
  case 'sleep':
300
- return done({ ...base, summary: 'slept' });
317
+ return done({ ...base, summary: say('slept', 'I slept.') });
301
318
  case 'rest':
302
- return done({ ...base, needs: { leisure: 0.1 }, summary: 'rested' });
319
+ return done({ ...base, needs: { leisure: 0.1 }, summary: say('rested', 'I rested.') });
303
320
  case 'wait':
304
- return done({ ...base, summary: 'waited' });
321
+ return done({ ...base, summary: say('waited', 'I waited.') });
305
322
  case 'work-field': {
306
323
  const ok = chance(state.rng, successChance(p, 'farming', 0.3, capacity));
307
324
  if (ok) {
@@ -310,7 +327,7 @@ export function createVillage(people, opts) {
310
327
  ...base,
311
328
  needs: { competence: 0.1 },
312
329
  material: V.wage,
313
- summary: 'worked the field; a good shift',
330
+ summary: say('worked the field; a good shift', 'I worked in the field. The shift went well.'),
314
331
  });
315
332
  }
316
333
  state.food += 1;
@@ -318,7 +335,7 @@ export function createVillage(people, opts) {
318
335
  ...base,
319
336
  status: 'failed',
320
337
  needs: { competence: 0.02 },
321
- summary: 'worked the field; a poor shift',
338
+ summary: say('worked the field; a poor shift', 'I worked in the field. The shift went poorly.'),
322
339
  };
323
340
  }
324
341
  case 'forage': {
@@ -329,23 +346,35 @@ export function createVillage(people, opts) {
329
346
  status: 'failed',
330
347
  injury: { part: 'leg', severity: risk.severity, healRatePerDay: 0.1 },
331
348
  summary: isNightAt(act.startedAt)
332
- ? 'went to the forest at night and got hurt by an animal'
333
- : 'got hurt by an animal in the forest',
349
+ ? say('went to the forest at night and got hurt by an animal', 'I went to the forest at night. An animal hurt me.')
350
+ : say('got hurt by an animal in the forest', 'An animal hurt me in the forest.'),
334
351
  };
335
352
  }
336
353
  state.food += V.forageYield;
337
- return done({ ...base, needs: { food: 0.3 }, summary: 'foraged in the forest' });
354
+ return done({
355
+ ...base,
356
+ needs: { food: 0.3 },
357
+ summary: say('foraged in the forest', 'I gathered food in the forest.'),
358
+ });
338
359
  }
339
360
  case 'cook': {
340
361
  const ok = chance(state.rng, successChance(p, 'cooking', 0.3, capacity));
341
362
  if (ok) {
342
363
  state.food += V.cookYield;
343
- return done({ ...base, needs: { competence: 0.08 }, summary: 'cooked a good meal' });
364
+ return done({
365
+ ...base,
366
+ needs: { competence: 0.08 },
367
+ summary: say('cooked a good meal', 'I cooked a good meal.'),
368
+ });
344
369
  }
345
- return { ...base, status: 'failed', summary: 'burnt the meal' };
370
+ return { ...base, status: 'failed', summary: say('burnt the meal', 'I burnt the meal.') };
346
371
  }
347
372
  case 'pray':
348
- return done({ ...base, needs: { meaning: 0.1 }, summary: 'prayed at the mosque' });
373
+ return done({
374
+ ...base,
375
+ needs: { meaning: 0.1 },
376
+ summary: say('prayed at the mosque', 'I prayed at the mosque.'),
377
+ });
349
378
  case 'apologize': {
350
379
  const other = act.targetId;
351
380
  if (other !== undefined) {
@@ -357,10 +386,13 @@ export function createVillage(people, opts) {
357
386
  targetId: other,
358
387
  valence: 0.3,
359
388
  salience: 0.6,
360
- summary: `${p.name} apologised`,
389
+ summary: say(`${p.name} apologised`, `${p.name} apologised.`),
361
390
  });
362
391
  }
363
- return done({ ...base, summary: `apologised to ${other ?? 'a neighbour'}` });
392
+ return done({
393
+ ...base,
394
+ summary: say(`apologised to ${other ?? 'a neighbour'}`, `I apologised to ${other ?? 'a neighbour'}.`),
395
+ });
364
396
  }
365
397
  case 'chat': {
366
398
  const other = aff.with?.[0];
@@ -373,13 +405,13 @@ export function createVillage(people, opts) {
373
405
  targetId: other,
374
406
  valence: 0.3,
375
407
  salience: 0.4,
376
- summary: `${p.name} stopped to chat`,
408
+ summary: say(`${p.name} stopped to chat`, `${p.name} stopped to chat.`),
377
409
  });
378
410
  }
379
411
  return done({
380
412
  ...base,
381
413
  needs: { belonging: 0.3, leisure: 0.1 },
382
- summary: `chatted with ${other ?? 'a neighbour'}`,
414
+ summary: say(`chatted with ${other ?? 'a neighbour'}`, `I chatted with ${other ?? 'a neighbour'}.`),
383
415
  });
384
416
  }
385
417
  case 'help': {
@@ -394,13 +426,13 @@ export function createVillage(people, opts) {
394
426
  valence: 0.5,
395
427
  salience: 0.6,
396
428
  norms: [{ normId: 'help-neighbor', relation: 'fulfills' }],
397
- summary: `${p.name} helped with the work`,
429
+ summary: say(`${p.name} helped with the work`, `${p.name} helped with the work.`),
398
430
  });
399
431
  }
400
432
  return done({
401
433
  ...base,
402
434
  needs: { meaning: 0.2, esteem: 0.05 },
403
- summary: `helped ${other ?? 'a neighbour'}`,
435
+ summary: say(`helped ${other ?? 'a neighbour'}`, `I helped ${other ?? 'a neighbour'}.`),
404
436
  percepts: other
405
437
  ? [
406
438
  {
@@ -411,7 +443,7 @@ export function createVillage(people, opts) {
411
443
  targetId: p.id,
412
444
  valence: 0.3,
413
445
  salience: 0.5,
414
- summary: `${other} said thank you`,
446
+ summary: say(`${other} said thank you`, `${other} said thank you.`),
415
447
  },
416
448
  ]
417
449
  : [],
@@ -429,13 +461,18 @@ export function createVillage(people, opts) {
429
461
  valence: -0.5,
430
462
  salience: 0.8,
431
463
  norms: [{ normId: 'theft', relation: 'violates' }],
432
- summary: `${p.name} took bread from the house`,
464
+ summary: say(`${p.name} took bread from the house`, `${p.name} took bread from the house.`),
433
465
  });
434
466
  }
435
- return done({ ...base, needs: { food: 0.5 }, material: 1, summary: 'took bread that was not mine' });
467
+ return done({
468
+ ...base,
469
+ needs: { food: 0.5 },
470
+ material: 1,
471
+ summary: say('took bread that was not mine', 'I took bread that was not mine.'),
472
+ });
436
473
  }
437
474
  default:
438
- return done({ ...base, summary: `${aff.label}: done` });
475
+ return done({ ...base, summary: say(`${aff.label}: done`, `I completed the task “${aff.label}”.`) });
439
476
  }
440
477
  };
441
478
  const perceptsFor = (p, since, until) => {
@@ -77,11 +77,28 @@ export function liveCommunity(c, until, opts) {
77
77
  days[p.id] = list;
78
78
  }
79
79
  for (const kind of r.onsets)
80
- events.push({ at: r.at, personId: p.id, kind: 'onset', detail: `${p.name}: ${kind}` });
80
+ events.push({
81
+ at: r.at,
82
+ personId: p.id,
83
+ kind: 'onset',
84
+ detail: p.lexicon?.plainEnglish ? `${p.name} developed ${kind}.` : `${p.name}: ${kind}`,
85
+ });
81
86
  if (r.stage)
82
- events.push({ at: r.at, personId: p.id, kind: 'stage', detail: `${p.name} is now ${r.stage}` });
87
+ events.push({
88
+ at: r.at,
89
+ personId: p.id,
90
+ kind: 'stage',
91
+ detail: p.lexicon?.plainEnglish
92
+ ? `${p.name} entered the ${r.stage} stage of life.`
93
+ : `${p.name} is now ${r.stage}`,
94
+ });
83
95
  if (r.died) {
84
- events.push({ at: r.at, personId: p.id, kind: 'died', detail: `${p.name} died` });
96
+ events.push({
97
+ at: r.at,
98
+ personId: p.id,
99
+ kind: 'died',
100
+ detail: p.lexicon?.plainEnglish ? `${p.name} died.` : `${p.name} died`,
101
+ });
85
102
  died.push(p);
86
103
  }
87
104
  }
package/dist/sim/sim.d.ts CHANGED
@@ -280,7 +280,7 @@ export declare function declineJoint(c: Community, proposal: JointProposal, prop
280
280
  * activities are resolved as interrupted first, then the proposer begins the offer and each partner their
281
281
  * mirror. Returns the activities begun (empty while other partners have yet to answer).
282
282
  */
283
- export declare function acceptJoint(c: Community, world: World, proposal: JointProposal, partner: Person, partnerRecord: DecisionRecord, log?: (e: SimEvent) => void): Activity[];
283
+ export declare function acceptJoint(c: Community, world: World, proposal: JointProposal, partner: Person, partnerRecord: DecisionRecord, writeLog?: (e: SimEvent) => void): Activity[];
284
284
  /**
285
285
  * Chance that `lead` succeeds at a skilled task with `partners` helping: `successChance` with
286
286
  * support = Σ partner skill × partner capacity / max(difficulty, 0.1), clamped to 0..1.
package/dist/sim/sim.js CHANGED
@@ -79,6 +79,54 @@ function finishOpts(world) {
79
79
  o.transfer = world.skillTransfer;
80
80
  return o;
81
81
  }
82
+ /** Complete sentences for generated event details. Host outcome summaries bypass this reader. */
83
+ function plainEventDetail(p, event) {
84
+ const name = p.name;
85
+ const detail = event.detail;
86
+ switch (event.kind) {
87
+ case 'decide':
88
+ return detail;
89
+ case 'idle':
90
+ return `${name} did not choose a task.`;
91
+ case 'died':
92
+ return `${name} died.`;
93
+ case 'begin':
94
+ case 'continue':
95
+ case 'finish':
96
+ return detail;
97
+ case 'perceive': {
98
+ const counts = /^(\d+)\/(\d+) percepts$/.exec(detail);
99
+ return counts ? `${name} noticed ${counts[1]} of ${counts[2]} events.` : `${name} noticed “${detail}”.`;
100
+ }
101
+ case 'interrupt':
102
+ return `${name} was interrupted by the event “${detail.replace(/^percept:/, '')}”.`;
103
+ case 'propose':
104
+ return `${name} proposed the shared task “${detail}”.`;
105
+ case 'accept':
106
+ return `${name} joined the shared task “${detail.replace(/^joins /, '')}”.`;
107
+ case 'decline':
108
+ return `${name} declined the shared task “${detail.replace(/^declined /, '')}”.`;
109
+ case 'converse':
110
+ return `${name} spoke. The exchange included: “${detail}”.`;
111
+ case 'contagion':
112
+ return `${name} ${detail}.`;
113
+ case 'command':
114
+ return `${name} received the order “${detail}”.`;
115
+ case 'release':
116
+ return `${name} was released from the order.`;
117
+ case 'onset':
118
+ return `${name} developed the condition “${detail}”.`;
119
+ case 'stage':
120
+ return `${name} entered the life stage “${detail}”.`;
121
+ }
122
+ }
123
+ function taskEndLine(p, label, status) {
124
+ if (status === 'completed')
125
+ return `${p.name} completed the task “${label}”.`;
126
+ if (status === 'failed')
127
+ return `${p.name} failed to complete the task “${label}”.`;
128
+ return `${p.name} was interrupted during the task “${label}”.`;
129
+ }
82
130
  /**
83
131
  * Whether standing suggestion `s` is heard by `p` at a decision over `affordances` (standing-advice SCOPE): its target
84
132
  * must be on offer or be the running activity, and if the action keeps commitments and was completed since the suggestion was given, doing it
@@ -208,7 +256,7 @@ export function tellDeath(c, dead, at, opts = {}) {
208
256
  targetId: dead.id,
209
257
  valence: -0.8,
210
258
  salience: 0.9,
211
- summary: `${dead.name} died`,
259
+ summary: q.lexicon?.plainEnglish ? `${dead.name} died.` : `${dead.name} died`,
212
260
  },
213
261
  ]);
214
262
  // A spouse is widowed even if the news went unattended.
@@ -355,7 +403,11 @@ export function declineJoint(c, proposal, proposer) {
355
403
  * activities are resolved as interrupted first, then the proposer begins the offer and each partner their
356
404
  * mirror. Returns the activities begun (empty while other partners have yet to answer).
357
405
  */
358
- export function acceptJoint(c, world, proposal, partner, partnerRecord, log = () => { }) {
406
+ export function acceptJoint(c, world, proposal, partner, partnerRecord, writeLog = () => { }) {
407
+ const log = (e) => {
408
+ const person = personById(c, e.personId);
409
+ writeLog(person?.lexicon?.plainEnglish ? { ...e, detail: plainEventDetail(person, e) } : e);
410
+ };
359
411
  if (proposal.status !== 'pending' || !proposal.partnerIds.includes(partner.id))
360
412
  return [];
361
413
  if (!proposal.accepted.includes(partner.id))
@@ -387,7 +439,9 @@ export function acceptJoint(c, world, proposal, partner, partnerRecord, log = ()
387
439
  at,
388
440
  personId: person.id,
389
441
  kind: 'finish',
390
- detail: `${act.action} ${outcome.status}`,
442
+ detail: person.lexicon?.plainEnglish
443
+ ? taskEndLine(person, act.affordance.label, outcome.status)
444
+ : `${act.action} ${outcome.status}`,
391
445
  decisionId: act.decisionId,
392
446
  affordanceId: act.affordanceId,
393
447
  action: act.action,
@@ -404,7 +458,9 @@ export function acceptJoint(c, world, proposal, partner, partnerRecord, log = ()
404
458
  at,
405
459
  personId: person.id,
406
460
  kind: 'begin',
407
- detail: `${offer.label} (joint ${proposal.id})`,
461
+ detail: person.lexicon?.plainEnglish
462
+ ? `${person.name} began the shared task “${offer.label}”. ${record.intention}`
463
+ : `${offer.label} (joint ${proposal.id})`,
408
464
  decisionId: record.id,
409
465
  affordanceId: offer.id,
410
466
  action: offer.action,
@@ -703,9 +759,12 @@ export function stepCommunity(c, world, until, opts = {}) {
703
759
  // Communities created before joint support (plain JSON from an older host) get the new bookkeeping.
704
760
  c.interrupts ??= {};
705
761
  c.joint ??= { nextId: 0, proposals: [], excluded: {} };
706
- const log = (e) => {
707
- if (events.length < maxEvents)
708
- events.push(e);
762
+ // Presentation only: retain host-authored summaries and every event field other than generated detail.
763
+ const log = (e, authored = false) => {
764
+ const person = personById(c, e.personId);
765
+ if (events.length < maxEvents) {
766
+ events.push(person?.lexicon?.plainEnglish && !authored ? { ...e, detail: plainEventDetail(person, e) } : e);
767
+ }
709
768
  };
710
769
  const contagion = opts.contagion ?? true;
711
770
  c.queued ??= {};
@@ -772,14 +831,17 @@ export function stepCommunity(c, world, until, opts = {}) {
772
831
  at: t,
773
832
  personId: p.id,
774
833
  kind: 'finish',
775
- detail: outcome.summary ?? `${act.action} ${outcome.status}`,
834
+ detail: outcome.summary ??
835
+ (p.lexicon?.plainEnglish
836
+ ? taskEndLine(p, act.affordance.label, outcome.status)
837
+ : `${act.action} ${outcome.status}`),
776
838
  decisionId: act.decisionId,
777
839
  affordanceId: act.affordanceId,
778
840
  action: act.action,
779
841
  };
780
842
  if (report)
781
843
  ev.status = report.status;
782
- log(ev);
844
+ log(ev, outcome.summary !== undefined);
783
845
  if (outcome.status === 'completed') {
784
846
  noteStandingDone(c, p, act, t);
785
847
  if (pairMinute !== t) {
@@ -837,14 +899,22 @@ export function stepCommunity(c, world, until, opts = {}) {
837
899
  detail: `joins ${acceptedProposal.offer.label}`,
838
900
  jointId: acceptedProposal.id,
839
901
  });
840
- acceptJoint(c, world, acceptedProposal, p, record, log);
902
+ acceptJoint(c, world, acceptedProposal, p, record, (event) => log(event, true));
841
903
  continue;
842
904
  }
843
905
  const chosen = affordances.find((a) => a.id === record.chosenAffordanceId);
844
906
  const continuing = act !== null && (record.chosenAffordanceId === null || record.chosenAffordanceId === act.affordanceId);
845
907
  if (act && continuing) {
846
908
  reviewed(p);
847
- log({ at: t, personId: p.id, kind: 'continue', detail: act.action, affordanceId: act.affordanceId });
909
+ log({
910
+ at: t,
911
+ personId: p.id,
912
+ kind: 'continue',
913
+ detail: p.lexicon?.plainEnglish
914
+ ? `${p.name} continued the task “${act.affordance.label}”.`
915
+ : act.action,
916
+ affordanceId: act.affordanceId,
917
+ });
848
918
  continue;
849
919
  }
850
920
  if (chosen && isJointOffer(c, p, chosen)) {
@@ -854,11 +924,15 @@ export function stepCommunity(c, world, until, opts = {}) {
854
924
  at: t,
855
925
  personId: p.id,
856
926
  kind: proposal.status === 'declined' ? 'decline' : 'propose',
857
- detail: `${chosen.label} with ${proposal.partnerIds.join(', ')}`,
927
+ detail: p.lexicon?.plainEnglish
928
+ ? proposal.status === 'declined'
929
+ ? `The shared task “${chosen.label}” could not start because a partner was unavailable.`
930
+ : `${chosen.label} with ${proposal.partnerIds.map((id) => personById(c, id)?.name ?? id).join(', ')}`
931
+ : `${chosen.label} with ${proposal.partnerIds.join(', ')}`,
858
932
  affordanceId: chosen.id,
859
933
  action: chosen.action,
860
934
  jointId: proposal.id,
861
- });
935
+ }, Boolean(p.lexicon?.plainEnglish && proposal.status === 'declined'));
862
936
  continue;
863
937
  }
864
938
  if (act) {
@@ -868,7 +942,9 @@ export function stepCommunity(c, world, until, opts = {}) {
868
942
  at: t,
869
943
  personId: p.id,
870
944
  kind: 'finish',
871
- detail: `${act.action} interrupted`,
945
+ detail: p.lexicon?.plainEnglish
946
+ ? taskEndLine(p, act.affordance.label, 'interrupted')
947
+ : `${act.action} interrupted`,
872
948
  decisionId: act.decisionId,
873
949
  affordanceId: act.affordanceId,
874
950
  action: act.action,
@@ -889,7 +965,9 @@ export function stepCommunity(c, world, until, opts = {}) {
889
965
  at: t,
890
966
  personId: p.id,
891
967
  kind: 'begin',
892
- detail: `${chosen.label} (${record.intention})`,
968
+ detail: p.lexicon?.plainEnglish
969
+ ? `${p.name} began the task “${chosen.label}”. ${record.intention}`
970
+ : `${chosen.label} (${record.intention})`,
893
971
  decisionId: record.id,
894
972
  affordanceId: chosen.id,
895
973
  action: chosen.action,
@@ -471,6 +471,16 @@ export const REPUTATION_DEFAULTS = {
471
471
  /** First-hand familiarity damps hearsay: scale = 1 - familiarityDamping × familiarity. */
472
472
  familiarityDamping: 0.5,
473
473
  };
474
+ /** Only authored, finite effects count; prototype properties are not character traits. */
475
+ function reputationEffect(trait, table) {
476
+ const effect = Object.hasOwn(table, trait) ? table[trait] : undefined;
477
+ return effect &&
478
+ Number.isFinite(effect.affection) &&
479
+ Number.isFinite(effect.trust) &&
480
+ Number.isFinite(effect.respect)
481
+ ? effect
482
+ : undefined;
483
+ }
474
484
  /** Splits a character proposition '<personId>:<trait>[:...]' into its subject and trait, if it is one. */
475
485
  export function parseTraitProp(prop, table = REPUTATION_TRAITS) {
476
486
  const parts = prop.split(':');
@@ -478,7 +488,7 @@ export function parseTraitProp(prop, table = REPUTATION_TRAITS) {
478
488
  return undefined;
479
489
  const personId = parts[0];
480
490
  const trait = parts[1];
481
- if (personId === '' || table[trait] === undefined)
491
+ if (personId === '' || reputationEffect(trait, table) === undefined)
482
492
  return undefined;
483
493
  return { personId, trait };
484
494
  }
@@ -509,7 +519,7 @@ export function applyReputationBelief(p, prop, before, after, now, table = REPUT
509
519
  }
510
520
  /** Whether a character claim about a person is unfavourable to them (for backbiting tags). */
511
521
  export function isUnfavourableTrait(trait, value, table = REPUTATION_TRAITS) {
512
- const e = table[trait];
522
+ const e = reputationEffect(trait, table);
513
523
  if (!e)
514
524
  return false;
515
525
  const sum = e.affection + e.trust + e.respect;
package/dist/types.d.ts CHANGED
@@ -1509,6 +1509,10 @@ export interface PersonSpec {
1509
1509
  values?: Partial<Values>;
1510
1510
  norms?: HeldNorm[];
1511
1511
  skills?: Record<string, Unit>;
1512
+ /** Authored habits, copied in order. Invalid or over-bound setup throws RangeError. */
1513
+ habits?: Habit[];
1514
+ /** Authored expectations, copied in order. Invalid or over-bound setup throws RangeError. */
1515
+ expectations?: ActionExpectation[];
1512
1516
  body?: Partial<Omit<BodyState, 'injuries' | 'illnesses' | 'nextId'>>;
1513
1517
  needs?: Partial<NeedReservoirs>;
1514
1518
  relationships?: (Partial<Relationship> & {
@@ -1767,6 +1771,8 @@ export interface VoiceConflict {
1767
1771
  * slots documented next to each key.
1768
1772
  */
1769
1773
  export interface Lexicon {
1774
+ /** Use complete, plain English in built-in narration. Omitted keeps exact legacy wording. */
1775
+ plainEnglish?: boolean;
1770
1776
  /** Include person identity and stable temperament in assent/deferral selection (no RNG draws). */
1771
1777
  variety?: boolean;
1772
1778
  /** What material gains mean here, e.g. 'food', 'household keep' or 'resources'; absent keeps legacy money lines. */
@@ -233,6 +233,18 @@ export declare function noteVoicePromise(p: Person, voiceId: EntityId, event: {
233
233
  at?: Minute;
234
234
  action?: string;
235
235
  }): VoiceRelation;
236
+ /**
237
+ * SCOPE (host trust gesture): add a host-supplied signed amount to an existing advising voice's trust,
238
+ * clamped to 0..1, and record the realized change in the usual bounded history. Magnitudes and reason
239
+ * are host premises, not calibrated claims. Missing voices return undefined without creating a voice.
240
+ * Non-finite amounts are ignored. No pressure, credit, counters, memory, time or RNG changes occur.
241
+ * Empty/non-string reasons and non-string actions reject before mutation with RangeError.
242
+ */
243
+ export declare function gestureVoiceTrust(p: Person, voiceId: EntityId, amount: number, event: {
244
+ reason: string;
245
+ at?: Minute;
246
+ action?: string;
247
+ }): VoiceRelation | undefined;
236
248
  /**
237
249
  * The next minute after `now` at which a pending commitment reaches `wakeCommitmentPressure`, the level at which a
238
250
  * sleeper may wake to serve it (the asleep veto lifts). The composite caps a sleeper's review there so a duty whose
package/dist/will/will.js CHANGED
@@ -37,6 +37,7 @@ import { commitmentPressure, isUnderWay, pressureReachedAt } from '../agenda/ind
37
37
  import { downedAllows, readCapacities } from '../body/index.js';
38
38
  import { CONSCIENCE_DEFAULTS, normVeto } from '../conscience/index.js';
39
39
  import { clamp01, cmpStr, decay, dexp, dpow, hasNumbers, isObj, random, round } from '../core/index.js';
40
+ import { commitmentLabel } from '../presentation.js';
40
41
  import { skillLevel } from '../skills/index.js';
41
42
  import { MINUTES_PER_DAY, MINUTES_PER_HOUR } from '../types.js';
42
43
  export const WILL_DEFAULTS = {
@@ -292,6 +293,27 @@ export function noteVoicePromise(p, voiceId, event) {
292
293
  noteTrust(v, v.trust - before, event.kept ? 'promise-kept' : 'promise-broken', at, event.action);
293
294
  return v;
294
295
  }
296
+ /**
297
+ * SCOPE (host trust gesture): add a host-supplied signed amount to an existing advising voice's trust,
298
+ * clamped to 0..1, and record the realized change in the usual bounded history. Magnitudes and reason
299
+ * are host premises, not calibrated claims. Missing voices return undefined without creating a voice.
300
+ * Non-finite amounts are ignored. No pressure, credit, counters, memory, time or RNG changes occur.
301
+ * Empty/non-string reasons and non-string actions reject before mutation with RangeError.
302
+ */
303
+ export function gestureVoiceTrust(p, voiceId, amount, event) {
304
+ if (!event || typeof event.reason !== 'string' || event.reason.length === 0)
305
+ throw new RangeError('A trust gesture needs a reason');
306
+ if (event.action !== undefined && typeof event.action !== 'string')
307
+ throw new RangeError('Invalid trust gesture action');
308
+ const v = voiceOf(p, voiceId);
309
+ if (!v || !Number.isFinite(amount))
310
+ return v;
311
+ const before = v.trust;
312
+ v.trust = clamp01(before + amount);
313
+ const at = event.at !== undefined && Number.isFinite(event.at) ? event.at : p.now;
314
+ noteTrust(v, v.trust - before, event.reason, at, event.action);
315
+ return v;
316
+ }
295
317
  const finite = (x) => x === undefined || Number.isFinite(x);
296
318
  /** Host numbers the framework computes with must be finite; a NaN offer is vetoed, never chosen. */
297
319
  function wellFormed(aff) {
@@ -829,7 +851,10 @@ function judgeVoices(p, ctx, suggestions, prepared, selection) {
829
851
  kind: 'notNow',
830
852
  commitmentId: v.omission,
831
853
  ...(winnerAff ? { insteadAffordanceId: winnerAff.id } : {}),
832
- counterOffer: { affordanceId: suggested[0]?.affordanceId, label: dutyLabel(duty, winnerAff) },
854
+ counterOffer: {
855
+ affordanceId: suggested[0]?.affordanceId,
856
+ label: dutyLabel(duty, winnerAff, p),
857
+ },
833
858
  }));
834
859
  }
835
860
  return out(resolution('refused', v.reason, { kind: 'willNot', commitmentId: v.omission }));
@@ -886,7 +911,12 @@ function judgeVoices(p, ctx, suggestions, prepared, selection) {
886
911
  kind: 'notNow',
887
912
  insteadAffordanceId: winnerAff?.id,
888
913
  counterOffer: winnerAff
889
- ? { affordanceId: bestSuggested.affordanceId, label: `after I ${winnerAff.label}` }
914
+ ? {
915
+ affordanceId: bestSuggested.affordanceId,
916
+ label: p.lexicon?.plainEnglish
917
+ ? `after I finish this task (${winnerAff.label})`
918
+ : `after I ${winnerAff.label}`,
919
+ }
890
920
  : { affordanceId: bestSuggested.affordanceId, label: 'later' },
891
921
  }));
892
922
  });
@@ -940,7 +970,11 @@ function arbitrateInsistence(p, judged, considered, affById, ev) {
940
970
  insteadAffordanceId: forced,
941
971
  counterOffer: {
942
972
  ...(firstTarget !== undefined ? { affordanceId: firstTarget } : {}),
943
- label: forcedAff ? `after I ${forcedAff.label}` : 'later',
973
+ label: forcedAff
974
+ ? p.lexicon?.plainEnglish
975
+ ? `after I finish this task (${forcedAff.label})`
976
+ : `after I ${forcedAff.label}`
977
+ : 'later',
944
978
  },
945
979
  };
946
980
  if (j.res.likelihood !== undefined)
@@ -1134,7 +1168,12 @@ export function conflictBetweenVoices(considered, resolutions, chosenAffordanceI
1134
1168
  };
1135
1169
  }
1136
1170
  /** Counter-offer for a request deferred behind a duty: "after I pray", "after I feed the children". */
1137
- function dutyLabel(duty, winner) {
1171
+ function dutyLabel(duty, winner, p) {
1172
+ if (p.lexicon?.plainEnglish) {
1173
+ if (duty)
1174
+ return `after I finish this duty (${commitmentLabel(duty, p.lexicon)})`;
1175
+ return winner ? `after I finish this task (${winner.label})` : 'after my duty';
1176
+ }
1138
1177
  if (duty?.kind === 'worship')
1139
1178
  return duty.label ? `after I pray ${duty.label}` : 'after I pray';
1140
1179
  if (duty?.label)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adam0white/human-framework",
3
- "version": "2.18.0",
3
+ "version": "2.20.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",