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