@adam0white/human-framework 2.2.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -123,6 +123,48 @@ The framework has no world of its own. The host supplies three things and receiv
123
123
  - **Decisions** (`DecisionRecord`) come back: the chosen offer, the top `considered` options with every
124
124
  utility term, the suggestion verdict(s), a private `intention`, and a `narration`.
125
125
 
126
+ ### Expected benefits and observed outcomes
127
+
128
+ An offer describes expected effects. An outcome reports what happened. The host resolves space,
129
+ contact, inventory and payment; HF uses the reported outcome to update the person. Do not copy
130
+ `Affordance.advertises` into `Outcome.needs` as a general completion rule.
131
+
132
+ | Host situation | Offer | Completion report |
133
+ |---|---|---|
134
+ | A person sleeps for part of the planned night. | Advertise typical sleep/rest benefits and set `mode: 'sleep'`. | Leave `sleep` and `rest` absent from `Outcome.needs`. `finish` advances the body to the actual end time and measures their change since the activity began. Explicit values replace that measurement; copying the advertisement can overstate the realized recovery reported to learning. |
135
+ | A task promises four coins but actually pays two. | Set expected `Affordance.material` to four host units. | Update the host ledger by two and report `Outcome.material: 2`. A completed task does not imply the promised payment arrived. Report zero for no payment and a negative value for an actual cost. HF does not maintain the host's money ledger. |
136
+ | A person can talk with someone who is present. | Tag the offer `social` when it really involves social contact; put participants in `with`. | Resolve actual participation. An empty room, a sleeping neighbour or someone who arrives after the conversation ends is not a completed encounter. Do not award contact or belonging merely because the activity uses a shared place. |
137
+
138
+ For in-person activities, the host must establish co-presence and overlapping participation. HF has
139
+ no map and does not discover companions. `with` names people taking part, not everyone assigned to a
140
+ place. A `social` tag is also a claim about the offered activity, not a request to find someone. If
141
+ contact becomes impossible, interrupt or replan the activity instead of completing the declared
142
+ encounter. Report only the need effects that the actual interaction supports.
143
+
144
+ The current behavior is defined in [`Affordance` and `Outcome`](src/types.ts) and
145
+ [`finish`](src/person.ts). Existing controls cover [body advancement and waking after sleep](test/person.test.ts),
146
+ [body recovery](test/body.test.ts), [material valuation under scarcity](test/cognition-scarcity.test.ts),
147
+ [actual material in day records](test/chronicle.test.ts), and [relationship-aware social terms](test/social.test.ts).
148
+ These boundaries require correct host reports; they do not imply a framework behavior change.
149
+
150
+ ### Scheduled activity reviews
151
+
152
+ A custom host must process `activity.reviewAt`, including during sleep. `tick` advances the person;
153
+ it does not dispatch a decision or automatically end the host's job. An `activity.interrupt` is also
154
+ a request to review, not an instruction to cancel every activity.
155
+
156
+ At a due review, call `decide` with physically feasible alternatives. It includes the current activity
157
+ with its remaining duration. If that activity is chosen again, call `reviewed(p)` and keep its original
158
+ job, arrival time and reservations. If another activity wins, reconcile the old physical job, call
159
+ `finish` with an interrupted outcome, then `begin` the chosen activity with its decision record.
160
+ Do not restart a continued activity or refund its resources. Hard capacity vetoes still apply.
161
+
162
+ Sleep reviews can move earlier for ordinary pending appointments. The appointment's time pressure
163
+ can permit waking; an obligatory norm is not required. A host that ignores the scheduled review can
164
+ make someone miss an obligation HF was ready to reconsider. This does not imply advance planning
165
+ for unavailable future actions. See [`reviewed` and the decision lifecycle](src/person.ts) and
166
+ [`wakeReviewAt` and commitment-based waking](src/will/will.ts).
167
+
126
168
  You can drive a person by hand: `decide(p, offers, { suggestion })` → `begin(p, offer, record)` →
127
169
  `tick(p, now)` → `finish(p, outcome)`, with `perceive(p, percepts)` and `interrupt(p, now, reason)` between.
128
170
  Or implement `World` and let `stepCommunity` sequence everyone in deterministic (time, id) order:
