@adam0white/human-framework 2.8.0 → 2.9.1
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/dist/agenda/agenda.d.ts +9 -1
- package/dist/agenda/agenda.js +37 -16
- package/dist/beliefs/beliefs.d.ts +22 -0
- package/dist/beliefs/beliefs.js +51 -0
- package/dist/cognition/cognition.js +8 -6
- package/dist/conscience/conscience.d.ts +7 -3
- package/dist/conscience/conscience.js +5 -1
- package/dist/core/math.d.ts +1 -0
- package/dist/core/math.js +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/migrate.js +16 -1
- package/dist/person.js +121 -98
- package/dist/promptings/promptings.d.ts +15 -2
- package/dist/promptings/promptings.js +82 -26
- package/dist/prospection/prospection.js +8 -2
- package/dist/restore.d.ts +9 -0
- package/dist/restore.js +20 -0
- package/dist/review/review.js +16 -2
- package/dist/sim/sim.d.ts +7 -2
- package/dist/sim/sim.js +118 -93
- package/dist/types.d.ts +41 -5
- package/dist/types.js +1 -1
- package/dist/will/will.js +94 -65
- package/package.json +1 -1
package/dist/sim/sim.js
CHANGED
|
@@ -125,6 +125,7 @@ export function communityState(c) {
|
|
|
125
125
|
/**
|
|
126
126
|
* A community over `people`. With `prior` (from `communityState`, possibly through JSON), the host-side state is
|
|
127
127
|
* resumed from it, so a run restored from people + community + world state continues exactly as it would have.
|
|
128
|
+
* `prior` is copied: after stepping, write back `communityState(c)`; the old `prior` stays stale.
|
|
128
129
|
*/
|
|
129
130
|
export function createCommunity(people, prior) {
|
|
130
131
|
const sorted = [...people].sort((a, b) => cmpStr(a.id, b.id));
|
|
@@ -181,9 +182,19 @@ export function birth(c, a, b, spec) {
|
|
|
181
182
|
/**
|
|
182
183
|
* Tell `dead`'s death at `at` to everyone alive in the community who has a tie to them (1.8.0): a told `death`
|
|
183
184
|
* percept through `perceive`, so they grieve, the tie is kept and marked, and a spouse is widowed. Each listener's
|
|
184
|
-
*
|
|
185
|
+
* receipt time is at least their own `now`. `late: 'now'` explicitly permits listeners past `at`, as does the
|
|
186
|
+
* existing default. `late: 'throw'` requires all eligible listeners to be at or before `at` and throws before
|
|
187
|
+
* delivering any news if one is ahead. This changes no listener's clock.
|
|
185
188
|
*/
|
|
186
|
-
export function tellDeath(c, dead, at) {
|
|
189
|
+
export function tellDeath(c, dead, at, opts = {}) {
|
|
190
|
+
if (opts.late === 'throw') {
|
|
191
|
+
for (const q of c.people) {
|
|
192
|
+
if (q === dead || !q.body.alive || q.now <= at)
|
|
193
|
+
continue;
|
|
194
|
+
if (q.social.relationships.some((r) => r.otherId === dead.id))
|
|
195
|
+
throw new Error(`tellDeath: listener ${q.id} is past the death minute`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
187
198
|
for (const q of c.people) {
|
|
188
199
|
if (q === dead || !q.body.alive)
|
|
189
200
|
continue;
|
|
@@ -572,24 +583,8 @@ function contagionBetween(p, q, act, t, log) {
|
|
|
572
583
|
}
|
|
573
584
|
}
|
|
574
585
|
}
|
|
575
|
-
/**
|
|
576
|
-
|
|
577
|
-
*/
|
|
578
|
-
export function stepCommunity(c, world, until, opts = {}) {
|
|
579
|
-
const events = [];
|
|
580
|
-
const quantum = Math.max(1, opts.idleQuantum ?? SIM_DEFAULTS.idleQuantum);
|
|
581
|
-
const maxEvents = opts.maxEvents ?? SIM_DEFAULTS.maxEvents;
|
|
582
|
-
const salience = opts.interruptSalience ?? SIM_DEFAULTS.interruptSalience;
|
|
583
|
-
// Communities created before joint support (plain JSON from an older host) get the new bookkeeping.
|
|
584
|
-
c.interrupts ??= {};
|
|
585
|
-
c.joint ??= { nextId: 0, proposals: [], excluded: {} };
|
|
586
|
-
const log = (e) => {
|
|
587
|
-
if (events.length < maxEvents)
|
|
588
|
-
events.push(e);
|
|
589
|
-
};
|
|
590
|
-
const contagion = opts.contagion ?? true;
|
|
591
|
-
c.queued ??= {};
|
|
592
|
-
c.dayDone ??= {};
|
|
586
|
+
/** Synchronize the host's commands before selecting the next event. */
|
|
587
|
+
function synchronizeCommands(c, opts, log) {
|
|
593
588
|
if (opts.controlled) {
|
|
594
589
|
for (const p of c.people) {
|
|
595
590
|
const cmd = opts.controlled[p.id];
|
|
@@ -614,36 +609,108 @@ export function stepCommunity(c, world, until, opts = {}) {
|
|
|
614
609
|
}
|
|
615
610
|
}
|
|
616
611
|
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
612
|
+
}
|
|
613
|
+
/** Run life-course bookkeeping and the host's day hook at the first event of a new day. */
|
|
614
|
+
function bookkeepDay(p, world, t, dayDone, opts) {
|
|
615
|
+
// First event of a new day: life-course rolls for the days crossed, then the host's day hook.
|
|
616
|
+
const day = dayOf(t);
|
|
617
|
+
const lastDay = dayDone[p.id];
|
|
618
|
+
if (lastDay === undefined || day > lastDay) {
|
|
619
|
+
dayDone[p.id] = day;
|
|
620
|
+
const lc = opts.lifecourse;
|
|
621
|
+
if (lc && lastDay !== undefined && p.body.alive) {
|
|
622
|
+
const dt = (day - lastDay) * MINUTES_PER_DAY;
|
|
623
|
+
if (lc.chronicOnsets) {
|
|
624
|
+
const onsetOpts = {};
|
|
625
|
+
if (lc.conditions)
|
|
626
|
+
onsetOpts.conditions = lc.conditions;
|
|
627
|
+
const ex = lc.exposures?.(p);
|
|
628
|
+
if (ex)
|
|
629
|
+
onsetOpts.exposures = ex;
|
|
630
|
+
for (const ill of chronicOnsets(p, dt, p.rng, onsetOpts))
|
|
631
|
+
sicken(p, ill);
|
|
632
|
+
}
|
|
633
|
+
if (lc.mortality) {
|
|
634
|
+
const roll = mortalityEvent(p, dt, p.rng, lc.multiplier !== undefined ? { multiplier: lc.multiplier } : {});
|
|
635
|
+
if (roll.died)
|
|
636
|
+
die(p);
|
|
637
|
+
}
|
|
631
638
|
}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
639
|
+
if (p.body.alive)
|
|
640
|
+
world.onDay?.(p, day);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
/** Merge heard advice and the host's decision options in their existing precedence order. */
|
|
644
|
+
function decisionOptions(c, world, p, opts, reason, affordances) {
|
|
645
|
+
const o = {};
|
|
646
|
+
// Voices: queued conversation advice first (fresher), then the host's standing suggestions that are heard now
|
|
647
|
+
// (standing-advice SCOPE); one per voice. At reviews they still weigh in, but `decide` resolves them quietly
|
|
648
|
+
// (no counters, pressure or autonomy cost), so one request is not re-counted as many verdicts.
|
|
649
|
+
const standing = opts.suggestions?.[p.id];
|
|
650
|
+
const merged = [];
|
|
651
|
+
const seen = new Set();
|
|
652
|
+
const heard = (standing ? (Array.isArray(standing) ? standing : [standing]) : []).filter((s) => standingHeard(c, p, s, affordances, p.now));
|
|
653
|
+
for (const s of [...(c.queued?.[p.id] ?? []), ...heard]) {
|
|
654
|
+
if (seen.has(s.voiceId))
|
|
655
|
+
continue;
|
|
656
|
+
seen.add(s.voiceId);
|
|
657
|
+
merged.push(s);
|
|
658
|
+
}
|
|
659
|
+
const [first, ...rest] = merged;
|
|
660
|
+
if (first)
|
|
661
|
+
o.suggestion = first;
|
|
662
|
+
if (rest.length > 0)
|
|
663
|
+
o.suggestions = rest;
|
|
664
|
+
const scarcity = world.scarcityFor?.(p);
|
|
665
|
+
if (scarcity !== undefined && scarcity > 0)
|
|
666
|
+
o.scarcity = scarcity;
|
|
667
|
+
if (opts.necessity !== undefined)
|
|
668
|
+
o.necessity = opts.necessity;
|
|
669
|
+
const forced = reason ?? c.interrupts[p.id]?.reason;
|
|
670
|
+
if (forced !== undefined && !p.activity)
|
|
671
|
+
o.interrupt = forced;
|
|
672
|
+
return o;
|
|
673
|
+
}
|
|
674
|
+
/** Add pending joint mirrors to world offers and apply this minute's declined-offer exclusions. */
|
|
675
|
+
function offersForDecision(c, world, p, t) {
|
|
676
|
+
// Offers: the world's, plus mirrors of joint proposals awaiting this person's answer, minus offers a partner
|
|
677
|
+
// just declined (this minute only).
|
|
678
|
+
const asked = pendingProposals(c).filter((x) => x.partnerIds.includes(p.id) && !x.accepted.includes(p.id));
|
|
679
|
+
const mirrors = new Map();
|
|
680
|
+
let affordances = world.affordancesFor(p);
|
|
681
|
+
for (const x of asked) {
|
|
682
|
+
const m = mirrorFor(world, p, x);
|
|
683
|
+
mirrors.set(m.id, x);
|
|
684
|
+
affordances = [...affordances.filter((a) => a.id !== m.id), m];
|
|
685
|
+
}
|
|
686
|
+
const ex = c.joint.excluded[p.id];
|
|
687
|
+
if (ex) {
|
|
688
|
+
if (ex.at === t)
|
|
689
|
+
affordances = affordances.filter((a) => !ex.ids.includes(a.id));
|
|
690
|
+
else
|
|
691
|
+
delete c.joint.excluded[p.id];
|
|
692
|
+
}
|
|
693
|
+
return { asked, mirrors, affordances };
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Advance the community to `until`, processing person events in (time, id) order. Returns the events.
|
|
697
|
+
*/
|
|
698
|
+
export function stepCommunity(c, world, until, opts = {}) {
|
|
699
|
+
const events = [];
|
|
700
|
+
const quantum = Math.max(1, opts.idleQuantum ?? SIM_DEFAULTS.idleQuantum);
|
|
701
|
+
const maxEvents = opts.maxEvents ?? SIM_DEFAULTS.maxEvents;
|
|
702
|
+
const salience = opts.interruptSalience ?? SIM_DEFAULTS.interruptSalience;
|
|
703
|
+
// Communities created before joint support (plain JSON from an older host) get the new bookkeeping.
|
|
704
|
+
c.interrupts ??= {};
|
|
705
|
+
c.joint ??= { nextId: 0, proposals: [], excluded: {} };
|
|
706
|
+
const log = (e) => {
|
|
707
|
+
if (events.length < maxEvents)
|
|
708
|
+
events.push(e);
|
|
646
709
|
};
|
|
710
|
+
const contagion = opts.contagion ?? true;
|
|
711
|
+
c.queued ??= {};
|
|
712
|
+
c.dayDone ??= {};
|
|
713
|
+
synchronizeCommands(c, opts, log);
|
|
647
714
|
// Pairs whose shared activity already ran its conversation and contagion at `pairMinute` (both partners
|
|
648
715
|
// finishing the same joint activity at the same minute would otherwise run them twice).
|
|
649
716
|
let pairMinute = Number.NEGATIVE_INFINITY;
|
|
@@ -662,33 +729,7 @@ export function stepCommunity(c, world, until, opts = {}) {
|
|
|
662
729
|
break;
|
|
663
730
|
const p = next;
|
|
664
731
|
tick(p, t);
|
|
665
|
-
|
|
666
|
-
const day = dayOf(t);
|
|
667
|
-
const lastDay = c.dayDone[p.id];
|
|
668
|
-
if (lastDay === undefined || day > lastDay) {
|
|
669
|
-
c.dayDone[p.id] = day;
|
|
670
|
-
const lc = opts.lifecourse;
|
|
671
|
-
if (lc && lastDay !== undefined && p.body.alive) {
|
|
672
|
-
const dt = (day - lastDay) * MINUTES_PER_DAY;
|
|
673
|
-
if (lc.chronicOnsets) {
|
|
674
|
-
const onsetOpts = {};
|
|
675
|
-
if (lc.conditions)
|
|
676
|
-
onsetOpts.conditions = lc.conditions;
|
|
677
|
-
const ex = lc.exposures?.(p);
|
|
678
|
-
if (ex)
|
|
679
|
-
onsetOpts.exposures = ex;
|
|
680
|
-
for (const ill of chronicOnsets(p, dt, p.rng, onsetOpts))
|
|
681
|
-
sicken(p, ill);
|
|
682
|
-
}
|
|
683
|
-
if (lc.mortality) {
|
|
684
|
-
const roll = mortalityEvent(p, dt, p.rng, lc.multiplier !== undefined ? { multiplier: lc.multiplier } : {});
|
|
685
|
-
if (roll.died)
|
|
686
|
-
die(p);
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
if (p.body.alive)
|
|
690
|
-
world.onDay?.(p, day);
|
|
691
|
-
}
|
|
732
|
+
bookkeepDay(p, world, t, c.dayDone, opts);
|
|
692
733
|
if (!p.body.alive) {
|
|
693
734
|
log({ at: t, personId: p.id, kind: 'died', detail: `${p.name} died` });
|
|
694
735
|
if (opts.tellDeaths ?? true)
|
|
@@ -758,24 +799,8 @@ export function stepCommunity(c, world, until, opts = {}) {
|
|
|
758
799
|
}
|
|
759
800
|
act = p.activity;
|
|
760
801
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
const asked = pendingProposals(c).filter((x) => x.partnerIds.includes(p.id) && !x.accepted.includes(p.id));
|
|
764
|
-
const mirrors = new Map();
|
|
765
|
-
let affordances = world.affordancesFor(p);
|
|
766
|
-
for (const x of asked) {
|
|
767
|
-
const m = mirrorFor(world, p, x);
|
|
768
|
-
mirrors.set(m.id, x);
|
|
769
|
-
affordances = [...affordances.filter((a) => a.id !== m.id), m];
|
|
770
|
-
}
|
|
771
|
-
const ex = c.joint.excluded[p.id];
|
|
772
|
-
if (ex) {
|
|
773
|
-
if (ex.at === t)
|
|
774
|
-
affordances = affordances.filter((a) => !ex.ids.includes(a.id));
|
|
775
|
-
else
|
|
776
|
-
delete c.joint.excluded[p.id];
|
|
777
|
-
}
|
|
778
|
-
const record = decide(p, affordances, decideOpts(p, perceptReason, affordances));
|
|
802
|
+
const { asked, mirrors, affordances } = offersForDecision(c, world, p, t);
|
|
803
|
+
const record = decide(p, affordances, decisionOptions(c, world, p, opts, perceptReason, affordances));
|
|
779
804
|
delete c.interrupts[p.id];
|
|
780
805
|
// Conversation advice is weighed until the next fresh decision, then it has been heard.
|
|
781
806
|
if (!act)
|
package/dist/types.d.ts
CHANGED
|
@@ -511,7 +511,10 @@ export interface Commitment {
|
|
|
511
511
|
status: 'pending' | 'kept' | 'broken' | 'released';
|
|
512
512
|
/** Optional recurrence: re-create after it closes (e.g. daily prayer windows). */
|
|
513
513
|
recurEvery?: number;
|
|
514
|
-
/**
|
|
514
|
+
/**
|
|
515
|
+
* Optional display name, e.g. 'Maghrib'; used in counter-offers ("after I pray Maghrib") and in `reviewLine`
|
|
516
|
+
* ("I didn't keep my call to Mum"), so write it as a noun phrase.
|
|
517
|
+
*/
|
|
515
518
|
label?: string;
|
|
516
519
|
/** For kind 'abstain': action ids whose completion inside the window breaks it. */
|
|
517
520
|
violatedBy?: string[];
|
|
@@ -837,7 +840,7 @@ export declare const PERSON_SCHEMA = "human/person@1";
|
|
|
837
840
|
* and an activity that would cover a closing stretch is reviewed when the stretch begins (`isUnderWay`,
|
|
838
841
|
* `dutyReviewAt`). Person shape unchanged.
|
|
839
842
|
*/
|
|
840
|
-
export declare const ENGINE_VERSION = "2.
|
|
843
|
+
export declare const ENGINE_VERSION = "2.9.0";
|
|
841
844
|
/** A pregnancy carried by this person (host-driven; see `family/`). */
|
|
842
845
|
export interface Pregnancy {
|
|
843
846
|
fatherId: PersonId;
|
|
@@ -1100,7 +1103,11 @@ export interface Affordance {
|
|
|
1100
1103
|
/** Free tags: 'work', 'leisure', 'social', 'worship', 'risky', 'outdoors', ... */
|
|
1101
1104
|
tags?: string[];
|
|
1102
1105
|
norms?: NormTag[];
|
|
1103
|
-
/**
|
|
1106
|
+
/**
|
|
1107
|
+
* Commitment ids this would fulfil if completed. Listing two or more keeps every listed one that is pending and
|
|
1108
|
+
* in window, and no other (2.9.0), not even an earlier-ending match by action; listing one keeps the
|
|
1109
|
+
* earliest-ending match, explicit or by action, as before.
|
|
1110
|
+
*/
|
|
1104
1111
|
fulfills?: string[];
|
|
1105
1112
|
/** Goal ids this advances. */
|
|
1106
1113
|
advances?: string[];
|
|
@@ -1249,7 +1256,11 @@ export interface Outcome {
|
|
|
1249
1256
|
* under protest and 1 otherwise. Lower quality lowers felt valence and therefore learned expectations.
|
|
1250
1257
|
*/
|
|
1251
1258
|
quality?: Unit;
|
|
1252
|
-
/**
|
|
1259
|
+
/**
|
|
1260
|
+
* Commitment ids this outcome keeps (defaults to the activity's `Affordance.fulfills`). With two or more listed,
|
|
1261
|
+
* every listed one pending and in window is kept, and no other (2.9.0); with one, one completion keeps the
|
|
1262
|
+
* earliest-ending match.
|
|
1263
|
+
*/
|
|
1253
1264
|
fulfills?: string[];
|
|
1254
1265
|
/** Goal ids this outcome advances (defaults to the activity's `Affordance.advances`). */
|
|
1255
1266
|
advances?: string[];
|
|
@@ -1333,9 +1344,16 @@ export interface Term {
|
|
|
1333
1344
|
* 'goal:g1', 'habit', 'emotion:fear:risky' (emotion id, then the tag it acts through), 'effort', 'risk',
|
|
1334
1345
|
* 'suggestion:player', 'social:p2', 'expectation', 'autonomy' (reactance to a pushing voice),
|
|
1335
1346
|
* 'promptings:turn' (a practised turning-away), 'forecast:body' (expected
|
|
1336
|
-
* bodily state during `followedBy` spans), 'promptings:echo' (recurrence under the `fault` reading)
|
|
1347
|
+
* bodily state during `followedBy` spans), 'promptings:echo' (recurrence under the `fault` reading),
|
|
1348
|
+
* 'review:resolve' (a resolve from the evening review) */
|
|
1337
1349
|
source: string;
|
|
1338
1350
|
value: number;
|
|
1351
|
+
/**
|
|
1352
|
+
* What a generic source refers to (2.9.0), so `source` stays stable for hosts that match it exactly. Set on
|
|
1353
|
+
* `review:resolve` to the resolve's key (`Resolve.key`) when exactly one resolve weighed in; absent when several
|
|
1354
|
+
* did. Absent on every other term.
|
|
1355
|
+
*/
|
|
1356
|
+
ref?: string;
|
|
1339
1357
|
}
|
|
1340
1358
|
export interface Considered {
|
|
1341
1359
|
affordanceId: string;
|
|
@@ -1455,6 +1473,13 @@ export interface PromptingResponse {
|
|
|
1455
1473
|
/** Skill-like practice 0..1 as of `lastUsedAt`; it decays slowly with disuse. */
|
|
1456
1474
|
practice: Unit;
|
|
1457
1475
|
lastUsedAt: Minute;
|
|
1476
|
+
/**
|
|
1477
|
+
* What this response was learned for (2.9.0): an action, an offer tag, or the opposing norm or goal (`n1`,
|
|
1478
|
+
* `norm:n1` or `goal:g1`). A cued response serves only prompts it matches; a response without a cue serves every
|
|
1479
|
+
* prompt that no cued response matches. A norm or goal cue matches only the prompt's strongest opposing term.
|
|
1480
|
+
* Cues longer than 200 characters are ignored.
|
|
1481
|
+
*/
|
|
1482
|
+
cue?: string;
|
|
1458
1483
|
}
|
|
1459
1484
|
export interface PromptingRecord {
|
|
1460
1485
|
at: Minute;
|
|
@@ -1482,6 +1507,8 @@ export interface PromptingsState {
|
|
|
1482
1507
|
}>;
|
|
1483
1508
|
/** Echo gained per prompt under `fault`. Default 0: the recurrence term is a host-set assumption (P3 contested). */
|
|
1484
1509
|
echoRate: number;
|
|
1510
|
+
/** Days over which disused practice halves (2.9.0, host-set); absent = `PROMPTINGS_DEFAULTS.practiceHalfLifeDays`. */
|
|
1511
|
+
practiceHalfLifeDays?: number;
|
|
1485
1512
|
}
|
|
1486
1513
|
/**
|
|
1487
1514
|
* How a person reviews a day (2.7.0, a held reading): `accounting` (muhasaba: name the lapse and resolve for its
|
|
@@ -1528,12 +1555,21 @@ export interface ReviewSpec {
|
|
|
1528
1555
|
export interface PromptingsSpec {
|
|
1529
1556
|
reading?: PromptingReading;
|
|
1530
1557
|
readingSource?: string;
|
|
1558
|
+
/** `cue` (2.9.0): what the response was learned for; see `PromptingResponse.cue`. */
|
|
1531
1559
|
responses?: {
|
|
1532
1560
|
id: string;
|
|
1533
1561
|
kind: PromptingResponseKind;
|
|
1534
1562
|
practice?: Unit;
|
|
1563
|
+
cue?: string;
|
|
1535
1564
|
}[];
|
|
1536
1565
|
echoRate?: number;
|
|
1566
|
+
/**
|
|
1567
|
+
* Days over which disused practice halves (2.9.0), for hosts whose time runs short (a one-week game). Applies to
|
|
1568
|
+
* every response of the person, including disuse since a response's last use. Absent = 60 days, as before.
|
|
1569
|
+
* Read on every `enablePromptings` call: re-enabling without it returns the person to the default. Values outside
|
|
1570
|
+
* (0, 36500] are ignored.
|
|
1571
|
+
*/
|
|
1572
|
+
practiceHalfLifeDays?: number;
|
|
1537
1573
|
}
|
|
1538
1574
|
/** What the body is doing over an interval. */
|
|
1539
1575
|
export interface BodyLoad {
|
package/dist/types.js
CHANGED
|
@@ -120,4 +120,4 @@ export const PERSON_SCHEMA = 'human/person@1';
|
|
|
120
120
|
* and an activity that would cover a closing stretch is reviewed when the stretch begins (`isUnderWay`,
|
|
121
121
|
* `dutyReviewAt`). Person shape unchanged.
|
|
122
122
|
*/
|
|
123
|
-
export const ENGINE_VERSION = '2.
|
|
123
|
+
export const ENGINE_VERSION = '2.9.0';
|
package/dist/will/will.js
CHANGED
|
@@ -562,10 +562,49 @@ const voiceTerm = (c, voiceId) => c?.terms.find((t) => t.source === `suggestion:
|
|
|
562
562
|
* group-advice findings only in shape (advice is weighed by trust in each adviser, not by headcount); it does not model conformity to a majority, persuasion between the voices, or the person asking for advice.
|
|
563
563
|
*/
|
|
564
564
|
function evaluate(p, input, ctx, given, draw) {
|
|
565
|
-
const W = WILL_DEFAULTS;
|
|
566
565
|
const cmd = ctx.command;
|
|
567
566
|
// A commanding voice is not also weighed as a suggestion in the same decision.
|
|
568
567
|
const suggestions = cmd ? given.filter((s) => s.voiceId !== cmd.voiceId) : given;
|
|
568
|
+
const prepared = prepareVetoes(p, input, ctx, suggestions);
|
|
569
|
+
const { considered, affById } = prepared;
|
|
570
|
+
const selection = selectOption(p, considered, ctx, draw);
|
|
571
|
+
const ev = {
|
|
572
|
+
chosenId: selection.winner?.affordanceId ?? null,
|
|
573
|
+
considered,
|
|
574
|
+
resolutions: [],
|
|
575
|
+
autonomyDelta: 0,
|
|
576
|
+
voices: [],
|
|
577
|
+
};
|
|
578
|
+
if (cmd && arbitrateCommand(p, ctx, cmd, suggestions, prepared, selection, ev))
|
|
579
|
+
return ev;
|
|
580
|
+
if (suggestions.length === 0)
|
|
581
|
+
return withCommandRefusal(ev, cmd);
|
|
582
|
+
if (inBreak(p)) {
|
|
583
|
+
// During a mental break no voice reaches them; nothing is counted for or against any of them.
|
|
584
|
+
ev.resolutions = suggestions.map((s) => ({
|
|
585
|
+
voiceId: s.voiceId,
|
|
586
|
+
verdict: 'refused',
|
|
587
|
+
kind: 'cannot',
|
|
588
|
+
reason: 'break',
|
|
589
|
+
says: '',
|
|
590
|
+
}));
|
|
591
|
+
ev.voices = suggestions.map((s) => ({ id: s.voiceId, pressure: 0, accepted: 0, refused: 0 }));
|
|
592
|
+
if (ev.resolutions.length === 1)
|
|
593
|
+
ev.suggestion = ev.resolutions[0];
|
|
594
|
+
return withCommandRefusal(ev, cmd);
|
|
595
|
+
}
|
|
596
|
+
const judged = judgeVoices(p, ctx, suggestions, prepared, selection);
|
|
597
|
+
arbitrateInsistence(p, judged, considered, affById, ev);
|
|
598
|
+
ev.resolutions = judged.map((j) => j.res);
|
|
599
|
+
ev.voices = judged.map((j) => j.side);
|
|
600
|
+
const chosen = considered.find((c) => c.affordanceId === ev.chosenId);
|
|
601
|
+
ev.suggestion =
|
|
602
|
+
creditedResolution(ev.resolutions, chosen) ??
|
|
603
|
+
(ev.resolutions.length === 1 ? ev.resolutions[0] : undefined);
|
|
604
|
+
return withCommandRefusal(ev, cmd);
|
|
605
|
+
}
|
|
606
|
+
/** Apply capacity, conscience, per-voice distrust and omission vetoes, in that order. */
|
|
607
|
+
function prepareVetoes(p, input, ctx, suggestions) {
|
|
569
608
|
const affById = new Map(ctx.affordances.map((a) => [a.id, a]));
|
|
570
609
|
const duties = closingDuties(p, ctx);
|
|
571
610
|
const servesDuty = (aff) => duties.some((c) => keepsDuty(p, c, aff, ctx.now));
|
|
@@ -609,6 +648,11 @@ function evaluate(p, input, ctx, given, draw) {
|
|
|
609
648
|
out.vetoed = veto;
|
|
610
649
|
return out;
|
|
611
650
|
});
|
|
651
|
+
return { affById, duties, baseVeto, distrustOf, considered };
|
|
652
|
+
}
|
|
653
|
+
/** Select once using the supplied draw; prediction passes no draw and stays deterministic. */
|
|
654
|
+
function selectOption(p, considered, ctx, draw) {
|
|
655
|
+
const W = WILL_DEFAULTS;
|
|
612
656
|
// Selection: inertia for the current activity, then argmax or softmax.
|
|
613
657
|
const live = considered.filter((c) => !c.vetoed);
|
|
614
658
|
const current = p.activity;
|
|
@@ -636,67 +680,55 @@ function evaluate(p, input, ctx, given, draw) {
|
|
|
636
680
|
}
|
|
637
681
|
}
|
|
638
682
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
verdict: 'commanded',
|
|
659
|
-
reason: 'command',
|
|
660
|
-
says: '',
|
|
661
|
-
margin: co.margin,
|
|
662
|
-
};
|
|
663
|
-
if (co.ownAffordanceId !== null && co.ownAffordanceId !== co.targetAffordanceId)
|
|
664
|
-
res.insteadAffordanceId = co.ownAffordanceId;
|
|
665
|
-
const all = [
|
|
666
|
-
res,
|
|
667
|
-
...suggestions.map((s) => ({
|
|
668
|
-
voiceId: s.voiceId,
|
|
669
|
-
verdict: 'refused',
|
|
670
|
-
kind: 'cannot',
|
|
671
|
-
reason: 'commanded',
|
|
672
|
-
says: '',
|
|
673
|
-
})),
|
|
674
|
-
].sort((a, b) => cmpStr(a.voiceId, b.voiceId));
|
|
675
|
-
ev.resolutions = all;
|
|
676
|
-
ev.voices = all.map((r) => ({ id: r.voiceId, pressure: 0, accepted: 0, refused: 0 }));
|
|
677
|
-
ev.suggestion = res;
|
|
678
|
-
return ev;
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
if (suggestions.length === 0)
|
|
682
|
-
return withCommandRefusal(ev, cmd);
|
|
683
|
-
if (inBreak(p)) {
|
|
684
|
-
// During a mental break no voice reaches them; nothing is counted for or against any of them.
|
|
685
|
-
ev.resolutions = suggestions.map((s) => ({
|
|
686
|
-
voiceId: s.voiceId,
|
|
687
|
-
verdict: 'refused',
|
|
688
|
-
kind: 'cannot',
|
|
689
|
-
reason: 'break',
|
|
683
|
+
return { winner, ranked, score };
|
|
684
|
+
}
|
|
685
|
+
/** Return true when a command holds and completes the evaluation ahead of all suggestions. */
|
|
686
|
+
function arbitrateCommand(p, ctx, cmd, suggestions, prepared, selection, ev) {
|
|
687
|
+
const { considered, baseVeto, duties } = prepared;
|
|
688
|
+
const { winner, score } = selection;
|
|
689
|
+
const co = commandOutcome(p, considered, ctx, cmd, winner, score, baseVeto, duties);
|
|
690
|
+
ev.command = co;
|
|
691
|
+
if (co.holds && co.targetAffordanceId !== undefined) {
|
|
692
|
+
// Direct control: the commanded option is done; every other voice is set aside (no counters) and a
|
|
693
|
+
// distrust veto another voice caused does not stand in the way.
|
|
694
|
+
ev.chosenId = co.targetAffordanceId;
|
|
695
|
+
for (const c of considered)
|
|
696
|
+
if (c.vetoed?.reason === 'distrust')
|
|
697
|
+
delete c.vetoed;
|
|
698
|
+
const res = {
|
|
699
|
+
voiceId: cmd.voiceId,
|
|
700
|
+
verdict: 'commanded',
|
|
701
|
+
reason: 'command',
|
|
690
702
|
says: '',
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
if (
|
|
694
|
-
|
|
695
|
-
|
|
703
|
+
margin: co.margin,
|
|
704
|
+
};
|
|
705
|
+
if (co.ownAffordanceId !== null && co.ownAffordanceId !== co.targetAffordanceId)
|
|
706
|
+
res.insteadAffordanceId = co.ownAffordanceId;
|
|
707
|
+
const all = [
|
|
708
|
+
res,
|
|
709
|
+
...suggestions.map((s) => ({
|
|
710
|
+
voiceId: s.voiceId,
|
|
711
|
+
verdict: 'refused',
|
|
712
|
+
kind: 'cannot',
|
|
713
|
+
reason: 'commanded',
|
|
714
|
+
says: '',
|
|
715
|
+
})),
|
|
716
|
+
].sort((a, b) => cmpStr(a.voiceId, b.voiceId));
|
|
717
|
+
ev.resolutions = all;
|
|
718
|
+
ev.voices = all.map((r) => ({ id: r.voiceId, pressure: 0, accepted: 0, refused: 0 }));
|
|
719
|
+
ev.suggestion = res;
|
|
720
|
+
return true;
|
|
696
721
|
}
|
|
722
|
+
return false;
|
|
723
|
+
}
|
|
724
|
+
/** Judge each voice against the same selected option before resolving competing insistence. */
|
|
725
|
+
function judgeVoices(p, ctx, suggestions, prepared, selection) {
|
|
726
|
+
const W = WILL_DEFAULTS;
|
|
727
|
+
const { considered, affById, distrustOf } = prepared;
|
|
728
|
+
const { winner, ranked, score } = selection;
|
|
697
729
|
const winnerAff = winner ? affById.get(winner.affordanceId) : undefined;
|
|
698
730
|
const reason = dominantTerm(winner);
|
|
699
|
-
|
|
731
|
+
return suggestions.map((s) => {
|
|
700
732
|
const suggested = considered.filter((c) => {
|
|
701
733
|
const aff = affById.get(c.affordanceId);
|
|
702
734
|
return aff !== undefined && suggestionTargets(s, aff);
|
|
@@ -815,6 +847,10 @@ function evaluate(p, input, ctx, given, draw) {
|
|
|
815
847
|
: { affordanceId: bestSuggested.affordanceId, label: 'later' },
|
|
816
848
|
}));
|
|
817
849
|
});
|
|
850
|
+
}
|
|
851
|
+
/** Settle competing insistence in the existing voice order, preserving equal-trust/strength ties. */
|
|
852
|
+
function arbitrateInsistence(p, judged, considered, affById, ev) {
|
|
853
|
+
const W = WILL_DEFAULTS;
|
|
818
854
|
// Rule (a): at most one insisted option is complied with.
|
|
819
855
|
const insisting = judged.filter((j) => j.comply !== undefined);
|
|
820
856
|
let forcer;
|
|
@@ -882,13 +918,6 @@ function evaluate(p, input, ctx, given, draw) {
|
|
|
882
918
|
}
|
|
883
919
|
}
|
|
884
920
|
}
|
|
885
|
-
ev.resolutions = judged.map((j) => j.res);
|
|
886
|
-
ev.voices = judged.map((j) => j.side);
|
|
887
|
-
const chosen = considered.find((c) => c.affordanceId === ev.chosenId);
|
|
888
|
-
ev.suggestion =
|
|
889
|
-
creditedResolution(ev.resolutions, chosen) ??
|
|
890
|
-
(ev.resolutions.length === 1 ? ev.resolutions[0] : undefined);
|
|
891
|
-
return withCommandRefusal(ev, cmd);
|
|
892
921
|
}
|
|
893
922
|
/** Add the commanding voice's refusal (a command that did not hold) to the resolutions, in voice-id order. */
|
|
894
923
|
function withCommandRefusal(ev, cmd) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adam0white/human-framework",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.1",
|
|
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",
|