@adam0white/human-framework 2.22.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +138 -55
  2. package/dist/agenda/agenda.js +1 -1
  3. package/dist/beliefs/beliefs.js +2 -2
  4. package/dist/body/doze.d.ts +6 -3
  5. package/dist/body/doze.js +7 -4
  6. package/dist/conscience/conscience.d.ts +2 -4
  7. package/dist/conscience/conscience.js +3 -5
  8. package/dist/conversation/conversation.js +11 -10
  9. package/dist/core/index.d.ts +0 -1
  10. package/dist/core/index.js +0 -1
  11. package/dist/core/math.d.ts +0 -2
  12. package/dist/core/math.js +3 -3
  13. package/dist/family/family.d.ts +0 -5
  14. package/dist/family/family.js +12 -6
  15. package/dist/impression.d.ts +2 -7
  16. package/dist/impression.js +5 -7
  17. package/dist/index.d.ts +2 -1
  18. package/dist/index.js +2 -1
  19. package/dist/longrun.d.ts +2 -2
  20. package/dist/longrun.js +1 -2
  21. package/dist/memory/memory.d.ts +0 -4
  22. package/dist/memory/memory.js +20 -6
  23. package/dist/migrate.js +5 -0
  24. package/dist/narrate/narrate.js +3 -1
  25. package/dist/observed-reputation.js +1 -1
  26. package/dist/person.d.ts +5 -1
  27. package/dist/person.js +16 -10
  28. package/dist/persona.js +1 -0
  29. package/dist/presentation.js +1 -0
  30. package/dist/promptings/promptings.js +2 -2
  31. package/dist/qalb.d.ts +0 -12
  32. package/dist/qalb.js +22 -8
  33. package/dist/sim/longrun.d.ts +3 -3
  34. package/dist/sim/longrun.js +1 -1
  35. package/dist/sim/sim.js +9 -4
  36. package/dist/social/impressions.d.ts +0 -12
  37. package/dist/social/impressions.js +13 -8
  38. package/dist/social/social.js +2 -2
  39. package/dist/tick-input.d.ts +24 -0
  40. package/dist/tick-input.js +56 -0
  41. package/dist/types.d.ts +1 -1
  42. package/dist/types.js +1 -1
  43. package/package.json +1 -1
@@ -4,7 +4,7 @@
4
4
  * - `outwardSigns(target)`: perceived fatigue, pain and fear and the felt mood, each reduced by the target's reserve;
5
5
  * a limp (lost moving capacity) shows through any reserve. `selfReport(target)` is the same in words, which hide
6
6
  * more (`IMPRESSION_DEFAULTS.wordsReserve`): "I'm fine".
7
- * - `glimpseOf(observer, target, at, clarity?)` = `glimpse` of the target's outward signs; `acquaintWith` hands the target's
7
+ * - `glimpsePerson(observer, target, at, clarity?)` = `glimpse` of the target's outward signs; `acquaintPerson` hands the target's
8
8
  * traits and family ties (public roles) to `acquaint`.
9
9
  * - `imagine(observer, target, now)`: the target as the observer pictures them — a copy whose traits, fear, pain,
10
10
  * fatigue, mood, ties and trust in voices are the observer's beliefs (each pulled to the prior by its uncertainty)
@@ -61,14 +61,12 @@ export function selfReport(target) {
61
61
  s.pain = round(body.perceived.pain * (1 - clamp01(reserveOf(target, 'pain') * IMPRESSION_DEFAULTS.wordsReserve)));
62
62
  return s;
63
63
  }
