@adam0white/human-framework 2.2.1 → 2.4.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
+ HF 2.3.0 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
  }
@@ -1,4 +1,5 @@
1
1
  import { type HabitContext } from '../habits/index.ts';
2
+ import { type PromptingSeen } from '../promptings/index.ts';
2
3
  import type { Affordance, BodyReadout, Command, Considered, DecisionRecord, LifeModifiers, Minute, NeedReading, Person, Suggestion, Term, Unit } from '../types.ts';
3
4
  import { type CommandOutcome, type WillContext } from '../will/index.ts';
4
5
  export declare const COGNITION_DEFAULTS: {
@@ -132,6 +133,8 @@ export interface Decision {
132
133
  needDeltas: Partial<Record<'autonomy', number>>;
133
134
  /** How a command in force fared (absent when none). */
134
135
  command?: CommandOutcome;
136
+ /** Prompts noticed at this decision point (only with `p.promptings`); the composite records them. */
137
+ prompts?: PromptingSeen[];
135
138
  }
136
139
  /** Highest physiological urgency: the input to the necessity exception. */
137
140
  export declare function desperationOf(needs: NeedReading[]): number;
@@ -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,8 @@ 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 { promptIn, promptingTerms } from '../promptings/index.js';
38
+ import { receivedGoalRoute } from '../prospection/index.js';
37
39
  import { companionSteadiness, socialTerms } from '../social/index.js';
38
40
  import { PHYSIOLOGICAL_NEEDS, VALUE_KEYS } from '../types.js';
39
41
  import { adviceWeight, resolveChoice, safeUtility, standingAdvice, suggestionTargets, voicesIn, } from '../will/index.js';
@@ -200,6 +202,29 @@ export function socialContext(ctx) {
200
202
  satiation: K.socialSatiationFloor + (1 - K.socialSatiationFloor) * (1 - clamp01(belongingLevel)),
201
203
  };
202
204
  }
205
+ /** Same perceived-body effort estimate for actual offers and received successors. @internal */
206
+ export function effortValue(p, aff, ctx) {
207
+ const K = COGNITION_DEFAULTS;
208
+ const per = ctx.body.perceived;
209
+ const effort = clamp01(aff.effort);
210
+ const focus = clamp01(aff.focus ?? 0);
211
+ // A cued habit eases initiation and attention (N4: the depleted fall back on habits), not physical exertion.
212
+ const ease = p.habits.length === 0
213
+ ? 1
214
+ : habitEase(p, aff, { ...ctx.habit, placeId: aff.placeId ?? ctx.habit.placeId });
215
+ const effortCost = (effort * K.effortBase + focus * per.sleepiness) * ease +
216
+ effort * per.fatigue * (1 - clamp01(p.body.fitness));
217
+ return -K.effortScale * effortCost;
218
+ }
219
+ /** Same material valuation for actual offers and received successor costs. @internal */
220
+ export function materialValue(p, material, scarcity) {
221
+ const K = COGNITION_DEFAULTS;
222
+ const v = p.values;
223
+ const weight = (v.security + v.achievement + v.power) / 3;
224
+ const scale = K.materialScale * (1 - K.scarcityScaleShrink * scarcity);
225
+ const sat = Math.sign(material) * (1 - dexp(-Math.abs(material) / scale));
226
+ return K.materialWeight * sat * 2 * weight * (1 + K.scarcityMaterialGain * scarcity);
227
+ }
203
228
  /** Score one option. Pure apart from reads; terms are rounded for stable snapshots. */
204
229
  export function consider(p, aff, ctx) {
205
230
  const K = COGNITION_DEFAULTS;
@@ -264,8 +289,13 @@ export function consider(p, aff, ctx) {
264
289
  }
265
290
  }
266
291
  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));
292
+ const maturityScale = 0.5 + 0.5 * maturity;
293
+ const received = receivedGoalRoute(p, aff, ctx.now, maturityScale, (successor) => effortValue(p, successor, ctx) +
294
+ (successor.material ? materialValue(p, successor.material, scarcity) : 0));
295
+ for (const t of agendaTermsWithGoalEstimates(p, aff, ctx.now, received?.goals))
296
+ push(t.source, t.value * maturityScale);
297
+ if (received)
298
+ push(received.cost.source, received.cost.value);
269
299
  // Habit: cue-matched automaticity (cued by the option's own place), stronger before planning matures.
270
300
  const habit = p.habits.length === 0
