@adam0white/human-framework 2.7.0 → 2.8.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.
@@ -125,6 +125,11 @@ export interface BodyPerceptionContext {
125
125
  fastingDays: number;
126
126
  }
127
127
  export type BodyParams = typeof BODY_DEFAULTS;
128
+ /**
129
+ * Turn on fullness (2.8.0): consumption for a need already past its comfort level carries a small cost in choice
130
+ * (see the fullness SCOPE in `cognition/`). Idempotent; off by default so existing hosts replay exactly.
131
+ */
132
+ export declare function enableFullness(p: Person): void;
128
133
  /** Turn on the fatigue store (2.6.0): accumulated exertion that recovers with rest and sleep. Idempotent. */
129
134
  export declare function enableFatigueStore(p: Person): void;
130
135
  export declare function createBody(init: PersonSpec['body'] | undefined, now: Minute): BodyState;
package/dist/body/body.js CHANGED
@@ -238,9 +238,18 @@ export function sanitizeBody(b, now) {
238
238
  delete b.lastSleep;
239
239
  if (b.lastDowned !== undefined && !span(b.lastDowned))
240
240
  delete b.lastDowned;
241
+ if (b.fullness !== undefined && b.fullness !== true)
242
+ delete b.fullness;
241
243
  if (b.depletion !== undefined)
242
244
  b.depletion = typeof b.depletion === 'number' && Number.isFinite(b.depletion) ? clamp01(b.depletion) : 0;
243
245
  }
246
+ /**
247
+ * Turn on fullness (2.8.0): consumption for a need already past its comfort level carries a small cost in choice
248
+ * (see the fullness SCOPE in `cognition/`). Idempotent; off by default so existing hosts replay exactly.
249
+ */
250
+ export function enableFullness(p) {
251
+ p.body.fullness = true;
252
+ }
244
253
  /** Turn on the fatigue store (2.6.0): accumulated exertion that recovers with rest and sleep. Idempotent. */