@@ -302,3 +344,60 @@ clearMethodContext(learner); // when leaving or switching the task
302
344
  ```
303
345
 
304
346
  Import these functions from the package. Rules/evidence live in optional `Person.methods` and survive snapshots. Current cues expire after 120 minutes. A stable demonstration id avoids counting the same example twice within the retained 256-example history, even after its rule or evidence is evicted. Once an id has left both that history and retained rule evidence, it can be admitted again. Missing observations cannot satisfy a condition. Failed demonstrations reduce support for that action. Successful alternatives retain their own support; ordinary cognition weighs the supported offers alongside other motives. A successful lecture does not certify reliable execution. [The routing control](examples/method-control.ts) demonstrates changed teaching, withheld teaching, a scalar-skill control and acquired-state ablation with two methods and an exception. [The alternatives control](examples/method-alternatives-control.ts) demonstrates two independently successful procedures under the same observations, failed practice, hunger and exact restore.
347
+
348
+
349
+ ## Receiving a possible future route
350
+
351
+ The pending 2.3.0 increment lets an attended explanation connect one current action to one possible successor. The host supplies the explanation the person received, not a private success bonus.
352
+
353
+ ```ts
354
+ import {
355
+ type Affordance, type ReceivedConsequence,
356
+ consequenceEvidence, createPerson, decide, perceive,
357
+ } from '@adam0white/human-framework';
358
+
359
+ const person = createPerson({
360
+ id: 'courier', name: 'Courier', seed: 19, now: 600,
361
+ bornAt: -30 * 365 * 1440, sex: 'female',
362
+ goals: [{
363
+ id: 'delivery', label: 'Deliver the parcel', serves: ['achievement'],
364
+ advancedBy: [{ action: 'deliver', amount: 1 }], importance: 0.8,
365
+ temporal: { halfLifeMinutes: 30 },
366
+ }],
367
+ });
368
+ const route: ReceivedConsequence = {
369
+ id: 'access-window', revision: 0,
370
+ trigger: { action: 'ask-access', targetId: 'keeper' },
371
+ successor: { action: 'deliver', duration: 20, effort: 0.1 },
372
+ availableFrom: 620, availableUntil: 680, expiresAt: 800,
373
+ chance: 0.8, confidence: 0.9,
374
+ };
375
+ perceive(person, [{
376
+ at: 600, channel: 'told', kind: 'explanation', actorId: 'keeper',
377
+ targetId: person.id, salience: 1,
378
+ summary: 'If your request succeeds, this route may become available.',
379
+ consequences: [route],
380
+ }]);
381
+ const request: Affordance = {
382
+ id: 'ask', action: 'ask-access', label: 'Request access', targetId: 'keeper',
383
+ duration: 10, effort: 0.1, advertises: {},
384
+ };
385
+ const offers: Affordance[] = [request,
386
+ { id: 'direct', action: 'deliver', label: 'Deliver directly',
387
+ duration: 180, effort: 0.1, advertises: {} },
388
+ ];
389
+ const decision = decide(person, offers);
390
+ const evidence = consequenceEvidence(person, request); // pure; no RNG consumed
391
+ ```
392
+
393
+ `consequences` and `withdrawConsequences: [{ id, revision }]` are accepted only on attended `told` percepts whose explicit `actorId` names someone else. `Percept.targetId` may identify this person or be omitted for a broadcast; testimony explicitly addressed to another recipient is rejected. A recipient-specific opportunity must be addressed to that recipient. Trigger action, target and place match exactly; an omitted target/place matches only an omitted value. `availableUntil` is the latest successor **completion**, inclusive. The whole successor must fit after the current action and `availableFrom`.
394
+
395
+ `Goal.temporal.halfLifeMinutes` must be finite and positive. It enables expected completion valuation for direct actions **as well as** received successors: existing goal value × estimated action success × time discount. Existing `deadline` remains urgency, never a hard cutoff. Keep the same temporal goal in knowledge-intact, withheld and removed-knowledge controls; compare temporal-off separately. Legacy goals retain their prior formula. A received successor serves only goals listing its action in `Goal.advancedBy`; it does not inherit an `Affordance.advances` hint from a current offer.
396
+
397
+ The received access estimate is current-action success × `chance` × `confidence` × current source trust. `chance` means access conditional on successful asking; it does not establish consent. One coherent route may contribute to goals. Expected successor effort and material costs use the same readers as direct actions, are charged once, and remain undiscounted and unscaled by maturity. A favorable explanation need not win against other motives.
398
+
399
+ Only actual terminal outcomes advance terminal goals. Finishing one's own request consumes matching received opportunities even if a refusal has not been heard. It grants no access and executes no successor. The host still decides physical availability, reports the other person's actual response, and filters repeated or impossible requests. Do not mark asking as `advances` merely to make the route attractive.
400
+
401
+ Knowledge lives in optional `Person.prospection` and survives snapshot/restore. A percept admits at most four additions/withdrawals combined, atomically. Invalid entries or duplicate source/id identities reject the entire batch. Equal/lower revisions do not reinforce or refresh; a higher revision replaces. There are at most 16 entries and 64 revision records. Consumed entries are evicted first, then the oldest remaining entries, while their revision records remain; a batch requiring more still-valid revision records is rejected. Entries and revision memory expire within seven days. Deduplication is bounded, not a durable audit. Capacity is shared across sources and admission does not allocate by trust. One prolific source can occupy the finite entry/revision capacity, causing other new ids to be rejected until revision space expires; this is bounded memory, not a source-fairness or trust firewall. Hosts must bound the premises they deliver. The pure query exposes retained entries, present feasibility and retained revisions.
402
+
403
+ [The frozen control](examples/received-consequences.ts) publishes useful, late, costly, uncertain, withheld, removed-knowledge, removed-goal, temporal-off and refused arms in two renamed domains. This faculty does not search action graphs, predict future body state, price all waiting opportunities or guarantee consent or success. Its numerical bounds and control parameters are engineering assumptions.
@@ -48,6 +48,7 @@
48
48
  // The bundled catalog is only the default; a host with its own catalog passes it (`World.catalog`).
49
49
  import { DEFAULT_NORMS } from '../conscience/catalog.js';
50
50
  import { clamp01, dayOf, dpow, hasNumbers, isObj } from '../core/index.js';
51
+ import { expectedEffect } from '../memory/index.js';
51
52
  import { MINUTES_PER_DAY } from '../types.js';
52
53
  export const AGENDA_DEFAULTS = {
53
54
  /** Minutes before a window opens during which a commitment already pulls (to get there). */
@@ -141,8 +142,23 @@ function copyArrays(c) {
141
142
  * Spec commitments keep a given id; entries without one get a free `c<n>` id after all given ids are placed, so
142
143
  * `prayerWindows()` output can be passed straight into `PersonSpec.commitments`.
143
144
  */
145
+ /** Copy valid temporal settings and drop invalid ones at every agenda entry point. */
146
+ function sanitizeGoalTemporal(g) {
147
+ const t = g.temporal;
148
+ if (t !== undefined &&
149
+ !(isObj(t) &&
150
+ typeof t.halfLifeMinutes === 'number' &&
151
+ Number.isFinite(t.halfLifeMinutes) &&
152
+ t.halfLifeMinutes > 0))
153
+ delete g.temporal;
154
+ else if (g.temporal)
155
+ g.temporal = { halfLifeMinutes: g.temporal.halfLifeMinutes };
156
+ }
144
157
  /** Restore-time check of `agenda.lapse` (1.7.0), in place: a malformed one is dropped (absent means none). @internal */
145
158
  export function sanitizeAgenda(a) {
159
+ for (const g of a.goals)
160
+ if (isObj(g))
161
+ sanitizeGoalTemporal(g);
146
162
  const lapse = a.lapse;
147
163
  if (lapse !== undefined && !(isObj(lapse) && hasNumbers(lapse, 'since', 'missed')))
148
164
  delete a.lapse;
@@ -177,6 +193,7 @@ export function createAgenda(spec, now) {
177
193
  lastAdvancedAt: now,
178
194
  });
179
195
  }
196
+ sanitizeAgenda(state);
180
197
  return state;
181
198
  }
182
199
  export function promise(p, c) {
@@ -471,7 +488,27 @@ function deadlinePressure(g, now) {
471
488
  const span = Math.max(1, g.deadline - g.adoptedAt);
472
489
  return 1 + AGENDA_DEFAULTS.goalDeadlineBoost * clamp01((now - g.adoptedAt) / span);
473
490
  }
491
+ /** Shared existing amount and urgency reader; no future knowledge is consulted here. @internal */
492
+ export function goalValue(g, aff, now) {
493
+ const amount = goalAmount(g, aff);
494
+ return amount > 0
495
+ ? g.importance * clamp01(amount * AGENDA_DEFAULTS.goalAmountGain) * deadlinePressure(g, now)
496
+ : 0;
497
+ }
498
+ /** Opt-in completion-time and success estimate, shared by direct and received routes. @internal */
499
+ export function temporalGoalValue(p, g, aff, now, completion) {
500
+ const base = goalValue(g, aff, now);
501
+ return g.temporal
502
+ ? base *
503
+ expectedEffect(p, aff).successRate *
504
+ dpow(0.5, Math.max(0, completion - now) / g.temporal.halfLifeMinutes)
505
+ : base;
506
+ }
474
507
  export function agendaTerms(p, aff, now) {
508
+ return agendaTermsWithGoalEstimates(p, aff, now);
509
+ }
510
+ /** Internal route integration; hosts cannot provide goal overrides through the public API. @internal */
511
+ export function agendaTermsWithGoalEstimates(p, aff, now, goalOverrides) {
475
512
  const d = AGENDA_DEFAULTS;
476
513
  const terms = [];
477
514
  for (const c of p.agenda.commitments) {
@@ -506,6 +543,13 @@ export function agendaTerms(p, aff, now) {
506
543
  for (const g of p.agenda.goals) {
507
544
  if (g.status !== 'active')
508
545
  continue;
546
+ const override = goalOverrides?.get(g.id);
547
+ if (override !== undefined || g.temporal) {
548
+ const value = override ?? temporalGoalValue(p, g, aff, now, now + Math.max(0, aff.duration));
549
+ if (value > 0)
550
+ terms.push({ source: `goal:${g.id}`, value });
551
+ continue;
552
+ }
509
553
  const amount = goalAmount(g, aff);
510
554
  if (amount <= 0)
511
555
  continue;
@@ -632,6 +676,7 @@ export function adoptGoal(p, g, now) {
632
676
  adoptedAt: now,
633
677
  lastAdvancedAt: now,
634
678
  };
679
+ sanitizeGoalTemporal(goal);
635
680
  p.agenda.goals.push(goal);
636
681
  return goal;
637
682
  }
@@ -25,7 +25,7 @@
25
25
  * `safety` need: `security` is a value, not a need, and no need is added.
26
26
  */
27
27
  import { emotionLevel, inBreak } from '../affect/index.js';
28
- import { agendaTerms } from '../agenda/index.js';
28
+ import { agendaTermsWithGoalEstimates } from '../agenda/index.js';
29
29
  import { trustOf } from '../beliefs/index.js';
30
30
  import { normTerms } from '../conscience/index.js';
31
31
  import { clamp01, cmpStr, dexp, minuteOfDay, round } from '../core/index.js';
@@ -34,6 +34,7 @@ import { expectedEffect } from '../memory/index.js';
34
34
  import { methodEvidence } from '../methods/index.js';
35
35
  import { urgencyOf } from '../needs/index.js';
36
36
  import { partneringTerms } from '../partnering/index.js';
37
+ import { receivedGoalRoute } from '../prospection/index.js';
37
38
  import { companionSteadiness, socialTerms } from '../social/index.js';
38
39
  import { PHYSIOLOGICAL_NEEDS, VALUE_KEYS } from '../types.js';
39
40
  import { adviceWeight, resolveChoice, safeUtility, standingAdvice, suggestionTargets, voicesIn, } from '../will/index.js';
@@ -200,6 +201,29 @@ export function socialContext(ctx) {
200
201
  satiation: K.socialSatiationFloor + (1 - K.socialSatiationFloor) * (1 - clamp01(belongingLevel)),
201
202
  };
202
203
  }
204
+ /** Same perceived-body effort estimate for actual offers and received successors. @internal */
205
+ export function effortValue(p, aff, ctx) {
206
+ const K = COGNITION_DEFAULTS;
207
+ const per = ctx.body.perceived;
208
+ const effort = clamp01(aff.effort);
209
+ const focus = clamp01(aff.focus ?? 0);
210
+ // A cued habit eases initiation and attention (N4: the depleted fall back on habits), not physical exertion.
211
+ const ease = p.habits.length === 0
212
+ ? 1
213
+ : habitEase(p, aff, { ...ctx.habit, placeId: aff.placeId ?? ctx.habit.placeId });
214
+ const effortCost = (effort * K.effortBase + focus * per.sleepiness) * ease +
215
+ effort * per.fatigue * (1 - clamp01(p.body.fitness));
216
+ return -K.effortScale * effortCost;
217
+ }
218
+ /** Same material valuation for actual offers and received successor costs. @internal */
219
+ export function materialValue(p, material, scarcity) {
220
+ const K = COGNITION_DEFAULTS;
221
+ const v = p.values;
222
+ const weight = (v.security + v.achievement + v.power) / 3;
223
+ const scale = K.materialScale * (1 - K.scarcityScaleShrink * scarcity);
224
+ const sat = Math.sign(material) * (1 - dexp(-Math.abs(material) / scale));
225
+ return K.materialWeight * sat * 2 * weight * (1 + K.scarcityMaterialGain * scarcity);
226
+ }
203
227
  /** Score one option. Pure apart from reads; terms are rounded for stable snapshots. */
204
228
  export function consider(p, aff, ctx) {
205
229
  const K = COGNITION_DEFAULTS;
@@ -264,8 +288,13 @@ export function consider(p, aff, ctx) {
264
288
  }
265
289
  }
266
290
  const maturity = clamp01(ctx.mods.maturity);
267
- for (const t of agendaTerms(p, aff, ctx.now))
268
- push(t.source, t.value * (0.5 + 0.5 * maturity));
291
+ const maturityScale = 0.5 + 0.5 * maturity;
292
+ const received = receivedGoalRoute(p, aff, ctx.now, maturityScale, (successor) => effortValue(p, successor, ctx) +
293
+ (successor.material ? materialValue(p, successor.material, scarcity) : 0));
294
+ for (const t of agendaTermsWithGoalEstimates(p, aff, ctx.now, received?.goals))
295
+ push(t.source, t.value * maturityScale);
296
+ if (received)
297
+ push(received.cost.source, received.cost.value);
269
298
  // Habit: cue-matched automaticity (cued by the option's own place), stronger before planning matures.
270
299
  const habit = p.habits.length === 0
271
300
  ? 0
@@ -316,17 +345,9 @@ export function consider(p, aff, ctx) {
316
345
  }
317
346
  }
318
347
  // Effort and mental load, read from the perceived body.
319
- const per = ctx.body.perceived;
320
- const effort = clamp01(aff.effort);
321
- const focus = clamp01(aff.focus ?? 0);
322
- // A cued habit eases initiation and attention (N4: the depleted fall back on habits), not physical exertion.
323
- const ease = p.habits.length === 0
324
- ? 1
325
- : habitEase(p, aff, { ...ctx.habit, placeId: aff.placeId ?? ctx.habit.placeId });
326
- const effortCost = (effort * K.effortBase + focus * per.sleepiness) * ease +
327
- effort * per.fatigue * (1 - clamp01(p.body.fitness));
328
- if (effortCost > 0)
329
- push('effort', -K.effortScale * effortCost);
348
+ const effort = effortValue(p, aff, ctx);
349
+ if (effort < 0)
350
+ push('effort', effort);
330
351
  // Risk: chance × severity × (fear + emotionality).
331
352
  if (aff.risk && aff.risk.chance > 0 && aff.risk.severity > 0) {
332
353
  const fear = emotionLevel(p, 'fear');
@@ -340,13 +361,8 @@ export function consider(p, aff, ctx) {
340
361
  }
341
362
  }
342
363
  // Material: saturating value of host units, scaled by security/achievement/power.
343
- if (aff.material) {
344
- const v = p.values;
345
- const weight = (v.security + v.achievement + v.power) / 3;
346
- const scale = K.materialScale * (1 - K.scarcityScaleShrink * scarcity);
347
- const sat = Math.sign(aff.material) * (1 - dexp(-Math.abs(aff.material) / scale));
348
- push('material', K.materialWeight * sat * 2 * weight * (1 + K.scarcityMaterialGain * scarcity));
349
- }
364
+ if (aff.material)
365
+ push('material', materialValue(p, aff.material, scarcity));
350
366
  // Precommitments inside their daily window.
351
367
  const mod = minuteOfDay(ctx.now);
352
368
  for (const pc of p.will.precommitments) {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Human Framework v1 public surface. */
2
- export declare const FRAMEWORK_VERSION = "2.2.1";
2
+ export declare const FRAMEWORK_VERSION = "2.3.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';
@@ -28,6 +28,8 @@ export * from './narrate/index.ts';
28
28
  export * from './needs/index.ts';
29
29
  export * from './partnering/index.ts';
30
30
  export * from './person.ts';
31
+ export type { ConsequenceEvidence } from './prospection/index.ts';
32
+ export { consequenceEvidence, PROSPECTION_DEFAULTS } from './prospection/index.ts';
31
33
  export * from './restore.ts';
32
34
  export * from './scenarios/index.ts';
33
35
  export * from './sim/index.ts';
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Human Framework v1 public surface. */
2
- export const FRAMEWORK_VERSION = '2.2.1';
2
+ export const FRAMEWORK_VERSION = '2.3.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';
@@ -25,6 +25,7 @@ export * from './narrate/index.js';
25
25
  export * from './needs/index.js';
26
26
  export * from './partnering/index.js';
27
27
  export * from './person.js';
28
+ export { consequenceEvidence, PROSPECTION_DEFAULTS } from './prospection/index.js';
28
29
  export * from './restore.js';
29
30
  export * from './scenarios/index.js';
30
31
  export * from './sim/index.js';
package/dist/migrate.js CHANGED
@@ -8,7 +8,7 @@
8
8
  * field of the optional surroundings slice (`ambient.now` → `ambient.percept`) and changes no behaviour (the optional `social.impressions` and `social.reserve`, like `retention`, are optional and start
9
9
  * absent in a migrated save); `restore` then fills and sanitizes
10
10
  * the slices as for any save. 2.0.0 to 2.1.0 adds optional conditional method state, absent
11
- * until explicitly used, so existing hosts retain their previous behavior; that step stamps the version. 2.1.0 to 2.2.0 preserves optional method records and stamps the version; successful alternatives now retain independent support. A migrated save continues under the current engine's rules: it restores and runs,
11
+ * until explicitly used, so existing hosts retain their previous behavior; that step stamps the version. 2.1.0 to 2.2.0 preserves optional method records and stamps the version; successful alternatives now retain independent support. 2.2.0 to 2.3.0 stamps the version; received consequences and temporal goals remain absent unless explicitly supplied. A migrated save continues under the current engine's rules: it restores and runs,
12
12
  * but it does not reproduce what the old engine would have done next where the rules changed (1.4.0 to 1.5.0
13
13
  * changed how standing advice is heard; 1.8.0 to 1.9.0 changed the omission rule). Saves older than 1.4.0 and unknown versions are refused with an error.
14
14
  *
@@ -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.2.0': {
62
+ to: '2.3.0',
63
+ note: 'received consequences and temporal goals are optional; existing hosts retain exact behavior',
64
+ apply: stamp,
65
+ },
61
66
  '2.1.0': {
62
67
  to: '2.2.0',
63
68
  note: 'person shape unchanged; method evidence now grades each action independently, preserving successful alternatives',
package/dist/person.js CHANGED
@@ -36,6 +36,7 @@ import { advanceMemory, createMemory, enableGists, foldGists, learnOutcome, reca
36
36
  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
+ import { consumeConsequences, pruneConsequences, receiveConsequences } from './prospection/index.js';
39
40
  import { enableSkillRetention, learnByWatching, practise, seedSkills, } from './skills/index.js';
40
41
  import { advanceSocial, applyReputationBelief, careFor, closeness, judge, markDeceased, relationshipWith, seedRelationships, socialEvent, threatAppraisal, } from './social/index.js';
41
42
  import { ENGINE_VERSION, MAX_MINUTE, PERSON_SCHEMA, PHYSIOLOGICAL_NEEDS, PSYCHOLOGICAL_NEEDS, TRAIT_KEYS, VALUE_KEYS, } from './types.js';
@@ -353,6 +354,7 @@ export function tick(p, now) {
353
354
  return;
354
355
  if (!p.body.alive) {
355
356
  p.now = now;
357
+ pruneConsequences(p);
356
358
  return;
357
359
  }
358
360
  const seg = PERSON_DEFAULTS.maxSegment;
@@ -366,13 +368,16 @@ export function tick(p, now) {
366
368
  // A minute so large that the grid step rounds away cannot advance (security review H2): stop, never spin.
367
369
  if (!(end > p.now)) {
368
370
  p.now = now;
371
+ pruneConsequences(p);
369
372
  return;
370
373
  }
371
374
  advanceSegment(p, end - p.now, load, active && act ? act.affordance : undefined);
372
375
  p.now = end;
376
+ pruneConsequences(p);
373
377
  if (!p.body.alive) {
374
378
  p.activity = null;
375
379
  p.now = now;
380
+ pruneConsequences(p);
376
381
  return;
377
382
  }
378
383
  // Discrete bookkeeping runs only on grid boundaries, so it happens at the same minutes whatever the
@@ -517,6 +522,7 @@ export function skip(p, to) {
517
522
  p.activity = null;
518
523
  skipBody(p, to);
519
524
  p.now = to;
525
+ pruneConsequences(p);
520
526
  skipAffect(p, to);
521
527
  advanceAgenda(p, to);
522
528
  // Lasting gists (1.8.0, opt-in): episodes now past the horizon fold into gists.
@@ -535,6 +541,7 @@ function socialKindOf(kind) {
535
541
  return SOCIAL_ALIASES[kind];
536
542
  }
537
543
  function perceiveOne(p, pc) {
544
+ receiveConsequences(p, pc);
538
545
  const at = Math.min(Math.max(pc.at, p.affect.lastUpdated), p.now);
539
546
  const actor = pc.actorId;
540
547
  const target = pc.targetId;
@@ -1191,6 +1198,7 @@ export function finish(p, outcome, opts = {}) {
1191
1198
  }
1192
1199
  const prev = lastAction(p);
1193
1200
  if (completed) {
1201
+ consumeConsequences(p, aff);
1194
1202
  const hctx = { now };
1195
1203
  if (aff.placeId !== undefined)
1196
1204
  hctx.placeId = aff.placeId;
@@ -0,0 +1 @@
1
+ export * from './prospection.ts';
@@ -0,0 +1 @@
1
+ export * from './prospection.js';
@@ -0,0 +1,23 @@
1
+ import type { Affordance, ConsequenceRevision, Minute, Person, RetainedConsequence } from '../types.ts';
2
+ export declare const PROSPECTION_DEFAULTS: {
3
+ maxEntries: number;
4
+ maxRevisions: number;
5
+ maxBatch: number;
6
+ maxIdentifierLength: number;
7
+ horizon: number;
8
+ maxMaterialCost: number;
9
+ };
10
+ export interface ConsequenceEvidence {
11
+ entry: RetainedConsequence;
12
+ matches: boolean;
13
+ feasible: boolean;
14
+ successorStartsAt: Minute;
15
+ successorEndsAt: Minute;
16
+ accessProbability: number;
17
+ terminalProbability: number;
18
+ }
19
+ /** Read current retained knowledge and feasibility. Does not mutate, prune or consume RNG. */
20
+ export declare function consequenceEvidence(p: Person, aff: Affordance, now?: Minute): {
21
+ entries: ConsequenceEvidence[];
22
+ revisions: ConsequenceRevision[];
23
+ };
@@ -0,0 +1,278 @@
1
+ /**
2
+ * SCOPE: one received current-action → successor consequence, acquired only through
3
+ * attended external testimony for this person or an untargeted broadcast. Knowledge is person-owned, source-labelled, bounded and saved.
4
+ * Temporal goals value expected completion using time, success and source reliability.
5
+ * One coherent route may contribute; it never executes actions, grants consent or
6
+ * advances a goal. Own completed triggers consume their received opportunity.
7
+ * This is not graph search, inferred procedure learning or a model of future bodies,
8
+ * waiting opportunity cost or another person's agency. Parameters are engineering
9
+ * bounds, not fitted psychological estimates. Revision memory is bounded, not an audit.
10
+ */
11
+ import { temporalGoalValue } from '../agenda/agenda.js';
12
+ import { trustOf } from '../beliefs/index.js';
13
+ import { cmpStr, isNum, isObj, round } from '../core/index.js';
14
+ import { expectedEffect } from '../memory/index.js';
15
+ import { MAX_MINUTE, MINUTES_PER_DAY } from '../types.js';
16
+ export const PROSPECTION_DEFAULTS = {
17
+ maxEntries: 16,
18
+ maxRevisions: 64,
19
+ maxBatch: 4,
20
+ maxIdentifierLength: 96,
21
+ horizon: 7 * MINUTES_PER_DAY,
22
+ maxMaterialCost: 1_000_000,
23
+ };
24
+ const id = (x) => typeof x === 'string' &&
25
+ x.length > 0 &&
26
+ x.length <= PROSPECTION_DEFAULTS.maxIdentifierLength &&
27
+ x !== '__proto__' &&
28
+ x !== 'constructor' &&
29
+ x !== 'prototype';
30
+ const minute = (x) => isNum(x) && Math.abs(x) <= MAX_MINUTE;
31
+ const unit = (x) => isNum(x) && x >= 0 && x <= 1;
32
+ const revision = (x) => typeof x === 'number' && Number.isSafeInteger(x) && x >= 0;
33
+ const identity = (a, b) => a.sourceId === b.sourceId && a.id === b.id;
34
+ const order = (a, b) => a.receivedAt - b.receivedAt || cmpStr(a.sourceId, b.sourceId) || cmpStr(a.id, b.id);
35
+ function action(x) {
36
+ if (!isObj(x) ||
37
+ !id(x.action) ||
38
+ (x.targetId !== undefined && !id(x.targetId)) ||
39
+ (x.placeId !== undefined && !id(x.placeId)))
40
+ return undefined;
41
+ return {
42
+ action: x.action,
43
+ ...(x.targetId !== undefined ? { targetId: x.targetId } : {}),
44
+ ...(x.placeId !== undefined ? { placeId: x.placeId } : {}),
45
+ };
46
+ }
47
+ function consequence(x, receivedAt) {
48
+ if (!isObj(x) || !id(x.id) || !revision(x.revision))
49
+ return undefined;
50
+ const trigger = action(x.trigger);
51
+ const successorAction = action(x.successor);
52
+ const s = x.successor;
53
+ if (!trigger ||
54
+ !successorAction ||
55
+ !isObj(s) ||
56
+ !isNum(s.duration) ||
57
+ s.duration < 0 ||
58
+ s.duration > PROSPECTION_DEFAULTS.horizon ||
59
+ !unit(s.effort) ||
60
+ (s.focus !== undefined && !unit(s.focus)) ||
61
+ (s.materialCost !== undefined &&
62
+ (!isNum(s.materialCost) || s.materialCost < 0 || s.materialCost > PROSPECTION_DEFAULTS.maxMaterialCost)))
63
+ return undefined;
64
+ if (!minute(x.availableFrom) ||
65
+ !minute(x.availableUntil) ||
66
+ !minute(x.expiresAt) ||
67
+ x.availableFrom > x.availableUntil ||
68
+ x.availableUntil > x.expiresAt ||
69
+ x.expiresAt < receivedAt ||
70
+ x.expiresAt > receivedAt + PROSPECTION_DEFAULTS.horizon ||
71
+ !unit(x.chance) ||
72
+ !unit(x.confidence))
73
+ return undefined;
74
+ return {
75
+ id: x.id,
76
+ revision: x.revision,
77
+ trigger,
78
+ successor: {
79
+ ...successorAction,
80
+ duration: s.duration,
81
+ effort: s.effort,
82
+ ...(s.focus !== undefined ? { focus: s.focus } : {}),
83
+ ...(s.materialCost !== undefined ? { materialCost: s.materialCost } : {}),
84
+ },
85
+ availableFrom: x.availableFrom,
86
+ availableUntil: x.availableUntil,
87
+ expiresAt: x.expiresAt,
88
+ chance: x.chance,
89
+ confidence: x.confidence,
90
+ };
91
+ }
92
+ function retained(x, now) {
93
+ if (!isObj(x) || !id(x.sourceId) || x.channel !== 'told' || !minute(x.receivedAt) || x.receivedAt > now)
94
+ return undefined;
95
+ const c = consequence(x, x.receivedAt);
96
+ if (!c ||
97
+ (x.triggeredAt !== undefined &&
98
+ (!minute(x.triggeredAt) || x.triggeredAt < x.receivedAt || x.triggeredAt > now)))
99
+ return undefined;
100
+ return {
101
+ ...c,
102
+ sourceId: x.sourceId,
103
+ channel: 'told',
104
+ receivedAt: x.receivedAt,
105
+ ...(x.triggeredAt !== undefined ? { triggeredAt: x.triggeredAt } : {}),
106
+ };
107
+ }
108
+ function cleaned(state, now) {
109
+ const revisions = state.revisions.filter((r) => now - r.receivedAt <= PROSPECTION_DEFAULTS.horizon);
110
+ return {
111
+ entries: state.entries.filter((e) => e.expiresAt >= now && revisions.some((r) => identity(r, e) && r.revision === e.revision)),
112
+ revisions,
113
+ };
114
+ }
115
+ function store(p, s) {
116
+ if (s.entries.length || s.revisions.length)
117
+ p.prospection = s;
118
+ else
119
+ delete p.prospection;
120
+ }
121
+ /** Expiry changes only existing optional state. @internal */
122
+ export function pruneConsequences(p, now = p.now) {
123
+ if (p.prospection)
124
+ store(p, cleaned(p.prospection, now));
125
+ }
126
+ /** Called only for an attended percept. Invalid combined batches mutate no prospection state. @internal */
127
+ export function receiveConsequences(p, pc) {
128
+ if (pc.consequences === undefined && pc.withdrawConsequences === undefined)
129
+ return false;
130
+ if (pc.channel !== 'told' ||
131
+ !id(pc.actorId) ||
132
+ pc.actorId === p.id ||
133
+ (pc.targetId !== undefined && pc.targetId !== p.id))
134
+ return false;
135
+ const adds = pc.consequences ?? [];
136
+ const removes = pc.withdrawConsequences ?? [];
137
+ if (!Array.isArray(adds) ||
138
+ !Array.isArray(removes) ||
139
+ adds.length + removes.length > PROSPECTION_DEFAULTS.maxBatch ||
140
+ adds.length + removes.length === 0)
141
+ return false;
142
+ const additions = adds.map((x) => consequence(x, p.now));
143
+ if (additions.some((x) => x === undefined) ||
144
+ removes.some((x) => !isObj(x) || !id(x.id) || !revision(x.revision)))
145
+ return false;
146
+ const withdrawals = removes;
147
+ const batch = [...additions.map((x) => ({ id: x?.id, revision: x?.revision })), ...withdrawals];
148
+ if (new Set(batch.map((x) => x.id)).size !== batch.length)
149
+ return false;
150
+ const state = cleaned(structuredClone(p.prospection ?? { entries: [], revisions: [] }), p.now);
151
+ const sourceId = pc.actorId;
152
+ const updates = batch.filter((b) => !state.revisions.some((r) => r.sourceId === sourceId && r.id === b.id && r.revision >= (b.revision ?? 0)));
153
+ const newIds = updates.filter((b) => !state.revisions.some((r) => r.sourceId === sourceId && r.id === b.id));
154
+ // Never evict a still-valid tombstone to admit a batch; dedup holds within the stated horizon.
155
+ if (state.revisions.length + newIds.length > PROSPECTION_DEFAULTS.maxRevisions)
156
+ return false;
157
+ for (const b of updates) {
158
+ state.entries = state.entries.filter((e) => !(e.sourceId === sourceId && e.id === b.id));
159
+ state.revisions = state.revisions.filter((r) => !(r.sourceId === sourceId && r.id === b.id));
160
+ const addition = additions.find((a) => a?.id === b.id);
161
+ if (addition)
162
+ state.entries.push({ ...addition, sourceId, channel: 'told', receivedAt: p.now });
163
+ state.revisions.push({ sourceId, id: b.id, revision: b.revision, receivedAt: p.now });
164
+ }
165
+ if (state.entries.length > PROSPECTION_DEFAULTS.maxEntries) {
166
+ const oldest = [...state.entries]
167
+ .sort((a, b) => Number(b.triggeredAt !== undefined) - Number(a.triggeredAt !== undefined) || order(a, b))
168
+ .slice(0, state.entries.length - PROSPECTION_DEFAULTS.maxEntries);
169
+ state.entries = state.entries.filter((e) => !oldest.includes(e));
170
+ }
171
+ store(p, state);
172
+ return true;
173
+ }
174
+ const matches = (a, b) => a.action === b.action && a.targetId === b.targetId && a.placeId === b.placeId;
175
+ /** Own actual completed trigger consumes an opportunity, without predicting consent. @internal */
176
+ export function consumeConsequences(p, completed) {
177
+ for (const e of p.prospection?.entries ?? [])
178
+ if (e.triggeredAt === undefined && matches(e.trigger, completed))
179
+ e.triggeredAt = p.now;
180
+ }
181
+ /** Read current retained knowledge and feasibility. Does not mutate, prune or consume RNG. */
182
+ export function consequenceEvidence(p, aff, now = p.now) {
183
+ return {
184
+ entries: (p.prospection?.entries ?? []).map((entry) => {
185
+ const start = Math.max(now + Math.max(0, aff.duration), entry.availableFrom);
186
+ const end = start + entry.successor.duration;
187
+ const match = matches(entry.trigger, aff);
188
+ const feasible = match &&
189
+ entry.triggeredAt === undefined &&
190
+ now <= entry.expiresAt &&
191
+ end <= entry.availableUntil &&
192
+ Number.isFinite(end);
193
+ const accessProbability = expectedEffect(p, aff).successRate * entry.chance * entry.confidence * trustOf(p, entry.sourceId);
194
+ return {
195
+ entry: structuredClone(entry),
196
+ matches: match,
197
+ feasible,
198
+ successorStartsAt: start,
199
+ successorEndsAt: end,
200
+ accessProbability,
201
+ terminalProbability: accessProbability * expectedEffect(p, successorOffer(entry)).successRate,
202
+ };
203
+ }),
204
+ revisions: structuredClone((p.prospection?.revisions ?? []).filter((r) => now - r.receivedAt <= PROSPECTION_DEFAULTS.horizon)),
205
+ };
206
+ }
207
+ function successorOffer(e) {
208
+ const { materialCost, ...rest } = e.successor;
209
+ return {
210
+ ...rest,
211
+ id: 'prospective-successor',
212
+ label: 'Received successor',
213
+ advertises: {},
214
+ ...(materialCost !== undefined ? { material: -materialCost } : {}),
215
+ };
216
+ }
217
+ /** Select a whole route in the exact rounded units cognition emits. @internal */
218
+ export function receivedGoalRoute(p, aff, now, maturityScale, costs) {
219
+ const goals = p.agenda.goals.filter((g) => g.status === 'active' && g.temporal !== undefined);
220
+ if (!goals.length || !p.prospection?.entries.length)
221
+ return undefined;
222
+ const direct = goals.map((g) => temporalGoalValue(p, g, aff, now, now + Math.max(0, aff.duration)));
223
+ const baseline = direct.reduce((sum, value) => sum + round(value * maturityScale), 0);
224
+ let bestScore = baseline;
225
+ let best;
226
+ const entries = consequenceEvidence(p, aff, now).entries.sort((a, b) => cmpStr(a.entry.sourceId, b.entry.sourceId) || cmpStr(a.entry.id, b.entry.id));
227
+ for (const e of entries) {
228
+ if (!e.feasible)
229
+ continue;
230
+ const successor = successorOffer(e.entry);
231
+ const values = goals.map((g, i) => Math.max(direct[i] ?? 0, e.accessProbability * temporalGoalValue(p, g, successor, now, e.successorEndsAt)));
232
+ const cost = round(e.accessProbability * Math.min(0, costs(successor)));
233
+ const total = values.reduce((sum, value) => sum + round(value * maturityScale), 0) + cost;
234
+ if (!(round(total - bestScore) > 0))
235
+ continue;
236
+ bestScore = total;
237
+ best = {
238
+ goals: new Map(goals.map((g, i) => [g.id, values[i] ?? 0])),
239
+ cost: { source: `prospection:cost:${e.entry.sourceId}:${e.entry.id}`, value: cost },
240
+ };
241
+ }
242
+ return best;
243
+ }
244
+ /** Conservative optional-slice restore; ambiguous identities drop the slice. @internal */
245
+ export function sanitizeProspection(input, now, personId) {
246
+ if (!isObj(input) ||
247
+ !Array.isArray(input.entries) ||
248
+ !Array.isArray(input.revisions) ||
249
+ input.entries.length > PROSPECTION_DEFAULTS.maxEntries ||
250
+ input.revisions.length > PROSPECTION_DEFAULTS.maxRevisions)
251
+ return undefined;
252
+ const entries = input.entries.map((x) => retained(x, now)).filter((e) => e?.sourceId !== personId);
253
+ const revisions = [];
254
+ for (const r of input.revisions) {
255
+ if (!isObj(r) ||
256
+ !id(r.sourceId) ||
257
+ r.sourceId === personId ||
258
+ !id(r.id) ||
259
+ !revision(r.revision) ||
260
+ !minute(r.receivedAt) ||
261
+ r.receivedAt > now)
262
+ continue;
263
+ if (revisions.some((old) => identity(old, r)))
264
+ return undefined;
265
+ revisions.push({ sourceId: r.sourceId, id: r.id, revision: r.revision, receivedAt: r.receivedAt });
266
+ }
267
+ const valid = [];
268
+ for (const e of entries) {
269
+ if (!e)
270
+ continue;
271
+ if (valid.some((old) => identity(old, e)))
272
+ return undefined;
273
+ if (revisions.some((r) => identity(r, e) && r.revision === e.revision && r.receivedAt === e.receivedAt))
274
+ valid.push(e);
275
+ }
276
+ const state = cleaned({ entries: valid, revisions }, now);
277
+ return state.entries.length || state.revisions.length ? state : undefined;
278
+ }
package/dist/restore.js CHANGED
@@ -20,6 +20,7 @@ import { sanitizeMethods } from './methods/index.js';
20
20
  import { migrate } from './migrate.js';
21
21
  import { sanitizeBonds } from './partnering/index.js';
22
22
  import { cleanRetention, createPerson, traceLimit } from './person.js';
23
+ import { sanitizeProspection } from './prospection/index.js';
23
24
  import { sanitizeSkillRetention } from './skills/index.js';
24
25
  import { sanitizeGroups, sanitizeImpressions } from './social/index.js';
25
26
  import { ENGINE_VERSION, MAX_MINUTE, PERSON_SCHEMA } from './types.js';
@@ -59,6 +60,7 @@ const OPTIONAL_KEYS = new Set([
59
60
  'ambient',
60
61
  'retention',
61
62
  'methods',
63
+ 'prospection',
62
64
  ]);
63
65
  /** Kind of a JSON value for the per-field type check in `restore`. */
64
66
  const kindOf = (x) => (Array.isArray(x) ? 'array' : x === null ? 'null' : typeof x);
@@ -171,6 +173,7 @@ export function restore(input) {
171
173
  keepValid(out, 'ambient', sanitizeAmbient);
172
174
  keepValid(out, 'retention', cleanRetention);
173
175
  keepValid(out, 'methods', sanitizeMethods);
176
+ keepValid(out, 'prospection', (x) => sanitizeProspection(x, out.now, out.id));
174
177
  boundLists(out);
175
178
  return out;
176
179
  }
package/dist/types.d.ts CHANGED
@@ -543,6 +543,10 @@ export interface Goal {
543
543
  status: 'active' | 'achieved' | 'abandoned';
544
544
  adoptedAt: Minute;
545
545
  deadline?: Minute;
546
+ /** Opt-in expected completion valuation; deadline remains urgency, never a cutoff. */
547
+ temporal?: {
548
+ halfLifeMinutes: number;
549
+ };
546
550
  /** Last minute the goal was advanced (or adopted); neglect is counted from here. */
547
551
  lastAdvancedAt?: Minute;
548
552
  /** Importance before neglect decay; `revisePurposes` recomputes `importance` from it. */
@@ -787,7 +791,8 @@ export interface Activity {
787
791
  }
788
792
  export declare const PERSON_SCHEMA = "human/person@1";
789
793
  /**
790
- * Current: 2.2.0 (2026-10-14): method evidence grades each action independently.
794
+ * Current: 2.3.0 (2026-10-17): optional temporal goals and received consequences.
795
+ * 2.2.0 (2026-10-14): method evidence grades each action independently.
791
796
  * Successful alternatives no longer count as failures of another action. Person state shape is unchanged.
792
797
  * 2.1.0 saves migrate by stamping the version.
793
798
  *
@@ -822,7 +827,7 @@ export declare const PERSON_SCHEMA = "human/person@1";
822
827
  * and an activity that would cover a closing stretch is reviewed when the stretch begins (`isUnderWay`,
823
828
  * `dutyReviewAt`). Person shape unchanged.
824
829
  */
825
- export declare const ENGINE_VERSION = "2.2.0";
830
+ export declare const ENGINE_VERSION = "2.3.0";
826
831
  /** A pregnancy carried by this person (host-driven; see `family/`). */
827
832
  export interface Pregnancy {
828
833
  fatherId: PersonId;
@@ -923,6 +928,49 @@ export interface MethodsState {
923
928
  cues: MethodCue[];
924
929
  };
925
930
  }
931
+ /** Exact current-action identity; omitted target/place match only omitted values. */
932
+ export interface ConsequenceAction {
933
+ action: string;
934
+ targetId?: EntityId;
935
+ placeId?: EntityId;
936
+ }
937
+ /** A received conditional opportunity, never another person's consent or an actual outcome. */
938
+ export interface ReceivedConsequence {
939
+ id: string;
940
+ revision: number;
941
+ trigger: ConsequenceAction;
942
+ successor: ConsequenceAction & {
943
+ duration: number;
944
+ effort: Unit;
945
+ focus?: Unit;
946
+ materialCost?: number;
947
+ };
948
+ availableFrom: Minute;
949
+ /** Latest permitted successor completion, inclusive. */
950
+ availableUntil: Minute;
951
+ expiresAt: Minute;
952
+ /** P(access | trigger completes successfully), not consent. */
953
+ chance: Unit;
954
+ confidence: Unit;
955
+ }
956
+ export interface ConsequenceRevision {
957
+ sourceId: EntityId;
958
+ id: string;
959
+ revision: number;
960
+ receivedAt: Minute;
961
+ }
962
+ export interface RetainedConsequence extends ReceivedConsequence {
963
+ sourceId: EntityId;
964
+ channel: 'told';
965
+ receivedAt: Minute;
966
+ /** Own completed trigger; does not establish access or goal progress. */
967
+ triggeredAt?: Minute;
968
+ }
969
+ /** Optional person-owned received knowledge. Deduplication lasts only within bounded revision memory. */
970
+ export interface ProspectionState {
971
+ entries: RetainedConsequence[];
972
+ revisions: ConsequenceRevision[];
973
+ }
926
974
  export interface Person {
927
975
  schema: typeof PERSON_SCHEMA;
928
976
  /** Engine version that last wrote this state. */
@@ -942,6 +990,8 @@ export interface Person {
942
990
  skills: Record<string, Skill>;
943
991
  /** Acquired conditional method evidence and current received cues; absent until used. */
944
992
  methods?: MethodsState;
993
+ /** Received future consequences, absent until attended testimony supplies them. */
994
+ prospection?: ProspectionState;
945
995
  /** Practice consolidation (1.8.0, opt-in via `enableSkillRetention`). Owned by `skills/`. */
946
996
  skillRetention?: {
947
997
  consolidationHours: number;
@@ -1107,6 +1157,12 @@ export interface Percept {
1107
1157
  minutes: number;
1108
1158
  domain?: LearningDomain;
1109
1159
  };
1160
+ /** At most four additions/withdrawals combined; attended external testimony to this person or a broadcast. */
1161
+ consequences?: readonly ReceivedConsequence[];
1162
+ withdrawConsequences?: readonly {
1163
+ id: string;
1164
+ revision: number;
1165
+ }[];
1110
1166
  }
1111
1167
  /**
1112
1168
  * Domains with distinct age curves for learning rate (`lifecourse.learningMultiplier`). 'general' is
package/dist/types.js CHANGED
@@ -84,7 +84,8 @@ export const EMOTIONS = [
84
84
  // ---------------------------------------------------------------------------------------------
85
85
  export const PERSON_SCHEMA = 'human/person@1';
86
86
  /**
87
- * Current: 2.2.0 (2026-10-14): method evidence grades each action independently.
87
+ * Current: 2.3.0 (2026-10-17): optional temporal goals and received consequences.
88
+ * 2.2.0 (2026-10-14): method evidence grades each action independently.
88
89
  * Successful alternatives no longer count as failures of another action. Person state shape is unchanged.
89
90
  * 2.1.0 saves migrate by stamping the version.
90
91
  *
@@ -119,4 +120,4 @@ export const PERSON_SCHEMA = 'human/person@1';
119
120
  * and an activity that would cover a closing stretch is reviewed when the stretch begins (`isUnderWay`,
120
121
  * `dutyReviewAt`). Person shape unchanged.
121
122
  */
122
- export const ENGINE_VERSION = '2.2.0';
123
+ export const ENGINE_VERSION = '2.3.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adam0white/human-framework",
3
- "version": "2.2.1",
3
+ "version": "2.3.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",