271
301
  ? 0
@@ -316,17 +346,9 @@ export function consider(p, aff, ctx) {
316
346
  }
317
347
  }
318
348
  // 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);
349
+ const effort = effortValue(p, aff, ctx);
350
+ if (effort < 0)
351
+ push('effort', effort);
330
352
  // Risk: chance × severity × (fear + emotionality).
331
353
  if (aff.risk && aff.risk.chance > 0 && aff.risk.severity > 0) {
332
354
  const fear = emotionLevel(p, 'fear');
@@ -340,13 +362,8 @@ export function consider(p, aff, ctx) {
340
362
  }
341
363
  }
342
364
  // 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
- }
365
+ if (aff.material)
366
+ push('material', materialValue(p, aff.material, scarcity));
350
367
  // Precommitments inside their daily window.
351
368
  const mod = minuteOfDay(ctx.now);
352
369
  for (const pc of p.will.precommitments) {
@@ -382,6 +399,10 @@ export function consider(p, aff, ctx) {
382
399
  if (!inBreak(p))
383
400
  for (const t of rememberedTerms(p, aff, ctx.now, voices))
384
401
  push(t.source, t.value);
402
+ // Promptings (2.4.0, opt-in): a recurrence pull under `fault` and a practised turning-away from a noticed prompt.
403
+ if (p.promptings)
404
+ for (const t of promptingTerms(p, aff.action, terms, aff.tags, ctx.now, ctx.desperation))
405
+ push(t.source, t.value);
385
406
  let utility = 0;
386
407
  for (const t of terms)
387
408
  utility += t.value;
@@ -455,5 +476,22 @@ export function decide(p, affordances, ctx) {
455
476
  const out = { record, needDeltas };
456
477
  if (res.command)
457
478
  out.command = res.command;
479
+ if (p.promptings) {
480
+ const prompts = [];
481
+ const tagsOf = new Map(sorted.map((a) => [a.id, a.tags]));
482
+ for (const c of res.considered) {
483
+ const seen = promptIn(c.terms, tagsOf.get(c.affordanceId));
484
+ if (seen)
485
+ prompts.push({
486
+ affordanceId: c.affordanceId,
487
+ action: c.action,
488
+ ...seen,
489
+ // A vetoed option was not turned from: the will blocked it, so the response earns no practice.
490
+ turnApplied: c.vetoed === undefined && c.terms.some((t) => t.source === 'promptings:turn'),
491
+ });
492
+ }
493
+ if (prompts.length > 0)
494
+ out.prompts = prompts;
495
+ }
458
496
  return out;
459
497
  }
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.4.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,9 @@ 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 * from './promptings/index.ts';
32
+ export type { ConsequenceEvidence } from './prospection/index.ts';
33
+ export { consequenceEvidence, PROSPECTION_DEFAULTS } from './prospection/index.ts';
31
34
  export * from './restore.ts';
32
35
  export * from './scenarios/index.ts';
33
36
  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.4.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,8 @@ 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 * from './promptings/index.js';
29
+ export { consequenceEvidence, PROSPECTION_DEFAULTS } from './prospection/index.js';
28
30
  export * from './restore.js';
29
31
  export * from './scenarios/index.js';
30
32
  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. 2.3.0 to 2.4.0 stamps the version; promptings remain absent unless enabled. 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,16 @@ 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.3.0': {
62
+ to: '2.4.0',
63
+ note: 'promptings are optional and start absent; existing hosts retain exact behavior',
64
+ apply: stamp,
65
+ },
66
+ '2.2.0': {
67
+ to: '2.3.0',
68
+ note: 'received consequences and temporal goals are optional; existing hosts retain exact behavior',
69
+ apply: stamp,
70
+ },
61
71
  '2.1.0': {
62
72
  to: '2.2.0',
63
73
  note: 'person shape unchanged; method evidence now grades each action independently, preserving successful alternatives',
package/dist/person.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type NafsPattern } from './promptings/index.ts';
1
2
  import { type SkillTransfer } from './skills/index.ts';
2
3
  import type { Activity, Affordance, BodyLoad, BodyReadout, Command, Commitment, DecisionRecord, LearningDomain, Minute, NeedId, NeedReading, NormDefinition, Outcome, Percept, Person, PersonSpec, Retention, Suggestion, SuggestionResolution, Unit } from './types.ts';
3
4
  import { type CommandOutcome } from './will/index.ts';
@@ -230,3 +231,8 @@ export declare function finish(p: Person, outcome: Outcome, opts?: FinishOptions
230
231
  * entries are ignored; an empty or wholly invalid retention restores the defaults. Changes no decision.
231
232
  */
232
233
  export declare function setRetention(p: Person, retention: Retention): void;
234
+ /**
235
+ * The nafs lens (2.4.0, read-only; see `promptings/` SCOPE): the three Qur'anic descriptions of one self (12:53,
236
+ * 75:2, 89:27), each with the matching evidence in existing state. Descriptions, never a level or a rank.
237
+ */
238
+ export declare function nafsReading(p: Person): NafsPattern[];
package/dist/person.js CHANGED
@@ -26,7 +26,7 @@ import { advanceBody, BODY_DEFAULTS, checkDowned, clearDowned, consume, createBo
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';
29
- import { createConscience, heldNorms, normVeto, recordDeed, recordRepair, repent, } from './conscience/index.js';
29
+ import { createConscience, guiltLoad, heldNorms, normVeto, recordDeed, recordRepair, repent, } from './conscience/index.js';
30
30
  import { clamp01, clampSigned, createRng, dayOf, isObj, minuteOfDay } from './core/index.js';
31
31
  import { ambientBodyParams, ambientModifiers, ambientMood, ambientNeeds } from './environment/index.js';
32
32
  import { aptitudeOf, createFamily, pregnancyModifiers } from './family/index.js';
@@ -36,6 +36,8 @@ 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 { enablePromptings, nafsPatterns, notePromptings } from './promptings/index.js';
40
+ import { consumeConsequences, pruneConsequences, receiveConsequences } from './prospection/index.js';
39
41
  import { enableSkillRetention, learnByWatching, practise, seedSkills, } from './skills/index.js';
40
42
  import { advanceSocial, applyReputationBelief, careFor, closeness, judge, markDeceased, relationshipWith, seedRelationships, socialEvent, threatAppraisal, } from './social/index.js';
41
43
  import { ENGINE_VERSION, MAX_MINUTE, PERSON_SCHEMA, PHYSIOLOGICAL_NEEDS, PSYCHOLOGICAL_NEEDS, TRAIT_KEYS, VALUE_KEYS, } from './types.js';
@@ -152,6 +154,8 @@ export function createPerson(spec) {
152
154
  enableCharacterChange(p);
153
155
  if (on?.skillRetention)
154
156
  enableSkillRetention(p);
157
+ if (on?.promptings)
158
+ enablePromptings(p, on.promptings);
155
159
  return p;
156
160
  }
157
161
  /**
@@ -353,6 +357,7 @@ export function tick(p, now) {
353
357
  return;
354
358
  if (!p.body.alive) {
355
359
  p.now = now;
360
+ pruneConsequences(p);
356
361
  return;
357
362
  }
358
363
  const seg = PERSON_DEFAULTS.maxSegment;
@@ -366,13 +371,16 @@ export function tick(p, now) {
366
371
  // A minute so large that the grid step rounds away cannot advance (security review H2): stop, never spin.
367
372
  if (!(end > p.now)) {
368
373
  p.now = now;
374
+ pruneConsequences(p);
369
375
  return;
370
376
  }
371
377
  advanceSegment(p, end - p.now, load, active && act ? act.affordance : undefined);
372
378
  p.now = end;
379
+ pruneConsequences(p);
373
380
  if (!p.body.alive) {
374
381
  p.activity = null;
375
382
  p.now = now;
383
+ pruneConsequences(p);
376
384
  return;
377
385
  }
378
386
  // Discrete bookkeeping runs only on grid boundaries, so it happens at the same minutes whatever the
@@ -517,6 +525,7 @@ export function skip(p, to) {
517
525
  p.activity = null;
518
526
  skipBody(p, to);
519
527
  p.now = to;
528
+ pruneConsequences(p);
520
529
  skipAffect(p, to);
521
530
  advanceAgenda(p, to);
522
531
  // Lasting gists (1.8.0, opt-in): episodes now past the horizon fold into gists.
@@ -535,6 +544,7 @@ function socialKindOf(kind) {
535
544
  return SOCIAL_ALIASES[kind];
536
545
  }
537
546
  function perceiveOne(p, pc) {
547
+ receiveConsequences(p, pc);
538
548
  const at = Math.min(Math.max(pc.at, p.affect.lastUpdated), p.now);
539
549
  const actor = pc.actorId;
540
550
  const target = pc.targetId;
@@ -810,7 +820,7 @@ export function decide(p, affordances, opts = {}) {
810
820
  const review = p.activity !== null;
811
821
  if (review)
812
822
  ctx.quiet = true;
813
- const { record, needDeltas, command: co } = cognitionDecide(p, list, ctx);
823
+ const { record, needDeltas, command: co, prompts } = cognitionDecide(p, list, ctx);
814
824
  if (review)
815
825
  record.review = true;
816
826
  const interruptReason = opts.interrupt ?? p.activity?.interrupt?.reason;
@@ -824,6 +834,12 @@ export function decide(p, affordances, opts = {}) {
824
834
  record.narration = narrateDecision(p, record);
825
835
  const resolutions = resolutionsOf(record);
826
836
  const continuing = review && (record.chosenAffordanceId === null || record.chosenAffordanceId === p.activity?.affordanceId);
837
+ // Promptings (opt-in): a review that keeps the running activity is not a new decision point for a prompt.
838
+ if (prompts && !continuing) {
839
+ const noted = notePromptings(p, prompts, record.chosenAffordanceId, p.now);
840
+ if (noted.guilt > 0)
841
+ feel(p, 'guilt', noted.guilt, 'prompting', p.now);
842
+ }
827
843
  for (const res of resolutions) {
828
844
  res.says = voiceLine(p, res, record.id);
829
845
  if (res.verdict === 'complied' && !continuing) {
@@ -1191,6 +1207,7 @@ export function finish(p, outcome, opts = {}) {
1191
1207
  }
1192
1208
  const prev = lastAction(p);
1193
1209
  if (completed) {
1210
+ consumeConsequences(p, aff);
1194
1211
  const hctx = { now };
1195
1212
  if (aff.placeId !== undefined)
1196
1213
  hctx.placeId = aff.placeId;
@@ -1382,3 +1399,14 @@ export function setRetention(p, retention) {
1382
1399
  p.trace.splice(0, p.trace.length - keep);
1383
1400
  trimChronicle(p);
1384
1401
  }
1402
+ /**
1403
+ * The nafs lens (2.4.0, read-only; see `promptings/` SCOPE): the three Qur'anic descriptions of one self (12:53,
1404
+ * 75:2, 89:27), each with the matching evidence in existing state. Descriptions, never a level or a rank.
1405
+ */
1406
+ export function nafsReading(p) {
1407
+ let guilt = 0;
1408
+ for (const e of p.affect.emotions)
1409
+ if (e.id === 'guilt' && e.cause !== 'prompting')
1410
+ guilt = Math.max(guilt, e.intensity);
1411
+ return nafsPatterns(p, { guiltLoad: guiltLoad(p), guilt });
1412
+ }
@@ -0,0 +1 @@
1
+ export * from './promptings.ts';
@@ -0,0 +1 @@
1
+ export * from './promptings.js';
@@ -0,0 +1,100 @@
1
+ import type { Minute, Person, PromptingReading, PromptingRecord, PromptingResponse, PromptingResponseKind, PromptingsSpec, PromptingsState, Term, Unit } from '../types.ts';
2
+ export declare const PROMPTINGS_DEFAULTS: {
3
+ /** Pull (utility units) from which a pull against a held norm or goal is noticed. */
4
+ readonly noticeAt: 0.15;
5
+ /** Largest turning-away term at full practice (utility units). */
6
+ readonly turnMax: 0.8;
7
+ /** No turning-away term at or above this desperation (same scale as will's survival desperation). */
8
+ readonly survivalDesperation: 0.75;
9
+ /** Practice gained at a prompt where the response held (× (1 - practice)). */
10
+ readonly gainTurned: 0.12;
11
+ /** Practice gained at a prompt where the response was tried but the person assented. */
12
+ readonly gainAttempt: 0.04;
13
+ /** Practice halves over this many days without use. */
14
+ readonly practiceHalfLifeDays: 60;
15
+ /** Guilt intensity at a prompt under the `fault` reading. */
16
+ readonly faultGuilt: 0.15;
17
+ /** Recurrence pull at echo level 1 (utility units); only reached when a host sets `echoRate` > 0. */
18
+ readonly echoScale: 0.5;
19
+ readonly echoHalfLifeDays: 3;
20
+ readonly maxRecent: 64;
21
+ readonly maxResponses: 8;
22
+ readonly maxEcho: 32;
23
+ };
24
+ /**
25
+ * Turn the slice on, or reconfigure it: recent records and echo are kept, and a response whose id is already held
26
+ * keeps its practice and last use (the spec's `practice` only seeds new responses). Returns the state.
27
+ */
28
+ export declare function enablePromptings(p: Person, spec?: PromptingsSpec): PromptingsState;
29
+ /** Practice of a response at `now`, after disuse decay. Pure. */
30
+ export declare function practiceAt(r: PromptingResponse, now: Minute): Unit;
31
+ /** Whether an option's terms carry a noticed prompt: its strongest pull and the opposing term. Pure. */
32
+ export declare function promptIn(terms: readonly Term[], tags: readonly string[] | undefined): {
33
+ source: string;
34
+ opposedBy: string;
35
+ } | undefined;
36
+ /**
37
+ * Extra terms for one option (cognition calls this before summing): the echo pull under `fault`, then the
38
+ * turning-away term when the option carries a prompt. Pure apart from reads; empty without the slice.
39
+ */
40
+ export declare function promptingTerms(p: Person, action: string, terms: readonly Term[], tags: readonly string[] | undefined, now: Minute, desperation: number): Term[];
41
+ /** A prompt found at a decision point, handed from cognition to the composite. */
42
+ export interface PromptingSeen {
43
+ affordanceId: string;
44
+ action: string;
45
+ source: string;
46
+ opposedBy: string;
47
+ /** Whether a turning-away term was applied. */
48
+ turnApplied: boolean;
49
+ }
50
+ /** What `notePromptings` did, for the composite (the `fault` guilt goes through affect). */
51
+ export interface PromptingsNoted {
52
+ records: PromptingRecord[];
53
+ /** Guilt intensity to feel at the prompt (only under `fault`); 0 otherwise. */
54
+ guilt: Unit;
55
+ }
56
+ /**
57
+ * Record the prompts of one decision point and learn from them. `chosenId` is the chosen affordance (null when
58
+ * nothing was chosen). The only writer of `p.promptings` after `enablePromptings`.
59
+ */
60
+ export declare function notePromptings(p: Person, seen: readonly PromptingSeen[], chosenId: string | null, now: Minute): PromptingsNoted;
61
+ /** Vocabulary for one prompt under a held reading (narration only; it changes nothing). */
62
+ export declare function promptingLine(reading: PromptingReading, outcome: PromptingRecord['outcome'], kind?: PromptingResponseKind): string;
63
+ /** Read-only summary of recent prompts (a lens; computes nothing new about the person). */
64
+ export interface PromptingsReadingView {
65
+ reading: PromptingReading;
66
+ readingSource?: string;
67
+ prompts: number;
68
+ turned: number;
69
+ passed: number;
70
+ assented: number;
71
+ responses: {
72
+ id: string;
73
+ kind: PromptingResponseKind;
74
+ practice: Unit;
75
+ }[];
76
+ /** Vocabulary for the last few prompts, newest last. */
77
+ lines: string[];
78
+ }
79
+ export declare function promptingsReading(p: Person, now?: Minute): PromptingsReadingView | undefined;
80
+ /**
81
+ * The nafs lens (reconciliation.md §3): the three Qur'anic descriptions of one self, each with the evidence in
82
+ * existing state that matches it. Descriptions, not a level, a ladder or three agents; several can hold at once,
83
+ * and none says anything about rank before God. Inputs are passed in by the composite (open-breach weight and
84
+ * felt guilt from conscience and affect), so this module reads nothing it does not own. Guilt that the `fault` reading
85
+ * feels at a prompt is excluded, so the scrupulous reading is not mapped onto self-reproach (75:2). These mappings
86
+ * from state to description are engineering conventions (C), not interpretations of the verses.
87
+ */
88
+ export interface NafsPattern {
89
+ name: 'urging' | 'reproaching' | 'at rest';
90
+ /** Qur'anic description it borrows (12:53 ammara, 75:2 lawwama, 89:27 mutmaʾinna). */
91
+ ref: string;
92
+ evident: boolean;
93
+ evidence: string;
94
+ }
95
+ export declare function nafsPatterns(p: Person, inputs: {
96
+ guiltLoad: Unit;
97
+ guilt: Unit;
98
+ }): NafsPattern[];
99
+ /** Validate a saved slice; a malformed slice is dropped (undefined), malformed entries are skipped. */
100
+ export declare function sanitizePromptings(input: unknown, now: Minute): PromptingsState | undefined;