245
254
  export function enableFatigueStore(p) {
246
255
  if (p.body.depletion === undefined)
@@ -66,6 +66,16 @@ export declare const COGNITION_DEFAULTS: {
66
66
  commitmentInertiaFade: number;
67
67
  /** Social pull per partner on a joint offer: jointTrust × (relationship trust - 0.5). */
68
68
  jointTrust: number;
69
+ /**
70
+ * Staying with a partner who is going (2.8.0, `Affordance.alongside`): 'together:<id>' = togetherScale ×
71
+ * closeness. It does not satiate with belonging: being together now does not remove the pull of not being left.
72
+ */
73
+ togetherScale: number;
74
+ /**
75
+ * Fullness (2.8.0, opt-in `body.fullness`): 'full:<need>' = -fullnessScale × gain × over², over = how far the
76
+ * need's level is past its comfort threshold toward 1, for food, water and relief.
77
+ */
78
+ fullnessScale: number;
69
79
  /** Multiplier on standing advice terms relative to a live suggestion of the same strength (N9). */
70
80
  rememberedScale: number;
71
81
  /**
@@ -129,6 +139,8 @@ export interface DecideContext extends ConsiderContext {
129
139
  quiet?: boolean;
130
140
  /** A host command in force (1.6.0); passed to the will. */
131
141
  command?: Command;
142
+ /** Drop the running activity's inertia bonus (2.8.0); passed to the will. */
143
+ reconsider?: boolean;
132
144
  }
133
145
  export interface Decision {
134
146
  record: DecisionRecord;
@@ -23,6 +23,17 @@
23
23
  * bias; and because losses are amplified too, a host must carry the benefit of paying a debt (a commitment,
24
24
  * an advertised `safety` gain) or paying looks only like a loss. The spec's "security need" is read as the
25
25
  * `safety` need: `security` is a value, not a need, and no need is added.
26
+ *
27
+ * SCOPE (2.8.0, both additive): an offer naming `alongside` partners (people already taking that course) gets one
28
+ * `together:<id>` term per known, living partner, `togetherScale` × closeness, without belonging satiation: the
29
+ * pull is not to be left behind, which stays when the pair is together and belonging is full (The Rope's resting
30
+ * partner, 2026-10-18 reproduction: belonging terms near 0.01 against inertia of 0.07-0.15). Shape only, after
31
+ * joint commitment and the sense of commitment in joint action (Gilbert 1990; Michael and Pacherie 2015; cited unaudited, research/empirical-models.md §6b); it does
32
+ * not model who leads, negotiation, or obligations between the partners. With opt-in `body.fullness`, an offer
33
+ * that advertises food, water or relief while that need is past its comfort threshold gets `full:<need>` =
34
+ * -fullnessScale × gain × over²: eating or drinking when full is mildly unpleasant (negative alliesthesia, unaudited citation, Cabanac
35
+ * 1971), so idle choice stops cycling short consumption at full hydration (The Rope, owner Kestrel export
36
+ * 2026-10-19). It models no bladder, sensory-specific satiety by food, or overeating for comfort.
26
37
  */
27
38
  import { emotionLevel, inBreak } from '../affect/index.js';
28
39
  import { agendaTermsWithGoalEstimates } from '../agenda/index.js';
@@ -32,12 +43,12 @@ import { clamp01, cmpStr, dexp, minuteOfDay, round } from '../core/index.js';
32
43
  import { habitEase, habitPull } from '../habits/index.js';
33
44
  import { expectedEffect } from '../memory/index.js';
34
45
  import { methodEvidence } from '../methods/index.js';
35
- import { urgencyOf } from '../needs/index.js';
46
+ import { levelOf, needThreshold, urgencyOf } from '../needs/index.js';
36
47
  import { partneringTerms } from '../partnering/index.js';
37
48
  import { promptIn, promptingTerms } from '../promptings/index.js';
38
49
  import { bodyForecastTerm, receivedGoalRoute } from '../prospection/index.js';
39
50
  import { reviewTerms } from '../review/index.js';
40
- import { companionSteadiness, socialTerms } from '../social/index.js';
51
+ import { closeness, companionSteadiness, isDeceasedTie, relationshipWith, socialTerms, } from '../social/index.js';
41
52
  import { PHYSIOLOGICAL_NEEDS, VALUE_KEYS } from '../types.js';
42
53
  import { adviceWeight, resolveChoice, safeUtility, standingAdvice, suggestionTargets, voicesIn, } from '../will/index.js';
43
54
  export const COGNITION_DEFAULTS = {
@@ -103,6 +114,16 @@ export const COGNITION_DEFAULTS = {
103
114
  commitmentInertiaFade: 0.15,
104
115
  /** Social pull per partner on a joint offer: jointTrust × (relationship trust - 0.5). */
105
116
  jointTrust: 0.6,
117
+ /**
118
+ * Staying with a partner who is going (2.8.0, `Affordance.alongside`): 'together:<id>' = togetherScale ×
119
+ * closeness. It does not satiate with belonging: being together now does not remove the pull of not being left.
120
+ */
121
+ togetherScale: 0.12,
122
+ /**
123
+ * Fullness (2.8.0, opt-in `body.fullness`): 'full:<need>' = -fullnessScale × gain × over², over = how far the
124
+ * need's level is past its comfort threshold toward 1, for food, water and relief.
125
+ */
126
+ fullnessScale: 0.03,
106
127
  // --- lane will+cognition ---
107
128
  /** Multiplier on standing advice terms relative to a live suggestion of the same strength (N9). */
108
129
  rememberedScale: 1,
@@ -226,6 +247,8 @@ export function materialValue(p, material, scarcity) {
226
247
  const sat = Math.sign(material) * (1 - dexp(-Math.abs(material) / scale));
227
248
  return K.materialWeight * sat * 2 * weight * (1 + K.scarcityMaterialGain * scarcity);
228
249
  }
250
+ /** Needs whose consumption can overfill (2.8.0 fullness): what `body.consume` feeds. */
251
+ const FULL_NEEDS = ['food', 'water', 'relief'];
229
252
  /** Score one option. Pure apart from reads; terms are rounded for stable snapshots. */
230
253
  export function consider(p, aff, ctx) {
231
254
  const K = COGNITION_DEFAULTS;
@@ -248,6 +271,19 @@ export function consider(p, aff, ctx) {
248
271
  const scarce = id === 'safety' ? 1 + K.scarcitySafetyGain * scarcity : 1;
249
272
  push(`need:${id}`, K.needScale * u * gain * success * scarce);
250
273
  }
274
+ // Fullness (2.8.0, opt-in): consuming for a need already past comfort is mildly unpleasant (alliesthesia).
275
+ if (p.body.fullness) {
276
+ for (const id of FULL_NEEDS) {
277
+ const gain = aff.advertises[id] ?? 0;
278
+ if (!(gain > 0))
279
+ continue;
280
+ const level = levelOf(ctx.needs, id);
281
+ const comfort = needThreshold(p, id);
282
+ const over = clamp01((level - comfort) / Math.max(1e-6, 1 - comfort));
283
+ if (over > 0)
284
+ push(`full:${id}`, -K.fullnessScale * Math.min(1, gain) * over * over);
285
+ }
286
+ }
251
287
  if (expected.samples > 0) {
252
288
  let relevance = 1;
253
289
  if (expected.valence > 0) {
@@ -324,6 +360,16 @@ export function consider(p, aff, ctx) {
324
360
  // Social pulls. Per-person emotion tendencies enter at the same scale as tag tendencies.
325
361
  for (const t of socialTerms(p, aff, social))
326
362
  push(t.source, K.socialScale * (t.value > 0 ? social.satiation * refractory() : 1) * t.value);
363
+ // Going with partners who are already taking this course (2.8.0).
364
+ if (aff.alongside) {
365
+ for (const id of [...new Set(aff.alongside)].sort()) {
366
+ if (id === p.id || !p.social.relationships.some((r) => r.otherId === id))
367
+ continue;
368
+ if (isDeceasedTie(relationshipWith(p, id)))
369
+ continue;
370
+ push(`together:${id}`, K.togetherScale * closeness(p, id));
371
+ }
372
+ }
327
373
  // Joint offers: whether I trust the partners to do their share.
328
374
  if (tags.includes('joint')) {
329
375
  for (const id of [...(aff.with ?? [])].sort()) {
@@ -453,6 +499,8 @@ export function scoreAll(p, affordances, ctx) {
453
499
  };
454
500
  if (ctx.command)
455
501
  willCtx.command = ctx.command;
502
+ if (ctx.reconsider)
503
+ willCtx.reconsider = true;
456
504
  return { sorted, considered, willCtx };
457
505
  }
458
506
  /**
@@ -479,6 +527,8 @@ export function decide(p, affordances, ctx) {
479
527
  };
480
528
  if (res.suggestion)
481
529
  record.suggestion = res.suggestion;
530
+ if (ctx.reconsider)
531
+ record.reconsider = true;
482
532
  if (res.suggestions && (res.suggestions.length > 1 || ctx.suggestions !== undefined))
483
533
  record.suggestions = res.suggestions;
484
534
  const needDeltas = {};
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Human Framework v1 public surface. */
2
- export declare const FRAMEWORK_VERSION = "2.7.0";
2
+ export declare const FRAMEWORK_VERSION = "2.8.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, PrayerCalendar, PrayerSchedule, PrayerTimes, Retimer, } from './agenda/index.ts';
@@ -30,7 +30,7 @@ export * from './partnering/index.ts';
30
30
  export * from './person.ts';
31
31
  export * from './promptings/index.ts';
32
32
  export type { ConsequenceEvidence } from './prospection/index.ts';
33
- export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, FORECAST_DEFAULTS, forecastLine, PROSPECTION_DEFAULTS, } from './prospection/index.ts';
33
+ export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, FORECAST_DEFAULTS, forecastLine, forecastThresholds, PROSPECTION_DEFAULTS, } from './prospection/index.ts';
34
34
  export * from './restore.ts';
35
35
  export * from './review/index.ts';
36
36
  export * from './scenarios/index.ts';
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Human Framework v1 public surface. */
2
- export const FRAMEWORK_VERSION = '2.7.0';
2
+ export const FRAMEWORK_VERSION = '2.8.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, isUnderWay, MAKRUH_DEFAULTS, makruhWindows, missedExcuse, onFinished, owedMakeUps, PRAYER_IMPORTANCE, PURPOSE_DEFAULTS, prayerWindows, pressureReachedAt, promise, proposeGoals, RAMADAN_DEFAULTS, ramadanFast, ramadanMeals, release as releaseCommitment, retimeCommitments, revisePurposes, scheduleMakeUp, spanMeetsWindow, suhoorWindow, timesFor, violatesAbstention, } from './agenda/index.js';
@@ -26,7 +26,7 @@ export * from './needs/index.js';
26
26
  export * from './partnering/index.js';
27
27
  export * from './person.js';
28
28
  export * from './promptings/index.js';
29
- export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, FORECAST_DEFAULTS, forecastLine, PROSPECTION_DEFAULTS, } from './prospection/index.js';
29
+ export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, FORECAST_DEFAULTS, forecastLine, forecastThresholds, PROSPECTION_DEFAULTS, } from './prospection/index.js';
30
30
  export * from './restore.js';
31
31
  export * from './review/index.js';
32
32
  export * from './scenarios/index.js';
package/dist/migrate.js CHANGED
@@ -58,6 +58,11 @@ export const MIGRATIONS = {
58
58
  note: 'the surroundings field `ambient.now` is renamed `ambient.percept`; per-person retention is optional and starts absent; behaviour unchanged',
59
59
  apply: renameAmbient,
60
60
  },
61
+ '2.7.0': {
62
+ to: '2.8.0',
63
+ note: 'fullness (body.fullness) is optional and starts absent; offers may name alongside partners and decisions may reconsider; existing hosts retain exact behavior',
64
+ apply: stamp,
65
+ },
61
66
  '2.6.0': {
62
67
  to: '2.7.0',
63
68
  note: 'the evening review (review) is optional and starts absent; existing hosts retain exact behavior',
package/dist/person.d.ts CHANGED
@@ -103,6 +103,12 @@ export interface DecideOptions {
103
103
  necessity?: boolean;
104
104
  /** Why this decision was forced (recorded on the decision); defaults to the activity's pending interrupt. */
105
105
  interrupt?: string;
106
+ /**
107
+ * Reconsider on the merits (2.8.0): the running activity gets no inertia bonus in this decision, for a review the
108
+ * host forces because something changed (a partner leaving, a call). Promise inertia (`begin` with a promise) and
109
+ * every other term stay. Absent = the usual hysteresis.
110
+ */
111
+ reconsider?: boolean;
106
112
  }
107
113
  /**
108
114
  * How this person would answer a suggestion right now, without changing them or consuming randomness
package/dist/person.js CHANGED
@@ -22,7 +22,7 @@
22
22
  import { actionTendencies, advanceAffect, appraise, CRISIS_DEFAULTS, checkCrisis, createAffect, easeBreak, emotionLevel, feel, readAffect, regulate, release, skipAffect, skipCrisis, strain, tendencyEmotions, } from './affect/index.js';
23
23
  import { advanceAgenda, createAgenda, onFinished, promise, proposeGoals, revisePurposes, } from './agenda/index.js';
24
24
  import { advanceBeliefs, attend, believe, confirm, credence } from './beliefs/index.js';
25
- import { advanceBody, BODY_DEFAULTS, checkDowned, clearDowned, consume, createBody, downedAllows, enableFatigueStore, expose, injure, nextBodyThreshold, readBody, setDowned, sicken, skipBody, } from './body/index.js';
25
+ import { advanceBody, BODY_DEFAULTS, checkDowned, clearDowned, consume, createBody, downedAllows, enableFatigueStore, enableFullness, expose, injure, nextBodyThreshold, readBody, setDowned, sicken, skipBody, } from './body/index.js';
26
26
  import { ageCharacter, enableCharacterChange, noteCharacterDay, noteCharacterSocial, } from './character/index.js';
27
27
  import { closeDay, enableYearbook, endDay, noteAnswer, noteCommitments, noteDecision, noteMood, noteOutcome, trimChronicle, } from './chronicle/index.js';
28
28
  import { decide as cognitionDecide, desperationOf, scoreAll } from './cognition/index.js';
@@ -37,7 +37,7 @@ import { intentionFor, narrateDecision, voiceLine } from './narrate/index.js';
37
37
  import { advanceNeeds, createNeeds, levelOf, meanSatisfaction, readNeeds, satisfy, urgencyOf, } from './needs/index.js';
38
38
  import { widow } from './partnering/index.js';
39
39
  import { enablePromptings, nafsPatterns, notePromptings } from './promptings/index.js';
40
- import { bodyForecastFor, consumeConsequences, forecastLine, pruneConsequences, receiveConsequences, } from './prospection/index.js';
40
+ import { bodyForecastFor, consumeConsequences, forecastLine, forecastThresholds, pruneConsequences, receiveConsequences, } from './prospection/index.js';
41
41
  import { enableReview, reviewIfDue } from './review/index.js';
42
42
  import { enableSkillRetention, learnByWatching, practise, seedSkills, } from './skills/index.js';
43
43
  import { advanceSocial, applyReputationBelief, careFor, closeness, judge, markDeceased, relationshipWith, seedRelationships, socialEvent, threatAppraisal, } from './social/index.js';
@@ -157,6 +157,8 @@ export function createPerson(spec) {
157
157
  enableSkillRetention(p);
158
158
  if (on?.fatigueStore)
159
159
  enableFatigueStore(p);
160
+ if (on?.fullness)
161
+ enableFullness(p);
160
162
  if (on?.promptings)
161
163
  enablePromptings(p, on.promptings);
162
164
  if (on?.review)
@@ -756,6 +758,8 @@ function decisionInputs(p, affordances, id, opts) {
756
758
  const cmd = opts.command ?? p.will.command;
757
759
  if (cmd)
758
760
  ctx.command = commandOf(cmd);
761
+ if (opts.reconsider)
762
+ ctx.reconsider = true;
759
763
  return { list, ctx };
760
764
  }
761
765
  /** The `Command` part of a command in force (without the will's bookkeeping). */
@@ -1430,5 +1434,5 @@ export function nafsReading(p) {
1430
1434
  */
1431
1435
  export function bodyForecast(p, aff) {
1432
1436
  const spans = bodyForecastFor(p, aff, lifeModifiers(p), fastingCtx(p));
1433
- return { spans, line: forecastLine(spans) };
1437
+ return { spans, line: forecastLine(spans, forecastThresholds(aff)) };
1434
1438
  }
@@ -16,7 +16,8 @@
16
16
  * open wound can read as being spent, and death in the forecast reads as fully spent. The term is absolute
17
17
  * (see `bodyForecastTerm`). It does not plan, search routes, schedule rests on the way or
18
18
  * predict injuries, and it is not a calibrated estimate of human anticipation. Every number is an engineering
19
- * default.
19
+ * default. 2.8.0: an offer may carry its own thresholds (`Affordance.forecastAt`, from The Glance's night
20
+ * keepers); offers without it read exactly as before.
20
21
  */
21
22
  import { type BodyForecastSpan, type BodyPerceptionContext } from '../body/index.ts';
22
23
  import type { Affordance, LifeModifiers, Person, Term } from '../types.ts';
@@ -31,6 +32,14 @@ export declare const FORECAST_DEFAULTS: {
31
32
  readonly maxSpans: 4;
32
33
  readonly maxMinutes: number;
33
34
  };
35
+ /**
36
+ * The forecast thresholds for an offer (2.8.0): its `forecastAt` when it forms a valid pair, else the defaults.
37
+ * Pure.
38
+ */
39
+ export declare function forecastThresholds(aff: Pick<Affordance, 'forecastAt'>): {
40
+ comfortAt: number;
41
+ spentAt: number;
42
+ };
34
43
  /** The person's forecast for an offer: the act, then its follow-on spans. Empty without `followedBy`. Pure. */
35
44
  export declare function bodyForecastFor(p: Person, aff: Affordance, mods: LifeModifiers, perception?: BodyPerceptionContext): BodyForecastSpan[];
36
45
  /**
@@ -41,5 +50,8 @@ export declare function bodyForecastFor(p: Person, aff: Affordance, mods: LifeMo
41
50
  * too, which cancelled the very case the forecast exists for. Pure.
42
51
  */
43
52
  export declare function bodyForecastTerm(p: Person, aff: Affordance, mods: LifeModifiers, perception?: BodyPerceptionContext): Term | undefined;
44
- /** A first-person line for the forecast (narration only). */
45
- export declare function forecastLine(spans: readonly BodyForecastSpan[]): string;
53
+ /** A first-person line for the forecast (narration only); `at` is the offer's thresholds (2.8.0, default as before). */
54
+ export declare function forecastLine(spans: readonly BodyForecastSpan[], at?: {
55
+ comfortAt: number;
56
+ spentAt: number;
57
+ }): string;
@@ -16,7 +16,8 @@
16
16
  * open wound can read as being spent, and death in the forecast reads as fully spent. The term is absolute
17
17
  * (see `bodyForecastTerm`). It does not plan, search routes, schedule rests on the way or
18
18
  * predict injuries, and it is not a calibrated estimate of human anticipation. Every number is an engineering
19
- * default.
19
+ * default. 2.8.0: an offer may carry its own thresholds (`Affordance.forecastAt`, from The Glance's night
20
+ * keepers); offers without it read exactly as before.
20
21
  */
21
22
  import { BODY_DEFAULTS, forecastBody, } from '../body/index.js';
22
23
  import { clamp01, round } from '../core/index.js';
@@ -31,6 +32,21 @@ export const FORECAST_DEFAULTS = {
31
32
  maxSpans: 4,
32
33
  maxMinutes: 24 * 60,
33
34
  };
35
+ /**
36
+ * The forecast thresholds for an offer (2.8.0): its `forecastAt` when it forms a valid pair, else the defaults.
37
+ * Pure.
38
+ */
39
+ export function forecastThresholds(aff) {
40
+ const D = FORECAST_DEFAULTS;
41
+ const at = aff.forecastAt;
42
+ if (!at || typeof at !== 'object')
43
+ return { comfortAt: D.comfortAt, spentAt: D.spentAt };
44
+ const c = Number.isFinite(at.comfortAt) ? at.comfortAt : D.comfortAt;
45
+ const s = Number.isFinite(at.spentAt) ? at.spentAt : D.spentAt;
46
+ return c >= 0 && c < s && s <= 1
47
+ ? { comfortAt: c, spentAt: s }
48
+ : { comfortAt: D.comfortAt, spentAt: D.spentAt };
49
+ }
34
50
  const smooth = (a, b, x) => {
35
51
  const t = clamp01((x - a) / (b - a));
36
52
  return t * t * (3 - 2 * t);
@@ -81,12 +97,13 @@ export function bodyForecastTerm(p, aff, mods, perception) {
81
97
  if (spans.length === 0)
82
98
  return undefined;
83
99
  const D = FORECAST_DEFAULTS;
100
+ const at = forecastThresholds(aff);
84
101
  const worst = Math.max(...spans.map((s) => s.worst));
85
- const v = round(-D.forecastScale * smooth(D.comfortAt, D.spentAt, worst));
102
+ const v = round(-D.forecastScale * smooth(at.comfortAt, at.spentAt, worst));
86
103
  return v < 0 ? { source: 'forecast:body', value: v } : undefined;
87
104
  }
88
- /** A first-person line for the forecast (narration only). */
89
- export function forecastLine(spans) {
105
+ /** A first-person line for the forecast (narration only); `at` is the offer's thresholds (2.8.0, default as before). */
106
+ export function forecastLine(spans, at = FORECAST_DEFAULTS) {
90
107
  const after = spans.slice(1);
91
108
  if (after.length === 0)
92
109
  return '';
@@ -95,9 +112,9 @@ export function forecastLine(spans) {
95
112
  if (s.worst > w.worst)
96
113
  w = s;
97
114
  const what = { fatigue: 'spent', sleepiness: 'too sleepy', hunger: 'too hungry', thirst: 'too thirsty' }[w.worstOf];
98
- if (w.worst >= FORECAST_DEFAULTS.spentAt)
115
+ if (w.worst >= at.spentAt)
99
116
  return `I can do it, but I'll be ${what} for ${w.label}.`;
100
- if (w.worst >= FORECAST_DEFAULTS.comfortAt)
117
+ if (w.worst >= at.comfortAt)
101
118
  return `I can do it, though ${w.label} will be hard.`;
102
119
  return `I'll still have enough for ${w.label}.`;
103
120
  }
package/dist/types.d.ts CHANGED
@@ -91,6 +91,11 @@ export interface BodyState {
91
91
  * recovering with rest and faster with sleep. Raises felt fatigue and lowers capacity. Absent = no store.
92
92
  */
93
93
  depletion?: Unit;
94
+ /**
95
+ * Fullness (2.8.0, opt-in via `enableFullness`): when true, eating, drinking or taking relief for a need already
96
+ * past its comfort level costs a little (`full:<need>` term) instead of being free. Absent = no term.
97
+ */
98
+ fullness?: boolean;
94
99
  asleep: boolean;
95
100
  /** Minute the current sleep or wake period began. */
96
101
  since: Minute;
@@ -832,7 +837,7 @@ export declare const PERSON_SCHEMA = "human/person@1";
832
837
  * and an activity that would cover a closing stretch is reviewed when the stretch begins (`isUnderWay`,
833
838
  * `dutyReviewAt`). Person shape unchanged.
834
839
  */
835
- export declare const ENGINE_VERSION = "2.7.0";
840
+ export declare const ENGINE_VERSION = "2.8.0";
836
841
  /** A pregnancy carried by this person (host-driven; see `family/`). */
837
842
  export interface Pregnancy {
838
843
  fatherId: PersonId;
@@ -1067,6 +1072,12 @@ export interface Affordance {
1067
1072
  placeId?: EntityId;
1068
1073
  /** Other people taking part. */
1069
1074
  with?: PersonId[];
1075
+ /**
1076
+ * People already taking this course (2.8.0): a partner moving on, a companion leaving for home. Choosing it keeps
1077
+ * the person with them, a `together:<id>` term from closeness (`cognition` SCOPE). Unlike `with`, it says who is
1078
+ * going this way, not who would take part. Absent = no term, exactly as before.
1079
+ */
1080
+ alongside?: PersonId[];
1070
1081
  /** Minutes the activity occupies (including travel if the host folds it in). */
1071
1082
  duration: number;
1072
1083
  /** Physical exertion 0..1. */
@@ -1105,6 +1116,16 @@ export interface Affordance {
1105
1116
  * spent during them. Absent = no forecast, exactly as before.
1106
1117
  */
1107
1118
  followedBy?: FollowOn[];
1119
+ /**
1120
+ * Readout thresholds for this offer's forecast (2.8.0), for hosts whose people live higher on a readout (a
1121
+ * night keeper at sleepiness 0.7). `comfortAt` is where the forecast starts to weigh and `spentAt` where it
1122
+ * weighs fully; either absent, or a pair that is not 0 ≤ comfortAt < spentAt ≤ 1, falls back to
1123
+ * `FORECAST_DEFAULTS`. Ignored without `followedBy`.
1124
+ */
1125
+ forecastAt?: {
1126
+ comfortAt?: Unit;
1127
+ spentAt?: Unit;
1128
+ };
1108
1129
  /** Set on a partner's mirror offer by the joint protocol in `sim/` (the proposal id). */
1109
1130
  jointId?: string;
1110
1131
  /** Material gain/cost in host units (e.g. coins). Valued via security/achievement/power values. */
@@ -1354,6 +1375,8 @@ export interface DecisionRecord {
1354
1375
  review?: boolean;
1355
1376
  /** Why this decision was brought forward, when `interrupt()` forced it (e.g. 'percept:injury'). */
1356
1377
  interrupt?: string;
1378
+ /** Made with `DecideOptions.reconsider` (2.8.0): the running activity had no inertia bonus. */
1379
+ reconsider?: true;
1357
1380
  /**
1358
1381
  * One resolution per voice that suggested something in this decision, in voice-id order (N1). `suggestion` is
1359
1382
  * the credited voice's resolution (same object), or the only one when a single voice spoke.
@@ -1402,6 +1425,8 @@ export interface PersonSpec {
1402
1425
  skillRetention?: boolean;
1403
1426
  /** Accumulated exertion (2.6.0): the same as `enableFatigueStore(p)` after creation. */
1404
1427
  fatigueStore?: boolean;
1428
+ /** Fullness (2.8.0): the same as `enableFullness(p)` after creation. */
1429
+ fullness?: boolean;
1405
1430
  /** Promptings and turning away (2.4.0): the same as `enablePromptings(p, spec)` after creation. */
1406
1431
  promptings?: PromptingsSpec;
1407
1432
  /** Evening self-review (2.7.0): the same as `enableReview(p, spec)` after creation. */
package/dist/types.js CHANGED
@@ -120,4 +120,4 @@ export const PERSON_SCHEMA = 'human/person@1';
120
120
  * and an activity that would cover a closing stretch is reviewed when the stretch begins (`isUnderWay`,
121
121
  * `dutyReviewAt`). Person shape unchanged.
122
122
  */
123
- export const ENGINE_VERSION = '2.7.0';
123
+ export const ENGINE_VERSION = '2.8.0';
@@ -28,6 +28,9 @@
28
28
  * when the answer leaves the person doing what they are doing. A host command is a separate input that yields the
29
29
  * `commanded` verdict (see the commanded-control SCOPE on `commandOutcome`); `learnFromVoice` learns from assented,
30
30
  * complied and commanded activities, and `adoptVoiceTrust` moves trust toward a target without a suggestion.
31
+ * 2.8.0: `seedVoice` sets an authored starting trust after creation; `noteVoicePromise` learns from whether the
32
+ * voice kept its own promise; a decision with `reconsider` (WillContext) gives the running activity no inertia
33
+ * bonus, for a review the host forces because something changed (promise inertia, a cognition term, stays).
31
34
  */
32
35
  import type { Affordance, BodyReadout, Command, Considered, EntityId, Minute, Percept, Person, RefusalKind, Signed, StandingAdvice, Suggestion, SuggestionResolution, Unit, VoiceConflict, VoiceRelation, WillState } from '../types.ts';
33
36
  export declare const WILL_DEFAULTS: {
@@ -108,6 +111,14 @@ export declare const WILL_DEFAULTS: {
108
111
  maxCredited: number;
109
112
  /** Share of a tie's inertia bonus that remains when the activity is about to end. */
110
113
  inertiaFloor: number;
114
+ /**
115
+ * A voice's own promise (2.8.0, `noteVoicePromise`): a kept promise moves trust this share of the way toward 1, a
116
+ * broken one this share toward 0. Broken weighs more than kept (trust asymmetry, unaudited citations in research/empirical-models.md §6b: Slovic 1993; broken promises
117
+ * damage trust and kept ones restore it only gradually, Schweitzer, Hershey and Bradlow 2006: research/
118
+ * empirical-models.md §6b). Shape only; the numbers are engineering defaults equal to `trustGain`/`trustLoss`.
119
+ */
120
+ promiseKeptGain: number;
121
+ promiseBrokenLoss: number;
111
122
  /**
112
123
  * Insisting cannot override survival: at this desperation (same scale as conscience's necessity threshold),
113
124
  * or when the person's own choice is driven by a bodily need at `survivalNeedUrgency`, the verdict is
@@ -151,6 +162,8 @@ export interface WillContext {
151
162
  }[];
152
163
  /** A host command in force (1.6.0); see the command SCOPE on `commandOutcome`. */
153
164
  command?: Command;
165
+ /** No inertia bonus for the running activity in this decision (2.8.0, `DecideOptions.reconsider`). */
166
+ reconsider?: boolean;
154
167
  }
155
168
  /**
156
169
  * How a command fared in one decision. `holds`: the commanded option was chosen. Otherwise `ends` says whether
@@ -194,6 +207,32 @@ export declare function voiceOf(p: Person, voiceId: EntityId): VoiceRelation | u
194
207
  * records no history event and no credit, since no suggestion of that voice was weighed.
195
208
  */
196
209
  export declare function adoptVoiceTrust(p: Person, voiceId: EntityId, target: Unit, rate: Unit): VoiceRelation;
210
+ /**
211
+ * Give `p` an authored starting trust in a voice after creation (2.8.0), marked `seeded` as `PersonSpec.voices`
212
+ * would (a seeded voice is never evicted to make room). A no-op returning the existing relation when the voice is
213
+ * already known, unless `replace` is set; replacing keeps counters and history and marks the voice seeded. Records
214
+ * no history event: no suggestion was weighed. A non-finite trust falls back to `defaultVoiceTrust`.
215
+ */
216
+ export declare function seedVoice(p: Person, voiceId: EntityId, trust: Unit, opts?: {
217
+ replace?: boolean;
218
+ }): VoiceRelation;
219
+ /**
220
+ * SCOPE (a voice's own promise, 2.8.0): the voice said it would do something ("I will carry food up by 18:00") and
221
+ * the host reports whether it did. This is evidence about the voice's word, separate from how following its advice
222
+ * felt (`learnFromVoice`), so it needs no suggestion resolution and works for a request that was only deferred.
223
+ * Kept moves trust `promiseKeptGain` of the way toward 1, divided by 1 + n where n is the decayed count of kept
224
+ * promises already credited for the same action (key `promise:<action>` in `credited`), so a string of trivial
225
+ * promises cannot farm trust (these keys share `credited` and its `maxCredited` bound with advice credits, so a
226
+ * host with many promise actions can evict older entries of either kind, which only shortens a discount);
227
+ * broken moves it `promiseBrokenLoss` toward 0, undiscounted. History reasons are
228
+ * `promise-kept` and `promise-broken`. The voice is added if absent (bounded like any new voice). Does not model
229
+ * excuses, partial keeping, apology, or the size of what was promised; the host judges kept or broken.
230
+ */
231
+ export declare function noteVoicePromise(p: Person, voiceId: EntityId, event: {
232
+ kept: boolean;
233
+ at?: Minute;
234
+ action?: string;
235
+ }): VoiceRelation;
197
236
  /**
198
237
  * The next minute after `now` at which a pending commitment reaches `wakeCommitmentPressure`, the level at which a
199
238
  * 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
@@ -28,6 +28,9 @@
28
28
  * when the answer leaves the person doing what they are doing. A host command is a separate input that yields the
29
29
  * `commanded` verdict (see the commanded-control SCOPE on `commandOutcome`); `learnFromVoice` learns from assented,
30
30
  * complied and commanded activities, and `adoptVoiceTrust` moves trust toward a target without a suggestion.
31
+ * 2.8.0: `seedVoice` sets an authored starting trust after creation; `noteVoicePromise` learns from whether the
32
+ * voice kept its own promise; a decision with `reconsider` (WillContext) gives the running activity no inertia
33
+ * bonus, for a review the host forces because something changed (promise inertia, a cognition term, stays).
31
34
  */
32
35
  import { breakAllows, inBreak } from '../affect/index.js';
33
36
  import { commitmentPressure, isUnderWay, pressureReachedAt } from '../agenda/index.js';
@@ -114,6 +117,14 @@ export const WILL_DEFAULTS = {
114
117
  maxCredited: 8,
115
118
  /** Share of a tie's inertia bonus that remains when the activity is about to end. */
116
119
  inertiaFloor: 0.5,
120
+ /**
121
+ * A voice's own promise (2.8.0, `noteVoicePromise`): a kept promise moves trust this share of the way toward 1, a
122
+ * broken one this share toward 0. Broken weighs more than kept (trust asymmetry, unaudited citations in research/empirical-models.md §6b: Slovic 1993; broken promises
123
+ * damage trust and kept ones restore it only gradually, Schweitzer, Hershey and Bradlow 2006: research/
124
+ * empirical-models.md §6b). Shape only; the numbers are engineering defaults equal to `trustGain`/`trustLoss`.
125
+ */
126
+ promiseKeptGain: 0.1,
127
+ promiseBrokenLoss: 0.25,
117
128
  /**
118
129
  * Insisting cannot override survival: at this desperation (same scale as conscience's necessity threshold),
119
130
  * or when the person's own choice is driven by a bodily need at `survivalNeedUrgency`, the verdict is
@@ -224,6 +235,61 @@ export function adoptVoiceTrust(p, voiceId, target, rate) {
224
235
  v.trust = clamp01(v.trust + r * (t - v.trust));
225
236
  return v;
226
237
  }
238
+ /**
239
+ * Give `p` an authored starting trust in a voice after creation (2.8.0), marked `seeded` as `PersonSpec.voices`
240
+ * would (a seeded voice is never evicted to make room). A no-op returning the existing relation when the voice is
241
+ * already known, unless `replace` is set; replacing keeps counters and history and marks the voice seeded. Records
242
+ * no history event: no suggestion was weighed. A non-finite trust falls back to `defaultVoiceTrust`.
243
+ */
244
+ export function seedVoice(p, voiceId, trust, opts = {}) {
245
+ const existing = voiceOf(p, voiceId);
246
+ if (existing && !opts.replace)
247
+ return existing;
248
+ const v = existing ?? ensureVoice(p, voiceId);
249
+ v.trust = clamp01(Number.isFinite(trust) ? trust : WILL_DEFAULTS.defaultVoiceTrust);
250
+ v.seeded = true;
251
+ return v;
252
+ }
253
+ /**
254
+ * SCOPE (a voice's own promise, 2.8.0): the voice said it would do something ("I will carry food up by 18:00") and
255
+ * the host reports whether it did. This is evidence about the voice's word, separate from how following its advice
256
+ * felt (`learnFromVoice`), so it needs no suggestion resolution and works for a request that was only deferred.
257
+ * Kept moves trust `promiseKeptGain` of the way toward 1, divided by 1 + n where n is the decayed count of kept
258
+ * promises already credited for the same action (key `promise:<action>` in `credited`), so a string of trivial
259
+ * promises cannot farm trust (these keys share `credited` and its `maxCredited` bound with advice credits, so a
260
+ * host with many promise actions can evict older entries of either kind, which only shortens a discount);
261
+ * broken moves it `promiseBrokenLoss` toward 0, undiscounted. History reasons are
262
+ * `promise-kept` and `promise-broken`. The voice is added if absent (bounded like any new voice). Does not model
263
+ * excuses, partial keeping, apology, or the size of what was promised; the host judges kept or broken.
264
+ */
265
+ export function noteVoicePromise(p, voiceId, event) {
266
+ const W = WILL_DEFAULTS;
267
+ const v = ensureVoice(p, voiceId);
268
+ const at = event.at !== undefined && Number.isFinite(event.at) ? event.at : p.now;
269
+ const before = v.trust;
270
+ if (event.kept) {
271
+ const key = `promise:${event.action ?? ''}`;
272
+ v.credited ??= [];
273
+ const list = v.credited;
274
+ const prior = list.find((c) => c.action === key);
275
+ const n = prior ? decay(prior.n, Math.max(0, at - prior.at), W.creditHalfLife) : 0;
276
+ v.trust = clamp01(v.trust + (W.promiseKeptGain * (1 - v.trust)) / (1 + n));
277
+ if (prior) {
278
+ prior.n = n + 1;
279
+ prior.at = at;
280
+ }
281
+ else
282
+ list.push({ action: key, n: 1, at });
283
+ if (list.length > W.maxCredited) {
284
+ list.sort((a, b) => b.at - a.at);
285
+ list.length = W.maxCredited;
286
+ }
287
+ }
288
+ else
289
+ v.trust = clamp01(v.trust - W.promiseBrokenLoss * v.trust);
290
+ noteTrust(v, v.trust - before, event.kept ? 'promise-kept' : 'promise-broken', at, event.action);
291
+ return v;
292
+ }
227
293
  const finite = (x) => x === undefined || Number.isFinite(x);
228
294
  /** Host numbers the framework computes with must be finite; a NaN offer is vetoed, never chosen. */
229
295
  function wellFormed(aff) {
@@ -547,7 +613,7 @@ function evaluate(p, input, ctx, given, draw) {
547
613
  const live = considered.filter((c) => !c.vetoed);
548
614
  const current = p.activity;
549
615
  const score = (c) => {
550
- if (!current || c.affordanceId !== current.affordanceId)
616
+ if (!current || c.affordanceId !== current.affordanceId || ctx.reconsider)
551
617
  return safeUtility(c.utility);
552
618
  const span = Math.max(1, current.endsAt - current.startedAt);
553
619
  const remaining = clamp01((current.endsAt - ctx.now) / span);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adam0white/human-framework",
3
- "version": "2.7.0",
3
+ "version": "2.8.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",