64
- export function glimpseOf(observer, target, when, clarity) {
65
- if (typeof when === 'number')
66
- glimpse(observer, target.id, outwardSigns(target), when, clarity);
67
- else
68
- glimpse(observer, target.id, outwardSigns(target), when.at, when.clarity);
64
+ /** `observer` looks at `target` at `at` (clarity 0..1: a clear look near by, or a distant figure; default 1). */
65
+ export function glimpsePerson(observer, target, at, clarity) {
66
+ glimpse(observer, target.id, outwardSigns(target), at, clarity);
69
67
  }
70
68
  /** `observer` has known `target` for a long time (familiarity 0..1): traits and family ties, condensed. */
71
- export function acquaintWith(observer, target, familiarity, at) {
69
+ export function acquaintPerson(observer, target, familiarity, at) {
72
70
  const ties = {};
73
71
  for (const r of target.social.relationships)
74
72
  if (r.roles.some((x) => PUBLIC_ROLES.includes(x)))
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Human Framework public surface. */
2
- export declare const FRAMEWORK_VERSION = "2.22.0";
2
+ export declare const FRAMEWORK_VERSION = "3.0.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';
@@ -46,5 +46,6 @@ export * from './scenarios/index.ts';
46
46
  export * from './sim/index.ts';
47
47
  export * from './skills/index.ts';
48
48
  export * from './social/index.ts';
49
+ export { canonicalTickInputs, type TickInput, type TickInputKey } from './tick-input.ts';
49
50
  export * from './types.ts';
50
51
  export * from './will/index.ts';
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Human Framework public surface. */
2
- export const FRAMEWORK_VERSION = '2.22.0';
2
+ export const FRAMEWORK_VERSION = '3.0.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';
@@ -42,5 +42,6 @@ export * from './scenarios/index.js';
42
42
  export * from './sim/index.js';
43
43
  export * from './skills/index.js';
44
44
  export * from './social/index.js';
45
+ export { canonicalTickInputs } from './tick-input.js';
45
46
  export * from './types.js';
46
47
  export * from './will/index.js';
package/dist/longrun.d.ts CHANGED
@@ -87,8 +87,8 @@ export interface RoutineDay {
87
87
  * Live one routine day from `p.now` (see SCOPE). A dead person only moves the clock. Returns what happened.
88
88
  */
89
89
  export declare function routineDay(p: Person, routine: Routine, opts?: RoutineOptions): RoutineDay;
90
- /** A routine, or one chosen per day (by season, age, role). */
91
- export type RoutineSource = Routine | ((p: Person, day: number) => Routine);
90
+ /** Choose a routine for a person at the start of an absolute day. Capture host context in a closure. */
91
+ export type RoutineSource = (p: Person, day: number) => Routine;
92
92
  /**
93
93
  * Live from `p.now` to `to` by routine (see SCOPE): the running activity is dropped and the open chronicle day closed
94
94
  * first, whole days are lived with `routineDay`, a remainder shorter than a day is skipped (`skip`), and past
package/dist/longrun.js CHANGED
@@ -181,7 +181,6 @@ export function routineDay(p, routine, opts = {}) {
181
181
  report.stage = stageAfter;
182
182
  return report;
183
183
  }
184
- const routineOf = (src, p) => typeof src === 'function' ? src(p, dayOf(p.now)) : src;
185
184
  /**
186
185
  * Live from `p.now` to `to` by routine (see SCOPE): the running activity is dropped and the open chronicle day closed
187
186
  * first, whole days are lived with `routineDay`, a remainder shorter than a day is skipped (`skip`), and past
@@ -199,7 +198,7 @@ export function liveRoutine(p, to, routine, opts = {}) {
199
198
  endDay(p);
200
199
  p.activity = null;
201
200
  while (p.now + MINUTES_PER_DAY <= to && p.body.alive) {
202
- const r = routineDay(p, routineOf(routine, p), opts);
201
+ const r = routineDay(p, routine(p, dayOf(p.now)), opts);
203
202
  if (r.episodes.length > 0 || r.onsets.length > 0 || r.stage || r.died)
204
203
  out.push(r);
205
204
  }
@@ -139,10 +139,6 @@ export interface RetellOptions {
139
139
  * from the listener to others (a host may call it again with the listener as teller).
140
140
  */
141
141
  export declare function retell(teller: Person, listener: Person, at: Minute, opts?: RetellOptions): Gist[];
142
- /** @deprecated since 2.1: pass `at` (and the optional knobs) positionally. Removed in 3.0. */
143
- export declare function retell(teller: Person, listener: Person, opts: RetellOptions & {
144
- at: Minute;
145
- }): Gist[];
146
142
  /** Encode an episode. Evicts the weakest (salience × recency) episode beyond the bound. */
147
143
  export declare function remember(p: Person, e: Omit<Episode, 'id' | 'salience'> & {
148
144
  salience?: Unit;
@@ -287,8 +287,22 @@ export const RETELL_DEFAULTS = {
287
287
  /** Gists weaker than this (teller's salience) are not worth telling. */
288
288
  minSalience: 0.1,
289
289
  };
290
- export function retell(teller, listener, when, knobs = {}) {
291
- const opts = typeof when === 'number' ? { ...knobs, at: when } : when;
290
+ /**
291
+ * `teller` tells `listener` at `at` the gist of what mattered to them (2.0, L2: stories passed in a household or around a
292
+ * fire). The listener must have gists on (`enableGists`); otherwise nothing happens. The teller's strongest gists
293
+ * (up to `limit`, optionally only those about `placeIds`) become the listener's own gists with the tag `told`, the
294
+ * teller's valence and summary, and a salience of the teller's × `trust` × `RETELL_DEFAULTS.share`. A listener who
295
+ * already holds a gist about the same thing (kind, action, target and place, whoever was there) from their own
296
+ * experience keeps it unchanged; one told before keeps the
297
+ * stronger telling (retelling the same story does not keep raising it). So a child of parents who fear a place
298
+ * weighs that fear (cognition's `memory` term, by place at half weight) before ever standing there, and their own
299
+ * later visits outweigh it as they would any gist. No randomness; returns the gists written or strengthened.
300
+ * It does not model distortion in the retelling, the listener doubting the teller beyond `trust`, or retelling on
301
+ * from the listener to others (a host may call it again with the listener as teller).
302
+ */
303
+ export function retell(teller, listener, at, opts = {}) {
304
+ if (!Number.isFinite(at))
305
+ throw new TypeError('retell requires a finite positional time');
292
306
  const told = teller.memory.gists;
293
307
  const list = listener.memory.gists;
294
308
  if (!told || !list || teller.id === listener.id)
@@ -312,7 +326,7 @@ export function retell(teller, listener, when, knobs = {}) {
312
326
  if (salience > mine.salience) {
313
327
  mine.salience = salience;
314
328
  mine.weight = salience;
315
- mine.lastAt = opts.at;
329
+ mine.lastAt = at;
316
330
  out.push(mine);
317
331
  }
318
332
  continue;
@@ -321,15 +335,15 @@ export function retell(teller, listener, when, knobs = {}) {
321
335
  listener.memory.nextGist = (listener.memory.nextGist ?? 0) + 1;
322
336
  const g = {
323
337
  id,
324
- at: opts.at,
338
+ at,
325
339
  kind: src.kind,
326
340
  valence: src.valence,
327
341
  salience,
328
342
  summary: src.summary,
329
343
  tags: ['told', ...src.tags.filter((t) => t !== 'told')].slice(0, GIST_DEFAULTS.maxTags),
330
344
  count: 0,
331
- firstAt: opts.at,
332
- lastAt: opts.at,
345
+ firstAt: at,
346
+ lastAt: at,
333
347
  weight: salience,
334
348
  peak: salience,
335
349
  };
package/dist/migrate.js CHANGED
@@ -41,6 +41,11 @@ function renameAmbient(json) {
41
41
  }
42
42
  /** The chain, keyed by the version each step upgrades from. */
43
43
  export const MIGRATIONS = {
44
+ '2.21.0': {
45
+ to: '2.23.0',
46
+ note: 'shape unchanged; doze delivery and plain narration fixes apply to opted-in runs',
47
+ apply: stamp,
48
+ },
44
49
  '2.17.0': {
45
50
  to: '2.21.0',
46
51
  note: 'doze and bladder state start absent; old callers retain their continuation',
@@ -332,7 +332,9 @@ export function describePerson(p, lexicon) {
332
332
  ? `${p.activity.action}${p.activity.targetId ? ` (${p.activity.targetId})` : ''}`
333
333
  : phraseLine('describe.idle', {}, lex);
334
334
  if (lex?.plainEnglish) {
335
- const state = bits.length > 0 ? bits.join(', ') : phraseLine('describe.well', {}, lex);
335
+ const state = bits.length > 1
336
+ ? `${bits.slice(0, -1).join(', ')} and ${bits.at(-1)}`
337
+ : (bits[0] ?? phraseLine('describe.well', {}, lex));
336
338
  const activity = p.activity
337
339
  ? `${p.name} is doing this: ${p.activity.affordance.label}${p.activity.targetId ? ` (${nameOf(p, p.activity.targetId, lex)})` : ''}.`
338
340
  : `${p.name} is ${phraseLine('describe.idle', {}, lex)}.`;
@@ -1,6 +1,6 @@
1
1
  /**
2
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,
3
+ * Owns no state or formula. This host composition handles observed-trait correction: direct observation corrects the belief,
4
4
  * recalibrates its sources and applies the credence change to the subject's relationship. It does
5
5
  * not infer opposite traits, accuse a source of lying, run attention or record a percept. Hosts
6
6
  * supply only observations their person has access to. Existing callers remain unchanged.
package/dist/person.d.ts CHANGED
@@ -196,7 +196,11 @@ export interface BeginOptions {
196
196
  /** Joint-activity proposal id (set by `sim/` when both partners begin together). */
197
197
  jointId?: string;
198
198
  }
199
- /** Start an activity chosen by `record`. Returns null when the person is dead. */
199
+ /**
200
+ * Start an activity chosen by `record`. Returns null when dead, or when dozed work
201
+ * is unresolved. Call `finish` for the stopped activity before starting any replacement,
202
+ * including voluntary sleep or an activity chosen after a waking percept.
203
+ */
200
204
  export declare function begin(p: Person, aff: Affordance, record: DecisionRecord, opts?: BeginOptions): Activity | null;
201
205
  /**
202
206
  * Push the review time forward after a decision to continue the current activity, refresh the cached
package/dist/person.js CHANGED
@@ -29,7 +29,8 @@ import { ageCharacter, enableCharacterChange, noteCharacterDay, noteCharacterSoc
29
29
  import { closeDay, enableYearbook, endDay, noteAnswer, noteCommitments, noteDecision, noteMood, noteOutcome, trimChronicle, } from './chronicle/index.js';
30
30
  import { decide as cognitionDecide, desperationOf, prospectiveRoute, scoreAll } from './cognition/index.js';
31
31
  import { createConscience, guiltLoad, heldNorms, normVeto, recordDeed, recordRepair, repent, } from './conscience/index.js';
32
- import { clamp01, clampSigned, createRng, dayOf, isObj, keysOf, minuteOfDay, round } from './core/index.js';
32
+ import { clamp01, clampSigned, createRng, dayOf, isObj, minuteOfDay, round } from './core/index.js';
33
+ import { keysOf } from './core/keys.js';
33
34
  import { enableCuriosity, noteCuriosity } from './curiosity/index.js';
34
35
  import { ambientBodyParams, ambientModifiers, ambientMood, ambientNeeds } from './environment/index.js';
35
36
  import { aptitudeOf, createFamily, pregnancyModifiers } from './family/index.js';
@@ -865,17 +866,18 @@ function judgeDeed(p, actor, pc, at) {
865
866
  export function perceive(p, percepts, options = {}) {
866
867
  if (percepts.length === 0 || !p.body.alive)
867
868
  return [];
868
- if (p.body.doze?.since !== undefined) {
869
+ const dozeSince = p.body.doze?.since;
870
+ if (dozeSince !== undefined) {
869
871
  const wakes = percepts.some((pc) => Number.isFinite(pc.at) &&
870
- pc.at === p.now &&
872
+ pc.at >= dozeSince &&
873
+ pc.at <= p.now &&
871
874
  Number.isFinite(pc.salience) &&
872
875
  pc.salience >= DOZE_DEFAULTS.wakeSalience &&
873
876
  pc.near !== false &&
874
877
  (pc.targetId === p.id || pc.near === true) &&
875
878
  (pc.channel === 'heard' || pc.channel === 'felt' || pc.channel === 'social'));
876
- if (!wakes)
877
- return [];
878
- wakeDoze(p, p.now);
879
+ if (wakes)
880
+ wakeDoze(p, p.now);
879
881
  }
880
882
  const body = readBodyOf(p);
881
883
  const attended = attend(p, [...percepts], {
@@ -1170,9 +1172,13 @@ function thresholdFor(p, load, review) {
1170
1172
  const t = nextBodyThreshold(p, load, segmentModifiers(p), PERSON_DEFAULTS.interruptThresholds, review, p.ambient ? ambientBodyParams(p, BODY_DEFAULTS) : BODY_DEFAULTS, fastingCtx(p));
1171
1173
  return Number.isFinite(t) ? p.now + t : undefined;
1172
1174
  }
1173
- /** Start an activity chosen by `record`. Returns null when the person is dead. */
1175
+ /**
1176
+ * Start an activity chosen by `record`. Returns null when dead, or when dozed work
1177
+ * is unresolved. Call `finish` for the stopped activity before starting any replacement,
1178
+ * including voluntary sleep or an activity chosen after a waking percept.
1179
+ */
1174
1180
  export function begin(p, aff, record, opts = {}) {
1175
- if (!p.body.alive)
1181
+ if (!p.body.alive || p.activity?.dozedAt !== undefined)
1176
1182
  return null;
1177
1183
  if (p.body.doze?.since !== undefined && aff.mode !== 'sleep')
1178
1184
  return null;
@@ -1385,7 +1391,7 @@ function respondToConscience(p, act, now, completed, minutes) {
1385
1391
  // Worship closes breaches against God alone; guilt toward a wronged person waits for repair.
1386
1392
  const repented = new Set();
1387
1393
  for (const b of p.conscience.breaches) {
1388
- if (!b.repaired && b.victimId === undefined && repent(p, b.id, now))
1394
+ if (!b.repaired && b.victimId === undefined && repent(p, b.id))
1389
1395
  repented.add(b.normId);
1390
1396
  }
1391
1397
  for (const b of p.conscience.breaches)
@@ -1396,7 +1402,7 @@ function respondToConscience(p, act, now, completed, minutes) {
1396
1402
  regulate(p, minutes, 'worship');
1397
1403
  }
1398
1404
  if (completed && (tags.includes('repair') || tags.includes('apologize')) && aff.targetId !== undefined) {
1399
- recordRepair(p, aff.targetId, now);
1405
+ recordRepair(p, aff.targetId);
1400
1406
  release(p, { id: 'guilt', targetId: aff.targetId });
1401
1407
  socialEvent(p, { at: now, kind: 'apology', otherId: aff.targetId, byMe: true, magnitude: 0.7 });
1402
1408
  }
package/dist/persona.js CHANGED
@@ -188,6 +188,7 @@ export function validatePersonaProjection(input) {
188
188
  keys(input.source, ['schema', 'engine', 'id', 'now']);
189
189
  // The 2.14.1 and 2.17.0 portable fields retain their semantics across the bodily opt-ins.
190
190
  const compatibleEngine = input.source.engine === ENGINE_VERSION ||
191
+ input.source.engine === '2.21.0' ||
191
192
  input.source.engine === '2.17.0' ||
192
193
  input.source.engine === '2.14.1';
193
194
  if (input.source.schema !== PERSON_SCHEMA || !compatibleEngine)
@@ -68,6 +68,7 @@ export function reasonLine(reason, lexicon) {
68
68
  'need:water': 'I need to drink first.',
69
69
  'need:sleep': 'I need to sleep first.',
70
70
  'need:rest': 'I need to rest first.',
71
+ 'need:bladder': 'I need to use the toilet first.',
71
72
  'need:survival': 'I need to meet an urgent bodily need first.',
72
73
  };
73
74
  if (Object.hasOwn(lines, reason))
@@ -360,7 +360,7 @@ export function nafsPatterns(p, inputs) {
360
360
  ref: 'Qur’an 12:53',
361
361
  evident: assentedAgainstNorm.length > 0,
362
362
  evidence: p.lexicon?.plainEnglish
363
- ? `The person acted on ${assentedAgainstNorm.length} recent urges despite a rule they hold.`
363
+ ? `The person acted on ${assentedAgainstNorm.length} recent ${assentedAgainstNorm.length === 1 ? 'urge' : 'urges'} despite a rule they hold.`
364
364
  : `${assentedAgainstNorm.length} recent prompts followed against a held norm`,
365
365
  },
366
366
  {
@@ -376,7 +376,7 @@ export function nafsPatterns(p, inputs) {
376
376
  ref: 'Qur’an 89:27',
377
377
  evident: letPass > 0 && assentedAgainstNorm.length === 0 && !reproach,
378
378
  evidence: p.lexicon?.plainEnglish
379
- ? `${letPass} recent urges passed without action. ${assentedAgainstNorm.length} were acted on against a held rule. Current guilt or unresolved breach: ${reproach ? 'yes' : 'no'}.`
379
+ ? `${letPass} recent ${letPass === 1 ? 'urge' : 'urges'} passed without action. ${assentedAgainstNorm.length} ${assentedAgainstNorm.length === 1 ? 'was' : 'were'} acted on against a held rule. Current guilt or unresolved breach: ${reproach ? 'yes' : 'no'}.`
380
380
  : `${letPass} recent prompts let pass, none followed against a norm, no felt reproach`,
381
381
  },
382
382
  ];
package/dist/qalb.d.ts CHANGED
@@ -1,15 +1,3 @@
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
1
  import type { Person } from './types.ts';
14
2
  export interface QalbDescription {
15
3
  facet: 'attention' | 'held-norm' | 'mood' | 'trust';
package/dist/qalb.js CHANGED
@@ -1,3 +1,18 @@
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 { DEFAULT_NORMS } from './conscience/catalog.js';
14
+ import { nameOf } from './narrate/lexicon.js';
15
+ import { normLabel } from './presentation.js';
1
16
  /**
2
17
  * Plain English descriptions; no scalar summary, comparison or hidden update.
3
18
  * Hosts must not present these records as spiritual health or a religious assessment.
@@ -19,18 +34,17 @@ export function qalbReading(p) {
19
34
  if (!p.conscience.norms.length)
20
35
  add('held-norm', 'unrecorded', 'Their records contain no held rules.', ['conscience.norms']);
21
36
  for (const n of p.conscience.norms) {
37
+ const rule = normLabel(DEFAULT_NORMS.find((rule) => rule.id === n.normId) ?? { id: n.normId, label: n.normId }, { ...p.lexicon, plainEnglish: true });
22
38
  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']);
39
+ ? `They have a recorded rule, “${rule}”, with no stated conviction.`
40
+ : `They hold an understanding of the rule “${rule}”. This is their view.`, [`norm:${n.normId}`, `standing:${n.standing}`, 'conscience.norms.conviction']);
25
41
  }
26
42
  add('mood', p.affect.mood.valence < 0 ? 'unpleasant' : p.affect.mood.valence > 0 ? 'pleasant' : 'neutral', p.affect.mood.valence < 0
27
43
  ? 'Their recorded mood feels unpleasant.'
28
44
  : p.affect.mood.valence > 0
29
45
  ? 'Their recorded mood feels pleasant.'
30
46
  : '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']);
47
+ add('mood', p.affect.mood.arousal === 0 ? 'no-arousal' : 'some-arousal', p.affect.mood.arousal === 0 ? 'Their recorded mood is calm.' : 'Their recorded mood is stirred.', ['affect.mood.arousal']);
34
48
  // Keep stores distinct. Do not substitute relationship trust for source or advising-voice trust.
35
49
  const ties = p.social.relationships.map((r) => ({
36
50
  store: 'social.relationships',
@@ -53,10 +67,10 @@ export function qalbReading(p) {
53
67
  for (const t of trusts) {
54
68
  // 0.5 is the numeric range midpoint, not a faith threshold or personal baseline.
55
69
  const subject = t.store === 'social.relationships'
56
- ? 'this person'
70
+ ? `the person ${nameOf(p, t.id)}`
57
71
  : t.store === 'will.voices'
58
- ? 'this advising voice'
59
- : 'this information source';
72
+ ? `the advising voice ${nameOf(p, t.id)}`
73
+ : `the information source ${nameOf(p, t.id)}`;
60
74
  add('trust', t.trust < 0.5 ? 'below-midpoint' : t.trust > 0.5 ? 'above-midpoint' : 'at-midpoint', t.trust < 0.5
61
75
  ? `Their trust in ${subject} is below the middle of its range.`
62
76
  : t.trust > 0.5
@@ -15,9 +15,9 @@
15
15
  * since a routine day writes only its own person. Births and arrivals are the host's (`birth`, `addPerson` in
16
16
  * `onDay`); a person added during the stretch starts on the next day.
17
17
  */
18
- import { type Routine, type RoutineDay } from '../longrun.ts';
18
+ import { type RoutineDay, type RoutineSource } from '../longrun.ts';
19
19
  import type { SkillTransfer } from '../skills/index.ts';
20
- import type { Minute, Person, PersonId } from '../types.ts';
20
+ import type { Minute, PersonId } from '../types.ts';
21
21
  import { type Community, type LifecourseOptions, type SimEvent } from './sim.ts';
22
22
  export declare const LIVE_COMMUNITY_DEFAULTS: {
23
23
  /** Upbringing time a day with the parents (engineering assumption: a few waking hours together). */
@@ -25,7 +25,7 @@ export declare const LIVE_COMMUNITY_DEFAULTS: {
25
25
  };
26
26
  export interface LiveCommunityOptions {
27
27
  /** Each living person's routine for a day (`day` is the day number being lived). */
28
- routineFor(p: Person, day: number, c: Community): Routine;
28
+ routineFor: RoutineSource;
29
29
  /** At each dawn, before anyone's day: births, arrivals, routine changes. */
30
30
  onDay?(c: Community, day: number, at: Minute): void;
31
31
  /** Tell each death to everyone with a tie (default true; see `tellDeath`). */
@@ -70,7 +70,7 @@ export function liveCommunity(c, until, opts) {
70
70
  tick(p, now);
71
71
  if (p.now !== now)
72
72
  continue; // added mid-stretch with a later clock: joins when the day catches up
73
- const r = routineDay(p, opts.routineFor(p, day, c), routineOpts);
73
+ const r = routineDay(p, opts.routineFor(p, day), routineOpts);
74
74
  if (r.episodes.length > 0 || r.onsets.length > 0 || r.stage || r.died) {
75
75
  const list = days[p.id] ?? [];
76
76
  list.push(r);
package/dist/sim/sim.js CHANGED
@@ -840,10 +840,15 @@ export function stepCommunity(c, world, until, opts = {}) {
840
840
  at: t,
841
841
  personId: p.id,
842
842
  kind: 'finish',
843
- detail: `${act.action} interrupted by doze`,
843
+ detail: outcome.summary ??
844
+ (p.lexicon?.plainEnglish
845
+ ? `${p.name} stopped the task “${act.affordance.label}” because they dozed off.`
846
+ : `${act.action} interrupted by doze`),
847
+ decisionId: act.decisionId,
848
+ affordanceId: act.affordanceId,
844
849
  action: act.action,
845
850
  status: outcome.status,
846
- });
851
+ }, outcome.summary !== undefined);
847
852
  }
848
853
  c.idleUntil[p.id] = t + quantum;
849
854
  if (!ordinaryDue)
@@ -995,12 +1000,12 @@ export function stepCommunity(c, world, until, opts = {}) {
995
1000
  detail: p.lexicon?.plainEnglish
996
1001
  ? proposal.status === 'declined'
997
1002
  ? `The shared task “${chosen.label}” could not start because a partner was unavailable.`
998
- : `${chosen.label} with ${proposal.partnerIds.map((id) => personById(c, id)?.name ?? id).join(', ')}`
1003
+ : `${p.name} proposed the shared task “${chosen.label}” with ${proposal.partnerIds.map((id) => personById(c, id)?.name ?? id).join(', ')}.`
999
1004
  : `${chosen.label} with ${proposal.partnerIds.join(', ')}`,
1000
1005
  affordanceId: chosen.id,
1001
1006
  action: chosen.action,
1002
1007
  jointId: proposal.id,
1003
- }, Boolean(p.lexicon?.plainEnglish && proposal.status === 'declined'));
1008
+ }, Boolean(p.lexicon?.plainEnglish));
1004
1009
  continue;
1005
1010
  }
1006
1011
  if (act) {
@@ -1,8 +1,6 @@
1
1
  import type { EntityId, Minute, NormTag, Person, PersonId, Signed, StateCue, Traits, Unit } from '../types.ts';
2
2
  export type CueKind = 'state' | 'place' | 'trait' | 'tie' | 'trust' | 'skill' | 'regard';
3
3
  export declare const STATE_CUES: readonly StateCue[];
4
- /** @deprecated since 2.1: use `TRAIT_KEYS` (types.ts), the same list. Removed in 3.0. */
5
- export declare const TRAIT_NAMES: readonly (keyof Traits)[];
6
4
  export declare const IMPRESSION_DEFAULTS: {
7
5
  /** Weight half-life by kind, minutes (0 = no decay). */
8
6
  halfLife: Record<CueKind, number>;
@@ -88,20 +86,10 @@ export declare function setReserve(p: Person, reserve: Partial<Record<StateCue,
88
86
  * the evidence and how true it is (a figure in the dark tells little and tells it wrong).
89
87
  */
90
88
  export declare function glimpse(observer: Person, targetId: PersonId, signs: OutwardSigns, at: Minute, clarity?: Unit): void;
91
- /** @deprecated since 2.1: pass `at` (and the optional knobs) positionally. Removed in 3.0. */
92
- export declare function glimpse(observer: Person, targetId: PersonId, signs: OutwardSigns, opts: {
93
- at: Minute;
94
- clarity?: Unit;
95
- }): void;
96
89
  /** What someone was seen to do: place fear, ties, trust in a voice and trait evidence (see `ObservedAct`). */
97
90
  export declare function observeAct(observer: Person, targetId: PersonId, act: ObservedAct, table?: Record<string, Partial<Record<keyof Traits, Unit>>>): void;
98
91
  /** Testimony about someone (their own words, or another's): `value` on `key`, weighted (e.g. by trust in the teller). */
99
92
  export declare function hear(observer: Person, targetId: PersonId, key: string, value: number, at: Minute, weight?: Unit): void;
100
- /** @deprecated since 2.1: pass `at` (and the optional knobs) positionally. Removed in 3.0. */
101
- export declare function hear(observer: Person, targetId: PersonId, key: string, value: number, opts: {
102
- at: Minute;
103
- weight?: Unit;
104
- }): void;
105
93
  /**
106
94
  * Years of history condensed (host call, e.g. at the start of a run for people who already know each other): one
107
95
  * sample per trait from `traits` with an error that shrinks with familiarity, at weight
@@ -30,8 +30,6 @@ import { clamp, clamp01, cmpStr, decay, latestPerId, newestN, round } from '../c
30
30
  import { MINUTES_PER_DAY, MINUTES_PER_YEAR, TRAIT_KEYS } from '../types.js';
31
31
  import { judge, SOCIAL_DEFAULTS } from './social.js';
32
32
  export const STATE_CUES = ['fatigue', 'pain', 'fear', 'mood'];
33
- /** @deprecated since 2.1: use `TRAIT_KEYS` (types.ts), the same list. Removed in 3.0. */
34
- export const TRAIT_NAMES = TRAIT_KEYS;
35
33
  export const IMPRESSION_DEFAULTS = {
36
34
  /** Weight half-life by kind, minutes (0 = no decay). */
37
35
  halfLife: {
@@ -208,11 +206,16 @@ function erred(observer, targetId, key, at, value, clarity) {
208
206
  const e = (hash01(`${observer.id}|${targetId}|${key}|${at}`) - 0.5) * IMPRESSION_DEFAULTS.noise;
209
207
  return value + e * (1 - clamp01(clarity));
210
208
  }
211
- export function glimpse(observer, targetId, signs, when, clarity) {
209
+ /**
210
+ * A look at someone's outward signs (from the composite's `outwardSigns`). `clarity` 0..1 scales both the weight of
211
+ * the evidence and how true it is (a figure in the dark tells little and tells it wrong).
212
+ */
213
+ export function glimpse(observer, targetId, signs, at, clarity) {
214
+ if (!Number.isFinite(at))
215
+ throw new TypeError('glimpse requires a finite positional time');
212
216
  if (targetId === observer.id)
213
217
  return;
214
- const at = typeof when === 'number' ? when : when.at;
215
- const clear = clamp01((typeof when === 'number' ? clarity : when.clarity) ?? 1);
218
+ const clear = clamp01(clarity ?? 1);
216
219
  if (clear <= 0)
217
220
  return;
218
221
  const imp = impressionFor(observer, targetId, true, at);
@@ -252,11 +255,13 @@ export function observeAct(observer, targetId, act, table = IMPRESSION_DEFAULTS.
252
255
  }
253
256
  }
254
257
  }
255
- export function hear(observer, targetId, key, value, when, weight) {
258
+ /** Testimony about someone (their own words, or another's): `value` on `key`, weighted (e.g. by trust in the teller). */
259
+ export function hear(observer, targetId, key, value, at, weight) {
260
+ if (!Number.isFinite(at))
261
+ throw new TypeError('hear requires a finite positional time');
256
262
  if (targetId === observer.id)
257
263
  return;
258
- const at = typeof when === 'number' ? when : when.at;
259
- const w = clamp01((typeof when === 'number' ? weight : when.weight) ?? 0.5);
264
+ const w = clamp01(weight ?? 0.5);
260
265
  if (w <= 0)
261
266
  return;
262
267
  const imp = impressionFor(observer, targetId, true, at);
@@ -12,7 +12,7 @@
12
12
  * further down; insiders, outsiders and threats in `groups.ts`); the
13
13
  * coefficients are engineering defaults, not calibrated estimates.
14
14
  */
15
- import { clamp, clamp01, clampSigned, cmpStr, decay, dlog, expit, round } from '../core/index.js';
15
+ import { clamp, clamp01, clampSigned, cmpStr, decay, dlog, round, sigmoid } from '../core/index.js';
16
16
  import { MINUTES_PER_DAY, MINUTES_PER_YEAR } from '../types.js';
17
17
  import { careFor } from './groups.js';
18
18
  import { regardOf } from './impressions.js';
@@ -554,7 +554,7 @@ export function reputation(people, targetId, table = REPUTATION_TRAITS) {
554
554
  continue;
555
555
  knows = true;
556
556
  const slot = traitSum[parsed.trait] ?? { sum: 0, n: 0 };
557
- slot.sum += expit(b.logOdds);
557
+ slot.sum += sigmoid(b.logOdds);
558
558
  slot.n += 1;
559
559
  traitSum[parsed.trait] = slot;
560
560
  }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * SCOPE: opt-in host input ordering for one declared logical tick. Waits for the
3
+ * caller's complete expected identity set, then sorts by sender and sequence.
4
+ * Lexical sender priority is deterministic host policy, not fairness or
5
+ * consensus. For conflicting same-voice requests, earlier senders take priority.
6
+ * Owns no Person, clock, queue or RNG state. Does not supply transport, consensus,
7
+ * payload validation, retries or snapshot storage. Payloads remain caller-owned;
8
+ * hosts must agree on their meaning and save pending input alongside host state.
9
+ */
10
+ export interface TickInputKey {
11
+ sender: string;
12
+ sequence: number;
13
+ }
14
+ export interface TickInput<T> extends TickInputKey {
15
+ tick: number;
16
+ payload: T;
17
+ }
18
+ /**
19
+ * Returns undefined while a declared input is missing. Unexpected, duplicate or
20
+ * wrong-tick inputs throw. An empty expected set is a complete barrier. This
21
+ * reader never mutates input arrays or envelopes and never reads payload fields.
22
+ * Call only when explicitly opting into this host scheduling convention.
23
+ */
24
+ export declare function canonicalTickInputs<T>(tick: number, inputs: readonly TickInput<T>[], expected: readonly TickInputKey[]): TickInput<T>[] | undefined;
@@ -0,0 +1,56 @@
1
+ const MAX_INPUTS = 256;
2
+ function identity(key) {
3
+ if (typeof key.sender !== 'string' ||
4
+ key.sender.length === 0 ||
5
+ key.sender.length > 128 ||
6
+ !Number.isSafeInteger(key.sequence) ||
7
+ key.sequence < 0) {
8
+ throw new Error('Invalid tick input identity');
9
+ }
10
+ return JSON.stringify([key.sender, key.sequence]);
11
+ }
12
+ /**
13
+ * Returns undefined while a declared input is missing. Unexpected, duplicate or
14
+ * wrong-tick inputs throw. An empty expected set is a complete barrier. This
15
+ * reader never mutates input arrays or envelopes and never reads payload fields.
16
+ * Call only when explicitly opting into this host scheduling convention.
17
+ */
18
+ export function canonicalTickInputs(tick, inputs, expected) {
19
+ if (!Number.isSafeInteger(tick) || tick < 0)
20
+ throw new Error('Invalid logical tick');
21
+ if (inputs.length > MAX_INPUTS || expected.length > MAX_INPUTS) {
22
+ throw new Error('Tick input limit exceeded');
23
+ }
24
+ const wanted = new Set();
25
+ for (const key of expected) {
26
+ const id = identity(key);
27
+ if (wanted.has(id))
28
+ throw new Error('Duplicate expected tick input');
29
+ wanted.add(id);
30
+ }
31
+ const received = new Set();
32
+ for (const input of inputs) {
33
+ const id = identity(input);
34
+ if (input.tick !== tick)
35
+ throw new Error('Input belongs to another tick');
36
+ if (!wanted.has(id))
37
+ throw new Error('Unexpected tick input');
38
+ if (received.has(id))
39
+ throw new Error('Duplicate tick input');
40
+ received.add(id);
41
+ }
42
+ if (received.size !== wanted.size)
43
+ return undefined;
44
+ return inputs
45
+ .map((input) => ({
46
+ tick: input.tick,
47
+ sender: input.sender,
48
+ sequence: input.sequence,
49
+ payload: input.payload,
50
+ }))
51
+ .sort((a, b) => {
52
+ if (a.sender !== b.sender)
53
+ return a.sender < b.sender ? -1 : 1;
54
+ return a.sequence - b.sequence;
55
+ });
56
+ }