@adam0white/human-framework 2.19.0 → 2.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -529,3 +529,14 @@ wording, the generic fact sentence still includes that id so the claim is not hi
529
529
  Stock village outcomes, birth/marriage memories, routine fallbacks and generated
530
530
  community log details use the same per-person option. World-supplied outcome
531
531
  summaries and routine summaries remain exact.
532
+
533
+
534
+ ### Authored setup and explicit host effects
535
+
536
+ Hosts may pass `habits: Habit[]` and `expectations: ActionExpectation[]` to `createPerson`. The framework copies records and their nested objects in order. Invalid or over-bound setup throws `RangeError`; omission preserves prior state. Duplicates remain authored records, with the existing matcher semantics.
537
+
538
+ Use `gestureVoiceTrust(p, voiceId, amount, { reason, at?, action? })` for an explicit signed gesture toward an existing advising voice. It records the realized delta in bounded history and returns `undefined` for a missing voice. Reasons must be nonempty strings; optional action must be a string. Use `addTiredness(p, { sleepPressure?, exertion? })` for positive host-caused costs. Neither advances time or draws randomness. Host magnitudes are engineering premises.
539
+
540
+ `confirmObservedReputation(p, prop, truth, now, table?)` replaces the manual `credence → confirm → applyReputationBelief` sequence. Supply only a fact the person could observe. It does not run attention or invent a moral judgement.
541
+
542
+ `qalbReading(p)` is a read-only, sourced narration lens over existing records. Its detached categorical rows are this person's recorded understanding and evidence. They are never a spiritual health score, a ruling, a rank, or a decision input. Do not present mood, low social trust or missing memories as evidence of poor faith.
@@ -154,6 +154,16 @@ export declare function fastingDamping(b: Pick<BodyState, 'satiety' | 'hydration
154
154
  * fast: perceived hunger is damped (see SCOPE, N2b); true values, capacity and health are unchanged.
155
155
  */
156
156
  export declare function readBody(p: Person, params?: BodyParams, ctx?: BodyPerceptionContext): BodyReadout;
157
+ /**
158
+ * SCOPE (host-caused tiredness): add nonnegative sleep-pressure and exertion costs, clamped to 0..1.
159
+ * The host supplies magnitudes; these are not calibrated claims. Absent, negative and non-finite costs
160
+ * do nothing. A dead body is unchanged, as with consume. Writes only these two body fields: no ticking,
161
+ * sleep transition, depletion, memory, RNG or other effects. Recovery remains with the existing body APIs.
162
+ */
163
+ export declare function addTiredness(p: Person, costs: {
164
+ sleepPressure?: number;
165
+ exertion?: number;
166
+ }): void;
157
167
  /** Eat, drink or take pain relief. `relief` lowers current pain; it then relaxes back toward its sources. */
158
168
  export declare function consume(p: Person, deltas: {
159
169
  food?: number;
package/dist/body/body.js CHANGED
@@ -578,6 +578,21 @@ export function readBody(p, params = BODY_DEFAULTS, ctx) {
578
578
  },
579
579
  };
580
580
  }
581
+ /**
582
+ * SCOPE (host-caused tiredness): add nonnegative sleep-pressure and exertion costs, clamped to 0..1.
583
+ * The host supplies magnitudes; these are not calibrated claims. Absent, negative and non-finite costs
584
+ * do nothing. A dead body is unchanged, as with consume. Writes only these two body fields: no ticking,
585
+ * sleep transition, depletion, memory, RNG or other effects. Recovery remains with the existing body APIs.
586
+ */
587
+ export function addTiredness(p, costs) {
588
+ if (!p.body.alive)
589
+ return;
590
+ for (const key of ['sleepPressure', 'exertion']) {
591
+ const amount = costs[key];
592
+ if (amount !== undefined && Number.isFinite(amount) && amount > 0)
593
+ p.body[key] = clamp01(p.body[key] + amount);
594
+ }
595
+ }
581
596
  /** Eat, drink or take pain relief. `relief` lowers current pain; it then relaxes back toward its sources. */
582
597
  export function consume(p, deltas) {
583
598
  const b = p.body;
@@ -1,3 +1,40 @@
1
+ /**
2
+ * SCOPE: scoring of offered options. `consider` sums the utility terms listed in docs/framework.md
3
+ * ("Utility of an option"): need terms use the person's need urgencies times the gain they *believe* an
4
+ * option gives (the advertisement blended with learned expectation, times believed success); norm,
5
+ * commitment/goal, habit, emotion-tendency, social, effort, risk, material, precommitment and suggestion
6
+ * terms come from the owning faculties or from simple coefficient forms. Traits and values only scale
7
+ * terms. `decide` processes affordances in stable id order, scores them all and hands the list to `will`
8
+ * for vetoes and selection. Borrowed shapes: additive multi-attribute utility with a saturating money
9
+ * term (diminishing marginal value of material gain), prediction-error-adjusted action values
10
+ * (model-free cached values alongside the host's model-based advertisement). It does NOT claim calibrated
11
+ * weights, rational-choice optimality, or that a weighted sum exhausts human deliberation; every term is
12
+ * kept in the trace so the UI can show why.
13
+ *
14
+ * SCOPE (lane will+cognition, 2026-10-03): several voices add one `suggestion:<voiceId>` term each, with
15
+ * reactance summed into one `autonomy` term; remembered advice adds `suggestion:remembered:<source>` terms
16
+ * (see `rememberedTerms` and `will.rememberAdvice`). Scarcity (N13): a host-supplied 0..1 shortfall raises the
17
+ * weight of the material term, shrinks its saturation scale so small sums still count, and raises the
18
+ * `need:safety` term. Borrowed shapes: diminishing marginal utility of wealth (the same sum weighs more to
19
+ * someone who has less) and the scarcity account of Mullainathan & Shafir, where a shortfall captures
20
+ * attention and raises the felt value of the scarce resource. Neither source is catalogued in
21
+ * research/empirical-models.md yet; the coefficients are engineering defaults. It does not model the claimed
22
+ * cognitive cost of scarcity ("bandwidth tax"), debt itself (a host ledger plus commitments), or present
23
+ * bias; and because losses are amplified too, a host must carry the benefit of paying a debt (a commitment,
24
+ * an advertised `safety` gain) or paying looks only like a loss. The spec's "security need" is read as the
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.
37
+ */
1
38
  import type { BodyPerceptionContext } from '../body/index.ts';
2
39
  import { type HabitContext } from '../habits/index.ts';
3
40
  import { type PromptingSeen } from '../promptings/index.ts';
@@ -40,6 +40,7 @@ import { agendaTermsWithGoalEstimates } from '../agenda/index.js';
40
40
  import { trustOf } from '../beliefs/index.js';
41
41
  import { normTerms } from '../conscience/index.js';
42
42
  import { clamp01, cmpStr, dexp, minuteOfDay, round } from '../core/index.js';
43
+ import { keysOf } from '../core/keys.js';
43
44
  import { curiosityTerms } from '../curiosity/index.js';
44
45
  import { habitEase, habitPull } from '../habits/index.js';
45
46
  import { expectedEffect } from '../memory/index.js';
@@ -270,7 +271,7 @@ export function consider(p, aff, ctx) {
270
271
  };
271
272
  // Needs: urgency × believed gain × believed success.
272
273
  const expected = expectedEffect(p, aff);
273
- for (const id of Object.keys(expected.needs)) {
274
+ for (const id of keysOf(expected.needs)) {
274
275
  const gain = expected.needs[id] ?? 0;
275
276
  if (gain === 0)
276
277
  continue;
@@ -296,7 +297,7 @@ export function consider(p, aff, ctx) {
296
297
  let relevance = 1;
297
298
  if (expected.valence > 0) {
298
299
  let served = -1;
299
- for (const id of Object.keys(expected.needs))
300
+ for (const id of keysOf(expected.needs))
300
301
  if ((expected.needs[id] ?? 0) > 0)
301
302
  served = Math.max(served, urgencyOf(ctx.needs, id));
302
303
  if (served >= 0)
@@ -1,3 +1,4 @@
1
+ export * from './keys.ts';
1
2
  export * from './libm.ts';
2
3
  export * from './math.ts';
3
4
  export * from './random.ts';
@@ -1,3 +1,4 @@
1
+ export * from './keys.js';
1
2
  export * from './libm.js';
2
3
  export * from './math.js';
3
4
  export * from './random.js';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Own enumerable string keys of a typed record, in Object.keys order.
3
+ * Callers must supply records whose runtime keys match their declared key type.
4
+ * This only types enumeration; it does not sort, validate keys or change state.
5
+ */
6
+ export declare function keysOf<K extends string>(record: Partial<Record<K, unknown>>): K[];
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Own enumerable string keys of a typed record, in Object.keys order.
3
+ * Callers must supply records whose runtime keys match their declared key type.
4
+ * This only types enumeration; it does not sort, validate keys or change state.
5
+ */
6
+ export function keysOf(record) {
7
+ return Object.keys(record);
8
+ }
@@ -88,7 +88,7 @@ function sanitizeAptitudes(x) {
88
88
  out[k] = clamp(v, 0.25, 4);
89
89
  return Object.keys(out).length > 0 ? out : undefined;
90
90
  }
91
- /** Validate a saved family slice; undefined when nothing usable remains (used by `restore`). @internal */
91
+ /** Validate a saved family slice; drop malformed pregnancies, keep usable fields. Undefined when none remain. @internal */
92
92
  export function sanitizeFamily(x) {
93
93
  if (!isObj(x))
94
94
  return undefined;
@@ -101,9 +101,9 @@ export function sanitizeFamily(x) {
101
101
  if (pg &&
102
102
  typeof pg === 'object' &&
103
103
  typeof pg.fatherId === 'string' &&
104
- typeof pg.conceivedAt === 'number' &&
105
- typeof pg.dueAt === 'number' &&
106
- typeof pg.seed === 'number')
104
+ isNum(pg.conceivedAt) &&
105
+ isNum(pg.dueAt) &&
106
+ isNum(pg.seed))
107
107
  out.pregnancy = { fatherId: pg.fatherId, conceivedAt: pg.conceivedAt, dueAt: pg.dueAt, seed: pg.seed };
108
108
  if (isNum(f.attachment))
109
109
  out.attachment = clamp01(f.attachment);
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Human Framework public surface. */
2
- export declare const FRAMEWORK_VERSION = "2.19.0";
2
+ export declare const FRAMEWORK_VERSION = "2.20.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, MakeUpReceipt, PrayerCalendar, PrayerSchedule, PrayerTimes, Retimer, } from './agenda/index.ts';
@@ -28,6 +28,7 @@ export type { MigrationStep } from './migrate.ts';
28
28
  export { MIGRATIONS, migratableVersions, migrate } from './migrate.ts';
29
29
  export * from './narrate/index.ts';
30
30
  export * from './needs/index.ts';
31
+ export { confirmObservedReputation } from './observed-reputation.ts';
31
32
  export * from './partnering/index.ts';
32
33
  export { enablePersistence, PERSISTENCE_DEFAULTS, pursuitOf } from './persistence/index.ts';
33
34
  export * from './person.ts';
@@ -38,6 +39,7 @@ export { adoptProjectTask, considerProjects, enableProjects, PROJECTS_DEFAULTS,
38
39
  export * from './promptings/index.ts';
39
40
  export type { ConsequenceEvidence, PlanRoute, PlanStep } from './prospection/index.ts';
40
41
  export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, enablePlanning, FORECAST_DEFAULTS, forecastLine, forecastThresholds, PROSPECTION_DEFAULTS, seedConsequences, } from './prospection/index.ts';
42
+ export { type QalbDescription, qalbReading } from './qalb.ts';
41
43
  export { fingerprint, restore, snapshot } from './restore.ts';
42
44
  export * from './review/index.ts';
43
45
  export * from './scenarios/index.ts';
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Human Framework public surface. */
2
- export const FRAMEWORK_VERSION = '2.19.0';
2
+ export const FRAMEWORK_VERSION = '2.20.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, isContinuingCommitment, isUnderWay, MAKRUH_DEFAULTS, makruhWindows, missedExcuse, onFinished, owedMakeUps, PRAYER_IMPORTANCE, PURPOSE_DEFAULTS, prayerWindows, pressureReachedAt, promise, proposeGoals, RAMADAN_DEFAULTS, ramadanFast, ramadanMeals, release as releaseCommitment, retimeCommitments, retirePaidMakeUps, revisePurposes, scheduleMakeUp, spanMeetsWindow, suhoorWindow, timesFor, violatesAbstention, } from './agenda/index.js';
@@ -25,6 +25,7 @@ export { clearMethodContext, demonstrateMethod, METHOD_DEFAULTS, methodEvidence,
25
25
  export { MIGRATIONS, migratableVersions, migrate } from './migrate.js';
26
26
  export * from './narrate/index.js';
27
27
  export * from './needs/index.js';
28
+ export { confirmObservedReputation } from './observed-reputation.js';
28
29
  export * from './partnering/index.js';
29
30
  export { enablePersistence, PERSISTENCE_DEFAULTS, pursuitOf } from './persistence/index.js';
30
31
  export * from './person.js';
@@ -34,6 +35,7 @@ export * from './presentation.js';
34
35
  export { adoptProjectTask, considerProjects, enableProjects, PROJECTS_DEFAULTS, projectAppeal, } from './projects/index.js';
35
36
  export * from './promptings/index.js';
36
37
  export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, enablePlanning, FORECAST_DEFAULTS, forecastLine, forecastThresholds, PROSPECTION_DEFAULTS, seedConsequences, } from './prospection/index.js';
38
+ export { qalbReading } from './qalb.js';
37
39
  export { fingerprint, restore, snapshot } from './restore.js';
38
40
  export * from './review/index.js';
39
41
  export * from './scenarios/index.js';
@@ -1,3 +1,29 @@
1
+ /**
2
+ * SCOPE: episodic memory and learned action expectations. Episodes carry a salience that decays
3
+ * exponentially (Ebbinghaus-style forgetting curve, modelled as a single exponential per episode) with a
4
+ * half-life that lengthens for emotionally strong episodes (emotional-enhancement of memory), and recall
5
+ * slightly raises the salience of what is retrieved (testing effect). Expectations are cached action
6
+ * values learned by a delta rule (prediction-error learning, Rescorla-Wagner shape) with a minimum
7
+ * learning rate and a clamped step, blended with the host's advertisement by sample-based confidence.
8
+ * It does NOT model reconsolidation, false memory, interference between similar episodes, sleep-dependent
9
+ * consolidation, semantic abstraction, or model-based planning; parameters are engineering defaults for
10
+ * game time scales, not calibrated estimates.
11
+ *
12
+ * SCOPE (lasting gists, 1.8.0, opt-in per person with `enableGists`): episodic detail does not last a life, but the
13
+ * gist of what mattered does. An episode that leaves the episode list (evicted beyond the bound, or older than
14
+ * `GIST_DEFAULTS.horizon` at a `foldGists` call, which the composite makes at each day's close) is folded into the
15
+ * gist with the same kind, action, people and place when it was emotional enough (|valence| ≥ `minValence`, or a
16
+ * loss); weaker ones are simply forgotten. A gist keeps the encoding-weighted mean valence, a count and the summary of
17
+ * its strongest episode, and fades with half-lives of years (`halfLifeNeutral`..`halfLifeEmotional` by |valence|).
18
+ * Gists answer `recall` and `recallByCue` like episodes (so a remembered fear or loss still comes back at its place,
19
+ * person or anniversary), and `expectedEffect` reports the gists about an option's action or place, which cognition
20
+ * weighs as a `memory` term that fades as fresh experience of the option accumulates. So a fear learned at 20 still
21
+ * shapes choices at 40 after its episodes are gone, and enough good later visits outweigh it. Named shapes: the
22
+ * fading of episodic detail while gist persists (fuzzy-trace theory, Brainerd & Reyna) and the long retention of
23
+ * emotional autobiographical memories; the horizon, thresholds, half-lives and the bound of 64 are engineering
24
+ * choices. It does not model reconstruction errors in gists, deliberate rehearsal beyond retelling (`recall`
25
+ * boosts), or semantic knowledge abstracted from gists.
26
+ */
1
27
  import type { Affordance, AppraisalEvent, EntityId, Episode, Gist, MemoryState, Minute, NeedId, Outcome, Person, Signed, Unit } from '../types.ts';
2
28
  export declare const MEMORY_DEFAULTS: {
3
29
  maxEpisodes: number;
@@ -25,6 +25,7 @@
25
25
  * boosts), or semantic knowledge abstracted from gists.
26
26
  */
27
27
  import { clamp, clamp01, clampSigned, decay, hourOf, isNum, isObj, lerp, runningMean, } from '../core/index.js';
28
+ import { keysOf } from '../core/keys.js';
28
29
  import { DAYS_PER_YEAR, MINUTES_PER_DAY, MINUTES_PER_YEAR } from '../types.js';
29
30
  export const MEMORY_DEFAULTS = {
30
31
  maxEpisodes: 200,
@@ -511,10 +512,7 @@ export function expectedEffect(p, aff) {
511
512
  const samples = learned?.samples ?? 0;
512
513
  const confidence = samples / (samples + MEMORY_DEFAULTS.confidenceK);
513
514
  const needs = {};
514
- const keys = new Set([
515
- ...Object.keys(aff.advertises),
516
- ...(learned ? Object.keys(learned.needs) : []),
517
- ]);
515
+ const keys = new Set([...keysOf(aff.advertises), ...(learned ? keysOf(learned.needs) : [])]);
518
516
  for (const k of [...keys].sort()) {
519
517
  const adv = aff.advertises[k] ?? 0;
520
518
  const lv = learned?.needs[k];
@@ -563,11 +561,7 @@ function updateExpectation(p, key, aff, success, realized, felt) {
563
561
  }
564
562
  x.samples += 1;
565
563
  const n = x.samples;
566
- const keys = new Set([
567
- ...Object.keys(x.needs),
568
- ...Object.keys(aff.advertises),
569
- ...Object.keys(realized),
570
- ]);
564
+ const keys = new Set([...keysOf(x.needs), ...keysOf(aff.advertises), ...keysOf(realized)]);
571
565
  for (const k of keys) {
572
566
  const prior = x.needs[k] ?? aff.advertises[k] ?? 0;
573
567
  x.needs[k] = step(prior, realized[k] ?? 0, n);
@@ -0,0 +1,4 @@
1
+ import { type TraitEffect } from './social/social.ts';
2
+ import type { EntityId, Minute, Person } from './types.ts';
3
+ /** Returns the same detached, sorted misled-source ids as confirm; no relationship alias escapes. */
4
+ export declare function confirmObservedReputation(p: Person, prop: string, truth: boolean, now: Minute, table?: Record<string, TraitEffect>): EntityId[];
@@ -0,0 +1,16 @@
1
+ /**
2
+ * SCOPE: opt-in composition of beliefs.confirm and social.applyReputationBelief, in that order.
3
+ * Owns no state or formula. This is the Hearsay 2 wrapper: direct observation corrects the belief,
4
+ * recalibrates its sources and applies the credence change to the subject's relationship. It does
5
+ * not infer opposite traits, accuse a source of lying, run attention or record a percept. Hosts
6
+ * supply only observations their person has access to. Existing callers remain unchanged.
7
+ */
8
+ import { confirm, credence } from './beliefs/beliefs.js';
9
+ import { applyReputationBelief } from './social/social.js';
10
+ /** Returns the same detached, sorted misled-source ids as confirm; no relationship alias escapes. */
11
+ export function confirmObservedReputation(p, prop, truth, now, table) {
12
+ const before = credence(p, prop);
13
+ const misled = confirm(p, prop, truth, now);
14
+ applyReputationBelief(p, prop, before, credence(p, prop), now, table);
15
+ return misled;
16
+ }
package/dist/person.d.ts CHANGED
@@ -50,6 +50,11 @@ export declare const PERSON_DEFAULTS: {
50
50
  nightFrom: number;
51
51
  nightTo: number;
52
52
  };
53
+ /**
54
+ * SCOPE (host setup): optional habits and expectations copy authored records, including nested cues and
55
+ * need deltas, in source order. Invalid numbers, domains or collection bounds reject with RangeError;
56
+ * setup never clamps, evicts, learns, advances time or consumes RNG. Omitted options keep legacy state.
57
+ */
53
58
  export declare function createPerson(spec: PersonSpec): Person;
54
59
  export interface PersonReadout {
55
60
  body: BodyReadout;
package/dist/person.js CHANGED
@@ -27,13 +27,13 @@ import { ageCharacter, enableCharacterChange, noteCharacterDay, noteCharacterSoc
27
27
  import { closeDay, enableYearbook, endDay, noteAnswer, noteCommitments, noteDecision, noteMood, noteOutcome, trimChronicle, } from './chronicle/index.js';
28
28
  import { decide as cognitionDecide, desperationOf, prospectiveRoute, scoreAll } from './cognition/index.js';
29
29
  import { createConscience, guiltLoad, heldNorms, normVeto, recordDeed, recordRepair, repent, } from './conscience/index.js';
30
- import { clamp01, clampSigned, createRng, dayOf, isObj, minuteOfDay, round } from './core/index.js';
30
+ import { clamp01, clampSigned, createRng, dayOf, isObj, keysOf, minuteOfDay, round } from './core/index.js';
31
31
  import { enableCuriosity, noteCuriosity } from './curiosity/index.js';
32
32
  import { ambientBodyParams, ambientModifiers, ambientMood, ambientNeeds } from './environment/index.js';
33
33
  import { aptitudeOf, createFamily, pregnancyModifiers } from './family/index.js';
34
- import { advanceHabits, reinforce, withholdCued } from './habits/index.js';
34
+ import { advanceHabits, HABIT_DEFAULTS, reinforce, withholdCued } from './habits/index.js';
35
35
  import { ageYears, learningMultiplier, lifeModifiers } from './lifecourse/index.js';
36
- import { advanceMemory, createMemory, enableGists, foldGists, learnOutcome, recallByCue, remember, } from './memory/index.js';
36
+ import { advanceMemory, createMemory, enableGists, foldGists, learnOutcome, MEMORY_DEFAULTS, recallByCue, remember, } from './memory/index.js';
37
37
  import { intentionFor, narrateDecision, voiceLine } from './narrate/index.js';
38
38
  import { advanceNeeds, createNeeds, levelOf, meanSatisfaction, readNeeds, satisfy, urgencyOf, } from './needs/index.js';
39
39
  import { widow } from './partnering/index.js';
@@ -114,7 +114,56 @@ function fill(keys, given, def) {
114
114
  }
115
115
  return out;
116
116
  }
117
+ function validateHostSetup(spec) {
118
+ const finite = (n) => Number.isFinite(n);
119
+ const unit = (n) => finite(n) && n >= 0 && n <= 1;
120
+ const count = (n) => Number.isSafeInteger(n) && n >= 0;
121
+ const optional = (n, valid) => n === undefined || valid(n);
122
+ if (spec.habits !== undefined) {
123
+ if (!Array.isArray(spec.habits) || spec.habits.length > HABIT_DEFAULTS.maxHabits)
124
+ throw new RangeError('Invalid habits setup bound');
125
+ for (const h of spec.habits) {
126
+ if (!h ||
127
+ !isObj(h.cue) ||
128
+ typeof h.action !== 'string' ||
129
+ h.action.length === 0 ||
130
+ !unit(h.strength) ||
131
+ !count(h.repetitions) ||
132
+ !finite(h.lastAt) ||
133
+ !optional(h.withheld, count) ||
134
+ !optional(h.lastWithheldAt, finite) ||
135
+ !optional(h.craving, unit) ||
136
+ !optional(h.cue.hour, (n) => finite(n) && n >= 0 && n < 24) ||
137
+ (h.cue.placeId !== undefined && typeof h.cue.placeId !== 'string') ||
138
+ (h.cue.after !== undefined && typeof h.cue.after !== 'string'))
139
+ throw new RangeError('Invalid habit setup record');
140
+ }
141
+ }
142
+ if (spec.expectations !== undefined) {
143
+ if (!Array.isArray(spec.expectations) || spec.expectations.length > MEMORY_DEFAULTS.maxExpectations)
144
+ throw new RangeError('Invalid expectations setup bound');
145
+ for (const e of spec.expectations) {
146
+ if (!e ||
147
+ typeof e.key !== 'string' ||
148
+ e.key.length === 0 ||
149
+ !isObj(e.needs) ||
150
+ !unit(e.successRate) ||
151
+ !count(e.samples) ||
152
+ !finite(e.valence) ||
153
+ Math.abs(e.valence) > 1 ||
154
+ Object.entries(e.needs).some(([key, n]) => ![...PHYSIOLOGICAL_NEEDS, ...PSYCHOLOGICAL_NEEDS].some((need) => need === key) ||
155
+ (n !== undefined && !finite(n))))
156
+ throw new RangeError('Invalid expectation setup record');
157
+ }
158
+ }
159
+ }
160
+ /**
161
+ * SCOPE (host setup): optional habits and expectations copy authored records, including nested cues and
162
+ * need deltas, in source order. Invalid numbers, domains or collection bounds reject with RangeError;
163
+ * setup never clamps, evicts, learns, advances time or consumes RNG. Omitted options keep legacy state.
164
+ */
117
165
  export function createPerson(spec) {
166
+ validateHostSetup(spec);
118
167
  const now = spec.now ?? 0;
119
168
  const traits = fill(TRAIT_KEYS, spec.traits, 0.5);
120
169
  const values = fill(VALUE_KEYS, spec.values, 0.5);
@@ -143,6 +192,38 @@ export function createPerson(spec) {
143
192
  trace: [],
144
193
  nextDecision: 0,
145
194
  };
195
+ if (spec.habits)
196
+ p.habits = spec.habits.map((h) => {
197
+ const cue = {};
198
+ if (h.cue.hour !== undefined)
199
+ cue.hour = h.cue.hour;
200
+ if (h.cue.placeId !== undefined)
201
+ cue.placeId = h.cue.placeId;
202
+ if (h.cue.after !== undefined)
203
+ cue.after = h.cue.after;
204
+ const out = {
205
+ cue,
206
+ action: h.action,
207
+ strength: h.strength,
208
+ repetitions: h.repetitions,
209
+ lastAt: h.lastAt,
210
+ };
211
+ if (h.craving !== undefined)
212
+ out.craving = h.craving;
213
+ if (h.withheld !== undefined)
214
+ out.withheld = h.withheld;
215
+ if (h.lastWithheldAt !== undefined)
216
+ out.lastWithheldAt = h.lastWithheldAt;
217
+ return out;
218
+ });
219
+ if (spec.expectations)
220
+ p.memory.expectations = spec.expectations.map((e) => ({
221
+ key: e.key,
222
+ needs: Object.fromEntries(Object.entries(e.needs)),
223
+ successRate: e.successRate,
224
+ samples: e.samples,
225
+ valence: e.valence,
226
+ }));
146
227
  if (spec.lexicon)
147
228
  p.lexicon = structuredClone(spec.lexicon);
148
229
  if (spec.retention)
@@ -1399,7 +1480,7 @@ export function finish(p, outcome, opts = {}) {
1399
1480
  const realized = realizedNeedDeltas(act, given, before, after);
1400
1481
  // Felt valence.
1401
1482
  let felt = 0;
1402
- for (const id of Object.keys(realized)) {
1483
+ for (const id of keysOf(realized)) {
1403
1484
  felt += D.feltGain * (D.feltBase + urgencyOf(before, id)) * (realized[id] ?? 0);
1404
1485
  }
1405
1486
  if (outcome.status === 'failed')
package/dist/qalb.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * SCOPE: a read-only narration lens over retained attention evidence, held norms and conviction,
3
+ * mood and the three distinct trust stores. Owns no state and supplies no decision input. It does
4
+ * not infer spiritual health, rank people, judge norms, measure the ruh or compute divine acceptance.
5
+ * Retained memories are evidence available for narration, not a complete attention history.
6
+ *
7
+ * Provenance: research/reconciliation.md §2–4, §7 (Q7: Qur'an 22:46; H9: Bukhari 52 /
8
+ * Muslim 1599a); research/islamic-foundations.md §2 (qalb as an overlapping interpretive view);
9
+ * research/decisions.md, “Qalb, nafs, fitra, ʿaql as state”. These support the lens, not a
10
+ * diagnostic mapping. Sign/zero categories below are engineering descriptions of stored values,
11
+ * not religious categories. Mood and interpersonal trust imply nothing about faith.
12
+ */
13
+ import type { Person } from './types.ts';
14
+ export interface QalbDescription {
15
+ facet: 'attention' | 'held-norm' | 'mood' | 'trust';
16
+ category: string;
17
+ /** Every description is attributed to this simulated person's records. */
18
+ personId: string;
19
+ text: string;
20
+ /** Detached identifiers and state paths, never mutable state objects or spiritual scores. */
21
+ evidence: string[];
22
+ sources: string[];
23
+ }
24
+ /**
25
+ * Plain English descriptions; no scalar summary, comparison or hidden update.
26
+ * Hosts must not present these records as spiritual health or a religious assessment.
27
+ */
28
+ export declare function qalbReading(p: Person): QalbDescription[];
package/dist/qalb.js ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Plain English descriptions; no scalar summary, comparison or hidden update.
3
+ * Hosts must not present these records as spiritual health or a religious assessment.
4
+ */
5
+ export function qalbReading(p) {
6
+ const sources = [
7
+ 'research/reconciliation.md §2–4, §7; Q7 (22:46); H9 (Bukhari 52; Muslim 1599a)',
8
+ 'research/islamic-foundations.md §2: Qalb',
9
+ 'research/decisions.md: Qalb, nafs, fitra, ʿaql as state (2026-10-18)',
10
+ ];
11
+ const rows = [];
12
+ const add = (facet, category, text, evidence) => {
13
+ rows.push({ facet, category, personId: p.id, text, evidence, sources: [...sources] });
14
+ };
15
+ const memories = [...p.memory.episodes, ...(p.memory.gists ?? [])];
16
+ add('attention', memories.length ? 'retained-evidence' : 'no-retained-evidence', memories.length
17
+ ? 'Their memories retain some experiences. These do not show everything they noticed.'
18
+ : 'Their records contain no retained experiences. This does not show what they noticed.', memories.map((e) => `memory:${e.id}:${e.kind}`));
19
+ if (!p.conscience.norms.length)
20
+ add('held-norm', 'unrecorded', 'Their records contain no held rules.', ['conscience.norms']);
21
+ for (const n of p.conscience.norms) {
22
+ add('held-norm', n.conviction === 0 ? 'no-conviction' : 'held-understanding', n.conviction === 0
23
+ ? 'They have a recorded rule with no stated conviction.'
24
+ : 'They hold an understanding of this rule. This is their view.', [`norm:${n.normId}`, `standing:${n.standing}`, 'conscience.norms.conviction']);
25
+ }
26
+ add('mood', p.affect.mood.valence < 0 ? 'unpleasant' : p.affect.mood.valence > 0 ? 'pleasant' : 'neutral', p.affect.mood.valence < 0
27
+ ? 'Their recorded mood feels unpleasant.'
28
+ : p.affect.mood.valence > 0
29
+ ? 'Their recorded mood feels pleasant.'
30
+ : 'Their recorded mood feels neutral.', ['affect.mood.valence']);
31
+ add('mood', p.affect.mood.arousal === 0 ? 'no-arousal' : 'some-arousal', p.affect.mood.arousal === 0
32
+ ? 'Their recorded mood has no arousal.'
33
+ : 'Their recorded mood has some arousal.', ['affect.mood.arousal']);
34
+ // Keep stores distinct. Do not substitute relationship trust for source or advising-voice trust.
35
+ const ties = p.social.relationships.map((r) => ({
36
+ store: 'social.relationships',
37
+ id: r.otherId,
38
+ trust: r.trust,
39
+ }));
40
+ const voices = p.will.voices.map((v) => ({ store: 'will.voices', id: v.voiceId, trust: v.trust }));
41
+ const sourcesTrust = Object.entries(p.memory.sourceTrust).map(([id, trust]) => ({
42
+ store: 'memory.sourceTrust',
43
+ id,
44
+ trust,
45
+ }));
46
+ const trusts = [...ties, ...voices, ...sourcesTrust];
47
+ if (!trusts.length)
48
+ add('trust', 'unrecorded', 'Their records contain no learned trust.', [
49
+ 'social.relationships',
50
+ 'will.voices',
51
+ 'memory.sourceTrust',
52
+ ]);
53
+ for (const t of trusts) {
54
+ // 0.5 is the numeric range midpoint, not a faith threshold or personal baseline.
55
+ const subject = t.store === 'social.relationships'
56
+ ? 'this person'
57
+ : t.store === 'will.voices'
58
+ ? 'this advising voice'
59
+ : 'this information source';
60
+ add('trust', t.trust < 0.5 ? 'below-midpoint' : t.trust > 0.5 ? 'above-midpoint' : 'at-midpoint', t.trust < 0.5
61
+ ? `Their trust in ${subject} is below the middle of its range.`
62
+ : t.trust > 0.5
63
+ ? `Their trust in ${subject} is above the middle of its range.`
64
+ : `Their trust in ${subject} is at the middle of its range.`, [`${t.store}:${t.id}`]);
65
+ }
66
+ return rows;
67
+ }
@@ -471,6 +471,16 @@ export const REPUTATION_DEFAULTS = {
471
471
  /** First-hand familiarity damps hearsay: scale = 1 - familiarityDamping × familiarity. */
472
472
  familiarityDamping: 0.5,
473
473
  };
474
+ /** Only authored, finite effects count; prototype properties are not character traits. */
475
+ function reputationEffect(trait, table) {
476
+ const effect = Object.hasOwn(table, trait) ? table[trait] : undefined;
477
+ return effect &&
478
+ Number.isFinite(effect.affection) &&
479
+ Number.isFinite(effect.trust) &&
480
+ Number.isFinite(effect.respect)
481
+ ? effect
482
+ : undefined;
483
+ }
474
484
  /** Splits a character proposition '<personId>:<trait>[:...]' into its subject and trait, if it is one. */
475
485
  export function parseTraitProp(prop, table = REPUTATION_TRAITS) {
476
486
  const parts = prop.split(':');
@@ -478,7 +488,7 @@ export function parseTraitProp(prop, table = REPUTATION_TRAITS) {
478
488
  return undefined;
479
489
  const personId = parts[0];
480
490
  const trait = parts[1];
481
- if (personId === '' || table[trait] === undefined)
491
+ if (personId === '' || reputationEffect(trait, table) === undefined)
482
492
  return undefined;
483
493
  return { personId, trait };
484
494
  }
@@ -509,7 +519,7 @@ export function applyReputationBelief(p, prop, before, after, now, table = REPUT
509
519
  }
510
520
  /** Whether a character claim about a person is unfavourable to them (for backbiting tags). */
511
521
  export function isUnfavourableTrait(trait, value, table = REPUTATION_TRAITS) {
512
- const e = table[trait];
522
+ const e = reputationEffect(trait, table);
513
523
  if (!e)
514
524
  return false;
515
525
  const sum = e.affection + e.trust + e.respect;
package/dist/types.d.ts CHANGED
@@ -1509,6 +1509,10 @@ export interface PersonSpec {
1509
1509
  values?: Partial<Values>;
1510
1510
  norms?: HeldNorm[];
1511
1511
  skills?: Record<string, Unit>;
1512
+ /** Authored habits, copied in order. Invalid or over-bound setup throws RangeError. */
1513
+ habits?: Habit[];
1514
+ /** Authored expectations, copied in order. Invalid or over-bound setup throws RangeError. */
1515
+ expectations?: ActionExpectation[];
1512
1516
  body?: Partial<Omit<BodyState, 'injuries' | 'illnesses' | 'nextId'>>;
1513
1517
  needs?: Partial<NeedReservoirs>;
1514
1518
  relationships?: (Partial<Relationship> & {
@@ -233,6 +233,18 @@ export declare function noteVoicePromise(p: Person, voiceId: EntityId, event: {
233
233
  at?: Minute;
234
234
  action?: string;
235
235
  }): VoiceRelation;
236
+ /**
237
+ * SCOPE (host trust gesture): add a host-supplied signed amount to an existing advising voice's trust,
238
+ * clamped to 0..1, and record the realized change in the usual bounded history. Magnitudes and reason
239
+ * are host premises, not calibrated claims. Missing voices return undefined without creating a voice.
240
+ * Non-finite amounts are ignored. No pressure, credit, counters, memory, time or RNG changes occur.
241
+ * Empty/non-string reasons and non-string actions reject before mutation with RangeError.
242
+ */
243
+ export declare function gestureVoiceTrust(p: Person, voiceId: EntityId, amount: number, event: {
244
+ reason: string;
245
+ at?: Minute;
246
+ action?: string;
247
+ }): VoiceRelation | undefined;
236
248
  /**
237
249
  * The next minute after `now` at which a pending commitment reaches `wakeCommitmentPressure`, the level at which a
238
250
  * sleeper may wake to serve it (the asleep veto lifts). The composite caps a sleeper's review there so a duty whose
package/dist/will/will.js CHANGED
@@ -293,6 +293,27 @@ export function noteVoicePromise(p, voiceId, event) {
293
293
  noteTrust(v, v.trust - before, event.kept ? 'promise-kept' : 'promise-broken', at, event.action);
294
294
  return v;
295
295
  }
296
+ /**
297
+ * SCOPE (host trust gesture): add a host-supplied signed amount to an existing advising voice's trust,
298
+ * clamped to 0..1, and record the realized change in the usual bounded history. Magnitudes and reason
299
+ * are host premises, not calibrated claims. Missing voices return undefined without creating a voice.
300
+ * Non-finite amounts are ignored. No pressure, credit, counters, memory, time or RNG changes occur.
301
+ * Empty/non-string reasons and non-string actions reject before mutation with RangeError.
302
+ */
303
+ export function gestureVoiceTrust(p, voiceId, amount, event) {
304
+ if (!event || typeof event.reason !== 'string' || event.reason.length === 0)
305
+ throw new RangeError('A trust gesture needs a reason');
306
+ if (event.action !== undefined && typeof event.action !== 'string')
307
+ throw new RangeError('Invalid trust gesture action');
308
+ const v = voiceOf(p, voiceId);
309
+ if (!v || !Number.isFinite(amount))
310
+ return v;
311
+ const before = v.trust;
312
+ v.trust = clamp01(before + amount);
313
+ const at = event.at !== undefined && Number.isFinite(event.at) ? event.at : p.now;
314
+ noteTrust(v, v.trust - before, event.reason, at, event.action);
315
+ return v;
316
+ }
296
317
  const finite = (x) => x === undefined || Number.isFinite(x);
297
318
  /** Host numbers the framework computes with must be finite; a NaN offer is vetoed, never chosen. */
298
319
  function wellFormed(aff) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adam0white/human-framework",
3
- "version": "2.19.0",
3
+ "version": "2.20.0",
4
4
  "description": "Deterministic, explainable simulated people: body, needs, affect, memory, beliefs, skills, habits, relationships, values and a will that can refuse a suggestion",
5
5
  "keywords": [
6
6
  "simulation",