@adam0white/human-framework 2.14.0 → 2.15.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 +126 -87
- package/dist/agenda/agenda.d.ts +14 -0
- package/dist/agenda/agenda.js +65 -6
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3 -2
- package/dist/migrate.js +7 -1
- package/dist/persistence/persistence.d.ts +1 -0
- package/dist/persistence/persistence.js +27 -8
- package/dist/persona.d.ts +76 -0
- package/dist/persona.js +457 -0
- package/dist/projects/projects.js +12 -1
- package/dist/types.d.ts +12 -2
- package/dist/types.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,13 +1,72 @@
|
|
|
1
1
|
# @adam0white/human-framework
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
choice comes with the terms that produced it and a first-person line, so a refusal reads as a reason rather
|
|
7
|
-
than a bug.
|
|
3
|
+
HF is a TypeScript library for simulated people in games and headless simulations. Your world offers
|
|
4
|
+
possible actions. Each person weighs needs, habits, duties, goals and suggestions, then chooses.
|
|
5
|
+
A player can suggest an action; the person can defer or refuse it.
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
HF 2.14.1 uses utility scoring and seeded randomness, with no LLM in the actor loop and no runtime
|
|
8
|
+
dependencies. Decision records expose scored terms and template narration. Person state is plain JSON.
|
|
9
|
+
This is an authored simulation model, not a validated predictor of real people.
|
|
10
|
+
|
|
11
|
+
## Try the games
|
|
12
|
+
|
|
13
|
+
- [Twice at the Well](https://human.adamwhite.work/colony/): compare a colony without and with HF.
|
|
14
|
+
- [The Day You Say Nothing](https://human.adamwhite.work/voice/): suggest actions to one man who can refuse.
|
|
15
|
+
- [The Night Watch](https://human.adamwhite.work/watch/): guide a village watch across winters.
|
|
16
|
+
- [All lab games](https://human.adamwhite.work/games/).
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
Use Node 24 or newer. In an empty directory, install the version this README describes:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npm install @adam0white/human-framework@2.14.1
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Save these 20 lines as `quickstart.mjs`, then run `node quickstart.mjs`:
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
import {
|
|
30
|
+
createCommunity, createPerson, createVillage,
|
|
31
|
+
predict, stepCommunity, villagerSpec,
|
|
32
|
+
} from '@adam0white/human-framework';
|
|
33
|
+
const start = 6 * 60; // simulation minutes
|
|
34
|
+
const ids = ['ada', 'bora'];
|
|
35
|
+
const people = ids.map((id, i) => createPerson(
|
|
36
|
+
villagerSpec(id, id, i + 1, { now: start, others: ids }),
|
|
37
|
+
));
|
|
38
|
+
const world = createVillage(people, { seed: 7 });
|
|
39
|
+
const community = createCommunity(people);
|
|
40
|
+
const ada = community.people[0];
|
|
41
|
+
const ask = { voiceId: 'player', action: 'chat', strength: 0.6 };
|
|
42
|
+
const hint = predict(ada, world.affordancesFor(ada), ask);
|
|
43
|
+
console.log('Prediction:', hint.verdict, hint.reason);
|
|
44
|
+
stepCommunity(community, world, start + 24 * 60, {
|
|
45
|
+
suggestions: { ada: ask },
|
|
46
|
+
});
|
|
47
|
+
console.log('Last decision:', ada.trace.at(-1)?.narration);
|
|
48
|
+
console.log('JSON snapshot characters:', JSON.stringify(ada).length);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`predict` leaves the person and RNG unchanged. `stepCommunity` advances a bundled example village
|
|
52
|
+
for one day; the suggestion stands at Ada's decisions. For your own game, replace that village
|
|
53
|
+
with a `World` that supplies offers, percepts and actual outcomes (see [the host contract](#the-host-contract)).
|
|
54
|
+
|
|
55
|
+
## What it models
|
|
56
|
+
|
|
57
|
+
- Bodily and psychological needs, affect, attention, memory, beliefs, skills, habits and relationships.
|
|
58
|
+
- Values, held moral norms, commitments, goals and trust in outside voices.
|
|
59
|
+
- Optional faculties for long runs: aging, family, gists and character change.
|
|
60
|
+
- Opt-in curiosity, bounded planning, self-set projects from host-shown prospects and pursuit persistence.
|
|
61
|
+
|
|
62
|
+
The host supplies the available actions and their meaning. Projects and planning do not invent actions
|
|
63
|
+
or solve arbitrary tasks. In the island control on 2.14.1, people made small free-time piles, but most
|
|
64
|
+
placements replaced earlier work and the carry task did not improve. See the
|
|
65
|
+
[experiment](https://github.com/adam0white/human-framework/blob/main/docs/lab/experiments/island-2026-10-20.md).
|
|
66
|
+
|
|
67
|
+
HF does not provide a map, pathfinding, free-form language, a general planner or calibrated human
|
|
68
|
+
behaviour. Its default norms are Islam-guided; it models a person's held understanding, never a ruling
|
|
69
|
+
or divine acceptance. See [the boundaries](#what-it-does-not-model).
|
|
11
70
|
|
|
12
71
|
## The spine: Urge → Assent → Act
|
|
13
72
|
|
|
@@ -38,69 +97,13 @@ never overrides `cannot` or `willNot`.
|
|
|
38
97
|
Every resolution also carries a machine-readable `reason` (`need:food`, `norm:theft`, `asleep`, `distrust`,
|
|
39
98
|
...) and a line in the person's voice (`says`).
|
|
40
99
|
|
|
41
|
-
##
|
|
42
|
-
|
|
43
|
-
The source is public on [GitHub](https://github.com/adam0white/human-framework) under the MIT license. Install
|
|
44
|
-
it from npm (published from 2.1.3; later versions are built by the release workflow and carry an npm provenance
|
|
45
|
-
attestation):
|
|
46
|
-
|
|
47
|
-
```sh
|
|
48
|
-
npm install @adam0white/human-framework
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
Each [GitHub release](https://github.com/adam0white/human-framework/releases) also has the tarball attached, which
|
|
52
|
-
installs by URL:
|
|
53
|
-
|
|
54
|
-
```sh
|
|
55
|
-
npm install https://github.com/adam0white/human-framework/releases/download/v2.2.0/adam0white-human-framework-2.2.0.tgz
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
or build one from a checkout:
|
|
59
|
-
|
|
60
|
-
```sh
|
|
61
|
-
npm pack -w packages/human # runs the build; writes adam0white-human-framework-<version>.tgz
|
|
62
|
-
npm install /path/to/adam0white-human-framework-<version>.tgz
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
The v2.1.0 release tarball (`human-framework-2.1.0.tgz`) predates the rename and installs as
|
|
66
|
-
`@human/framework`.
|
|
67
|
-
|
|
68
|
-
It ships ES modules and `.d.ts` files (`exports["."]` with `types`). It requires Node ≥ 24 or a modern
|
|
69
|
-
browser. The library needs `structuredClone` and nothing else from the host.
|
|
70
|
-
|
|
71
|
-
## Quick start
|
|
72
|
-
|
|
73
|
-
```ts
|
|
74
|
-
import {
|
|
75
|
-
createCommunity, createPerson, createVillage, MINUTES_PER_DAY, predict, stepCommunity,
|
|
76
|
-
type Suggestion, villagerSpec,
|
|
77
|
-
} from '@adam0white/human-framework';
|
|
78
|
-
|
|
79
|
-
const ids = ['ada', 'bora'];
|
|
80
|
-
const people = ids.map((id, i) => createPerson(villagerSpec(id, id, 1 + i, { now: 6 * 60, others: ids })));
|
|
81
|
-
const world = createVillage(people, { seed: 7 }); // a bundled World: home, field, well, kitchen, mosque, forest
|
|
82
|
-
const community = createCommunity(people);
|
|
83
|
-
const ada = community.people[0]!;
|
|
84
|
-
|
|
85
|
-
const ask: Suggestion = { voiceId: 'player', action: 'chat', strength: 0.6 };
|
|
86
|
-
const hint = predict(ada, world.affordancesFor(ada), ask); // pure: no state change, no RNG
|
|
87
|
-
console.log(hint.verdict, hint.reason, hint.says);
|
|
88
|
-
|
|
89
|
-
// One day; the suggestion stands at each of Ada's decisions.
|
|
90
|
-
const events = stepCommunity(community, world, 6 * 60 + MINUTES_PER_DAY, { suggestions: { ada: ask } });
|
|
91
|
-
const last = ada.trace.at(-1)!;
|
|
92
|
-
console.log(last.chosenAction, last.suggestion?.verdict, last.narration);
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
The runnable examples in [`examples/`](examples) go further. Run them all with
|
|
96
|
-
`npm run examples -w packages/human`, or one at a time with `node examples/<name>.ts` after a build. A
|
|
97
|
-
vitest (`test/examples.test.ts`) runs them, so they keep working as the code changes.
|
|
100
|
+
## Package and source
|
|
98
101
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
The package ships ES modules and TypeScript declarations. It requires Node ≥24 or a modern browser
|
|
103
|
+
with `structuredClone`. Source, tests and runnable examples are on
|
|
104
|
+
[GitHub](https://github.com/adam0white/human-framework/tree/main/packages/human).
|
|
105
|
+
In a source checkout, `npm run examples -w packages/human` builds and runs `minimal.ts`,
|
|
106
|
+
`suggestions.ts` and `community.ts`. The `examples/` and `test/` directories are not in the npm package.
|
|
104
107
|
|
|
105
108
|
## The host contract
|
|
106
109
|
|
|
@@ -141,10 +144,10 @@ place. A `social` tag is also a claim about the offered activity, not a request
|
|
|
141
144
|
contact becomes impossible, interrupt or replan the activity instead of completing the declared
|
|
142
145
|
encounter. Report only the need effects that the actual interaction supports.
|
|
143
146
|
|
|
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).
|
|
147
|
+
The current behavior is defined in [`Affordance` and `Outcome`](https://github.com/adam0white/human-framework/blob/main/packages/human/src/types.ts) and
|
|
148
|
+
[`finish`](https://github.com/adam0white/human-framework/blob/main/packages/human/src/person.ts). Existing controls cover [body advancement and waking after sleep](https://github.com/adam0white/human-framework/blob/main/packages/human/test/person.test.ts),
|
|
149
|
+
[body recovery](https://github.com/adam0white/human-framework/blob/main/packages/human/test/body.test.ts), [material valuation under scarcity](https://github.com/adam0white/human-framework/blob/main/packages/human/test/cognition-scarcity.test.ts),
|
|
150
|
+
[actual material in day records](https://github.com/adam0white/human-framework/blob/main/packages/human/test/chronicle.test.ts), and [relationship-aware social terms](https://github.com/adam0white/human-framework/blob/main/packages/human/test/social.test.ts).
|
|
148
151
|
These boundaries require correct host reports; they do not imply a framework behavior change.
|
|
149
152
|
|
|
150
153
|
### Scheduled activity reviews
|
|
@@ -162,8 +165,8 @@ Do not restart a continued activity or refund its resources. Hard capacity vetoe
|
|
|
162
165
|
Sleep reviews can move earlier for ordinary pending appointments. The appointment's time pressure
|
|
163
166
|
can permit waking; an obligatory norm is not required. A host that ignores the scheduled review can
|
|
164
167
|
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).
|
|
168
|
+
for unavailable future actions. See [`reviewed` and the decision lifecycle](https://github.com/adam0white/human-framework/blob/main/packages/human/src/person.ts) and
|
|
169
|
+
[`wakeReviewAt` and commitment-based waking](https://github.com/adam0white/human-framework/blob/main/packages/human/src/will/will.ts).
|
|
167
170
|
|
|
168
171
|
You can drive a person by hand: `decide(p, offers, { suggestion })` → `begin(p, offer, record)` →
|
|
169
172
|
`tick(p, now)` → `finish(p, outcome)`, with `perceive(p, percepts)` and `interrupt(p, now, reason)` between.
|
|
@@ -212,20 +215,20 @@ reference host: `createVillage`.
|
|
|
212
215
|
- Hosts that live people sparsely over decades can shrink what a save carries per person with
|
|
213
216
|
`setRetention(p, { trace, chronicleDays })` (or `PersonSpec.retention`): fewer decision records, and day records
|
|
214
217
|
bounded by age as well as count. No decision changes. `restore` also refuses a non-finite or out-of-range `now`
|
|
215
|
-
and
|
|
218
|
+
and bounds retained lists; hosts must still limit input size before parsing.
|
|
216
219
|
- Version numbers are described under [Versions](#versions).
|
|
217
220
|
|
|
218
221
|
## Versions
|
|
219
222
|
|
|
220
223
|
There are two version numbers, and they move independently.
|
|
221
224
|
|
|
222
|
-
- **The release version** is the package version (`package.json`, now `2.
|
|
225
|
+
- **The release version** is the package version (`package.json`, now `2.14.1`) and `FRAMEWORK_VERSION`, the same
|
|
223
226
|
string compiled into the build; a test keeps them equal. It versions the public API under
|
|
224
227
|
[semver](https://semver.org/): a breaking change to an exported name or signature bumps the major, a new
|
|
225
228
|
faculty or export the minor, a fix the patch. Each release is a tag `vX.Y.Z` and a GitHub release whose notes
|
|
226
|
-
are that version's section of the root [CHANGELOG.md](
|
|
227
|
-
(`npm run release`,
|
|
228
|
-
- **`ENGINE_VERSION`** (exported; `2.
|
|
229
|
+
are that version's section of the root [CHANGELOG.md](https://github.com/adam0white/human-framework/blob/main/CHANGELOG.md), with the package tarball attached
|
|
230
|
+
(`npm run release`, from a clean checkout of pushed `origin/main`).
|
|
231
|
+
- **`ENGINE_VERSION`** (exported; `2.14.1` at release 2.14.1) versions simulation behaviour and the save format. It
|
|
229
232
|
changes when the same seed and inputs would give different decisions, or when person state changes shape.
|
|
230
233
|
`restore` upgrades saves from engine 1.4.0 and later through `migrate` and refuses older ones. Every release's notes name the
|
|
231
234
|
`ENGINE_VERSION` it ships, so a host can tell whether its saves still restore.
|
|
@@ -295,22 +298,22 @@ that place lose appeal until the fear fades. Groups and factions themselves stay
|
|
|
295
298
|
starts with a scope paragraph that also lists what that faculty leaves out.
|
|
296
299
|
- **Rulings or divine judgment.** Norms come from a host catalog with provenance (`NormDefinition.sources`;
|
|
297
300
|
the bundled `DEFAULT_NORMS` is Islam-guided; each reference's verification status is listed in
|
|
298
|
-
[research/norm-sources.md](
|
|
301
|
+
[research/norm-sources.md](https://github.com/adam0white/human-framework/blob/main/research/norm-sources.md)). A host with its own catalog passes it as
|
|
299
302
|
`World.catalog`, so necessity excuses and exemptions read that catalog; a norm not in it is not excused.
|
|
300
303
|
A person holds an *understanding* of a norm, with a standing and a conviction. That understanding is
|
|
301
304
|
not a ruling. The library never computes divine acceptance. Where schools of thought differ, the
|
|
302
305
|
position the bundled catalog takes, and why, is recorded in
|
|
303
|
-
[research/decisions.md](
|
|
306
|
+
[research/decisions.md](https://github.com/adam0white/human-framework/blob/main/research/decisions.md).
|
|
304
307
|
|
|
305
308
|
## Further reading
|
|
306
309
|
|
|
307
|
-
- [docs/framework.md](
|
|
310
|
+
- [docs/framework.md](https://github.com/adam0white/human-framework/blob/main/docs/framework.md): the architecture, which module owns which state slice, the
|
|
308
311
|
utility terms, will rules, host protocols, and locked decisions.
|
|
309
|
-
- [docs/api.md](
|
|
312
|
+
- [docs/api.md](https://github.com/adam0white/human-framework/blob/main/docs/api.md): every export, grouped by module, generated from the built declarations.
|
|
310
313
|
Helpers tagged `@internal` are stripped from the shipped declarations and are not part of the API.
|
|
311
314
|
The affect module's `release` is exported as `releaseEmotion`, the agenda's as `releaseCommitment`, and the
|
|
312
315
|
will's scorer as `scoreAndResolve`.
|
|
313
|
-
- [docs/findings.md](
|
|
316
|
+
- [docs/findings.md](https://github.com/adam0white/human-framework/blob/main/docs/findings.md): negative findings recorded during development.
|
|
314
317
|
|
|
315
318
|
## License
|
|
316
319
|
|
|
@@ -343,7 +346,7 @@ const evidence = methodEvidence(learner, 'send-north', 'new-parcel');
|
|
|
343
346
|
clearMethodContext(learner); // when leaving or switching the task
|
|
344
347
|
```
|
|
345
348
|
|
|
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.
|
|
349
|
+
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](https://github.com/adam0white/human-framework/blob/main/packages/human/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](https://github.com/adam0white/human-framework/blob/main/packages/human/examples/method-alternatives-control.ts) demonstrates two independently successful procedures under the same observations, failed practice, hunger and exact restore.
|
|
347
350
|
|
|
348
351
|
|
|
349
352
|
## Receiving a possible future route
|
|
@@ -400,4 +403,40 @@ Only actual terminal outcomes advance terminal goals. Finishing one's own reques
|
|
|
400
403
|
|
|
401
404
|
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
405
|
|
|
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,
|
|
406
|
+
[The frozen control](https://github.com/adam0white/human-framework/blob/main/packages/human/examples/received-consequences.ts) publishes useful, late, costly, uncertain, withheld, removed-knowledge, removed-goal, temporal-off and refused arms in two renamed domains. This one-step faculty does not search action graphs, price all waiting opportunities or guarantee consent or success. Opt-in bounded planning (2.12.0) and bodily forecast (2.5.0) extend valuation separately. Its numerical bounds and control parameters are engineering assumptions.
|
|
407
|
+
|
|
408
|
+
## Persona projection (opt-in)
|
|
409
|
+
|
|
410
|
+
`projectPersona(source, mapping)` produces `PersonaProjectionSnapshot` (`hf-persona-state/1`).
|
|
411
|
+
`admitPersona(projection, targetOpening, { mappingVersion, sourceId, targetId })` returns a detached
|
|
412
|
+
`{ person, report }`. `validatePersonaProjection(unknown)` validates portable data without filling defaults.
|
|
413
|
+
These functions never enter the actor loop and never mutate the source or the host opening.
|
|
414
|
+
|
|
415
|
+
The host supplies a versioned map of skill ids, selected episode ids and every continuing obligation.
|
|
416
|
+
Skills keep exact levels and practice evidence. Episode action/person/place/voice references need explicit
|
|
417
|
+
bindings. Both evidence timestamps and birth minute shift by `targetOpening.now - source.now`; the Person
|
|
418
|
+
RNG carries unchanged. Commitments have explicit target-calendar windows, identical duration, kind,
|
|
419
|
+
importance and recurrence, and stay pending. This is a binding for a known calendar, not a calendar translator.
|
|
420
|
+
|
|
421
|
+
Admission requires the declared identity binding, matching name, sex, age, healthy compatible body,
|
|
422
|
+
and matching family/bond provenance. Version 1 rejects injuries, illness, downing and exposures. It also rejects pregnancy, courtships, marriages
|
|
423
|
+
and mourning because their dates require a separate explicit clock mapping. It rejects
|
|
424
|
+
running source activities, active goals/projects, owed duties and unmapped ongoing obligations, including
|
|
425
|
+
completed occurrences of unreleased recurring chains. The host must release finished employment in its own
|
|
426
|
+
replay. The host owns the cast registry, death history, branch authority, destination references and truth of
|
|
427
|
+
selected facts. A hash is not authentication. An old projection cannot override a dead target Person.
|
|
428
|
+
|
|
429
|
+
Target authors transient body state, needs, affect, social/conscience state and optional behaviour settings.
|
|
430
|
+
Portable skills and selected episodes replace target priors in those stores. The report discloses exclusions;
|
|
431
|
+
it does not claim that the complete source life continued. The original checkpoint belongs in the source save.
|
|
432
|
+
Pin the admitted Person and mapping version in the target replay. The transport owns its cryptographic hash,
|
|
433
|
+
branch metadata, consent and storage bounds. See `examples/persona-control.ts` for a deterministic two-host
|
|
434
|
+
control with preserved evidence, source immutability and an unmapped-skill comparison.
|
|
435
|
+
|
|
436
|
+
The agenda retains scheduled make-up records after the matching commitment is kept. Passport admission
|
|
437
|
+
recognizes that positive kept record as completed history. Missing or pruned records do not establish completion.
|
|
438
|
+
A host that needs durable completion evidence can explicitly call `retirePaidMakeUps(person)` before bounded
|
|
439
|
+
commitment history prunes it. It removes only records matched to an existing, closed, kept make-up and returns
|
|
440
|
+
recursively frozen, detached `MakeUpReceipt` objects. Persist those receipts in the source replay. Pending,
|
|
441
|
+
broken, released, exempt and missing matches remain unresolved. The normal actor loop never calls retirement;
|
|
442
|
+
existing games and saves keep their previous behavior until a host opts in.
|
package/dist/agenda/agenda.d.ts
CHANGED
|
@@ -182,6 +182,20 @@ export declare function applyExemptions(p: Person, now: Minute, ctx?: {
|
|
|
182
182
|
exempted: Commitment[];
|
|
183
183
|
owed: OwedMakeUp[];
|
|
184
184
|
};
|
|
185
|
+
export interface MakeUpReceipt {
|
|
186
|
+
readonly owed: Readonly<OwedMakeUp>;
|
|
187
|
+
readonly commitment: Readonly<Commitment>;
|
|
188
|
+
/** Source clock when the host explicitly retired the recorded debt. */
|
|
189
|
+
readonly recordedAt: Minute;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Opt-in host archive boundary. Remove only retained make-up records with a matching existing kept instance,
|
|
193
|
+
* returning detached, recursively frozen receipts for the host to persist in its replay. Keep commitments and
|
|
194
|
+
* every unpaid, pending, released, exempt or unproven record. Call before ordinary bounded history prunes the
|
|
195
|
+
* evidence. No actor-loop call is added; hosts that do not call this retain exact prior state and behavior.
|
|
196
|
+
* Records describe simulated completion, never divine acceptance or a waiver of an unfulfilled duty.
|
|
197
|
+
*/
|
|
198
|
+
export declare function retirePaidMakeUps(p: Person): readonly MakeUpReceipt[];
|
|
185
199
|
/** Make-ups still owed and not yet scheduled. */
|
|
186
200
|
export declare const owedMakeUps: (p: Person) => OwedMakeUp[];
|
|
187
201
|
/**
|
package/dist/agenda/agenda.js
CHANGED
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
*/
|
|
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
|
-
import { clamp01, dayOf, dpow, hasNumbers, isObj } from '../core/index.js';
|
|
50
|
+
import { clamp01, dayOf, dpow, hasNumbers, isNum, isObj } from '../core/index.js';
|
|
51
51
|
import { expectedEffect } from '../memory/index.js';
|
|
52
52
|
import { MINUTES_PER_DAY } from '../types.js';
|
|
53
53
|
export const AGENDA_DEFAULTS = {
|
|
@@ -150,8 +150,11 @@ function sanitizeGoalTemporal(g) {
|
|
|
150
150
|
typeof project.prospectId === 'string' &&
|
|
151
151
|
project.prospectId.length > 0 &&
|
|
152
152
|
project.prospectId.length <= 512 &&
|
|
153
|
-
typeof project.perceivedProgress === 'boolean')
|
|
153
|
+
typeof project.perceivedProgress === 'boolean') {
|
|
154
154
|
g.project = { prospectId: project.prospectId, perceivedProgress: project.perceivedProgress };
|
|
155
|
+
if (isNum(project.peak) && project.peak >= 0 && project.peak <= 1)
|
|
156
|
+
g.project.peak = project.peak;
|
|
157
|
+
}
|
|
155
158
|
else
|
|
156
159
|
delete g.project;
|
|
157
160
|
}
|
|
@@ -228,7 +231,8 @@ export function release(p, id) {
|
|
|
228
231
|
c.status = 'released';
|
|
229
232
|
return true;
|
|
230
233
|
}
|
|
231
|
-
|
|
234
|
+
/** Compare recurrence identity without changing agenda state. @internal */
|
|
235
|
+
export const sameChain = (a, b) => a.chain !== undefined && b.chain !== undefined ? a.kind === b.kind && a.chain === b.chain : sameShape(a, b);
|
|
232
236
|
const sameShape = (a, b) => a.kind === b.kind &&
|
|
233
237
|
(a.violatedBy ?? []).join('|') === (b.violatedBy ?? []).join('|') &&
|
|
234
238
|
a.normId === b.normId &&
|
|
@@ -690,9 +694,12 @@ export function onFinished(p, outcome, startedAt = outcome.at, opts = {}) {
|
|
|
690
694
|
const amount = own > 0 ? own : namesGoal(g, outcome.advances) ? AGENDA_DEFAULTS.defaultAdvanceAmount : 0;
|
|
691
695
|
if (amount <= 0)
|
|
692
696
|
continue;
|
|
693
|
-
//
|
|
697
|
+
// onFinished runs before the host shows the result: advanced counts a contribution made from
|
|
698
|
+
// at or above the high-water mark, not a confirmed new high. Only the next perception moves
|
|
699
|
+
// the progress clock, importance and pull; the composite does not consume advanced.
|
|
694
700
|
if (g.project?.perceivedProgress) {
|
|
695
|
-
|
|
701
|
+
if (g.project.peak === undefined || g.progress >= g.project.peak)
|
|
702
|
+
advanced.push(g);
|
|
696
703
|
continue;
|
|
697
704
|
}
|
|
698
705
|
g.progress = clamp01(g.progress + amount);
|
|
@@ -730,7 +737,11 @@ export function perceiveProjectProgress(p, prospectId, progress, now) {
|
|
|
730
737
|
for (const g of p.agenda.goals) {
|
|
731
738
|
if (g.status !== 'active' || !g.project?.perceivedProgress || g.project.prospectId !== prospectId)
|
|
732
739
|
continue;
|
|
733
|
-
if (
|
|
740
|
+
if (p.projects?.behaviour === 2 && g.project.peak === undefined)
|
|
741
|
+
g.project.peak = g.progress;
|
|
742
|
+
if (progress > (g.project.peak ?? g.progress)) {
|
|
743
|
+
if (g.project.peak !== undefined)
|
|
744
|
+
g.project.peak = clamp01(progress);
|
|
734
745
|
g.lastAdvancedAt = now;
|
|
735
746
|
if (g.baseImportance !== undefined)
|
|
736
747
|
g.importance = g.baseImportance;
|
|
@@ -878,6 +889,54 @@ function boundOwed(state) {
|
|
|
878
889
|
owed.splice(i >= 0 ? i : 0, 1);
|
|
879
890
|
}
|
|
880
891
|
}
|
|
892
|
+
/** A kept, closed instance is positive recorded evidence; scheduling or missing history is not. */
|
|
893
|
+
function recordedKeptMakeUp(p, owed) {
|
|
894
|
+
return p.agenda.commitments.find((c) => owed.scheduledAs !== undefined &&
|
|
895
|
+
c.id === owed.scheduledAs &&
|
|
896
|
+
c.makeUpOf === owed.ofId &&
|
|
897
|
+
c.status === 'kept' &&
|
|
898
|
+
c.kind === owed.kind &&
|
|
899
|
+
c.normId === owed.normId &&
|
|
900
|
+
c.exempt === undefined &&
|
|
901
|
+
c.until <= p.now);
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Whether any retained make-up record lacks a recorded kept matching instance. Scheduling alone is not
|
|
905
|
+
* fulfillment. Missing/pruned history remains unresolved to this reader; it does not infer payment from absence.
|
|
906
|
+
* Read-only; does not change the agenda's retained debt/history representation. @internal
|
|
907
|
+
*/
|
|
908
|
+
export function hasUnresolvedMakeUps(p) {
|
|
909
|
+
return (p.agenda.owed ?? []).some((owed) => recordedKeptMakeUp(p, owed) === undefined);
|
|
910
|
+
}
|
|
911
|
+
function freezeReceipt(value) {
|
|
912
|
+
if (value !== null && typeof value === 'object') {
|
|
913
|
+
for (const child of Object.values(value))
|
|
914
|
+
freezeReceipt(child);
|
|
915
|
+
Object.freeze(value);
|
|
916
|
+
}
|
|
917
|
+
return value;
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Opt-in host archive boundary. Remove only retained make-up records with a matching existing kept instance,
|
|
921
|
+
* returning detached, recursively frozen receipts for the host to persist in its replay. Keep commitments and
|
|
922
|
+
* every unpaid, pending, released, exempt or unproven record. Call before ordinary bounded history prunes the
|
|
923
|
+
* evidence. No actor-loop call is added; hosts that do not call this retain exact prior state and behavior.
|
|
924
|
+
* Records describe simulated completion, never divine acceptance or a waiver of an unfulfilled duty.
|
|
925
|
+
*/
|
|
926
|
+
export function retirePaidMakeUps(p) {
|
|
927
|
+
const receipts = [];
|
|
928
|
+
const unresolved = [];
|
|
929
|
+
for (const owed of p.agenda.owed ?? []) {
|
|
930
|
+
const commitment = recordedKeptMakeUp(p, owed);
|
|
931
|
+
if (commitment)
|
|
932
|
+
receipts.push(structuredClone({ owed, commitment, recordedAt: p.now }));
|
|
933
|
+
else
|
|
934
|
+
unresolved.push(owed);
|
|
935
|
+
}
|
|
936
|
+
if (receipts.length)
|
|
937
|
+
p.agenda.owed = unresolved;
|
|
938
|
+
return freezeReceipt(receipts);
|
|
939
|
+
}
|
|
881
940
|
/** Make-ups still owed and not yet scheduled. */
|
|
882
941
|
export const owedMakeUps = (p) => (p.agenda.owed ?? []).filter((o) => o.scheduledAs === undefined);
|
|
883
942
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/** Human Framework public surface. */
|
|
2
|
-
export declare const FRAMEWORK_VERSION = "2.
|
|
2
|
+
export declare const FRAMEWORK_VERSION = "2.15.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
|
-
export type { GoalTemplate, PrayerCalendar, PrayerSchedule, PrayerTimes, Retimer, } from './agenda/index.ts';
|
|
6
|
-
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.ts';
|
|
5
|
+
export type { GoalTemplate, MakeUpReceipt, PrayerCalendar, PrayerSchedule, PrayerTimes, Retimer, } from './agenda/index.ts';
|
|
6
|
+
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, retirePaidMakeUps, revisePurposes, scheduleMakeUp, spanMeetsWindow, suhoorWindow, timesFor, violatesAbstention, } from './agenda/index.ts';
|
|
7
7
|
export * from './beliefs/index.ts';
|
|
8
8
|
export * from './body/index.ts';
|
|
9
9
|
export * from './character/index.ts';
|
|
@@ -30,6 +30,7 @@ export * from './needs/index.ts';
|
|
|
30
30
|
export * from './partnering/index.ts';
|
|
31
31
|
export { enablePersistence, PERSISTENCE_DEFAULTS, pursuitOf } from './persistence/index.ts';
|
|
32
32
|
export * from './person.ts';
|
|
33
|
+
export * from './persona.ts';
|
|
33
34
|
export { considerProjects, enableProjects, PROJECTS_DEFAULTS, projectAppeal } from './projects/index.ts';
|
|
34
35
|
export * from './promptings/index.ts';
|
|
35
36
|
export type { ConsequenceEvidence, PlanRoute, PlanStep } from './prospection/index.ts';
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/** Human Framework public surface. */
|
|
2
|
-
export const FRAMEWORK_VERSION = '2.
|
|
2
|
+
export const FRAMEWORK_VERSION = '2.15.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
|
-
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';
|
|
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, retirePaidMakeUps, revisePurposes, scheduleMakeUp, spanMeetsWindow, suhoorWindow, timesFor, violatesAbstention, } from './agenda/index.js';
|
|
6
6
|
export * from './beliefs/index.js';
|
|
7
7
|
export * from './body/index.js';
|
|
8
8
|
export * from './character/index.js';
|
|
@@ -27,6 +27,7 @@ export * from './needs/index.js';
|
|
|
27
27
|
export * from './partnering/index.js';
|
|
28
28
|
export { enablePersistence, PERSISTENCE_DEFAULTS, pursuitOf } from './persistence/index.js';
|
|
29
29
|
export * from './person.js';
|
|
30
|
+
export * from './persona.js';
|
|
30
31
|
export { considerProjects, enableProjects, PROJECTS_DEFAULTS, projectAppeal } from './projects/index.js';
|
|
31
32
|
export * from './promptings/index.js';
|
|
32
33
|
export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, enablePlanning, FORECAST_DEFAULTS, forecastLine, forecastThresholds, PROSPECTION_DEFAULTS, seedConsequences, } from './prospection/index.js';
|
package/dist/migrate.js
CHANGED
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* version; fullness stays absent unless enabled, and alongside partners and reconsideration are opt-in.
|
|
19
19
|
* 2.8.0 to 2.9.0 stamps the version; prompting cues and practice half-lives stay absent unless supplied,
|
|
20
20
|
* review terms may carry refs, and an outcome naming several commitments now keeps each rather than only
|
|
21
|
-
* the earliest-ending one. 2.
|
|
21
|
+
* the earliest-ending one. 2.14.0 to 2.14.1 only stamps the version: slices without behaviour markers
|
|
22
|
+
* retain 2.14.0 crediting and an undecaying pull until the host re-enables them. 2.13.0 to 2.14.0 stamps the version (projects and persistence start absent; goals may carry `project`, received successors may carry `advances`). 2.12.0 to 2.13.0 stamps the version (suggestion reasons and firm rules start absent). 2.11.0 to 2.12.0 stamps the version (planning starts absent). 2.10.0 to 2.11.0 stamps the version (curiosity starts absent). 2.9.0 to 2.10.0 stamps the version; belief provenance, noted contradictions and
|
|
22
23
|
* believed regard stay absent unless a host supplies them;
|
|
23
24
|
* `regard` becomes a reserved impression cue key. A migrated save continues under the current engine's rules: it restores and runs,
|
|
24
25
|
* but it does not reproduce what the old engine would have done next where the rules changed (1.4.0 to 1.5.0
|
|
@@ -70,6 +71,11 @@ export const MIGRATIONS = {
|
|
|
70
71
|
note: 'the surroundings field `ambient.now` is renamed `ambient.percept`; per-person retention is optional and starts absent; behaviour unchanged',
|
|
71
72
|
apply: renameAmbient,
|
|
72
73
|
},
|
|
74
|
+
'2.14.0': {
|
|
75
|
+
to: '2.14.1',
|
|
76
|
+
note: 'version stamp only; restored projects and persistence retain 2.14.0 behaviour until explicitly re-enabled',
|
|
77
|
+
apply: stamp,
|
|
78
|
+
},
|
|
73
79
|
'2.13.0': {
|
|
74
80
|
to: '2.14.0',
|
|
75
81
|
note: 'projects (Person.projects) and persistence (Person.pursuit) are optional and start absent; Goal.project, Percept.prospect and received successor advances are optional; existing hosts retain exact behavior',
|
|
@@ -11,6 +11,7 @@ export declare const PERSISTENCE_DEFAULTS: {
|
|
|
11
11
|
readonly slackMinutes: 20;
|
|
12
12
|
readonly maxMinutes: 240;
|
|
13
13
|
readonly restMinutes: 20;
|
|
14
|
+
readonly stallHalfLifeMinutes: 30;
|
|
14
15
|
};
|
|
15
16
|
/** Enable or reconfigure, retaining a current pursuit. Invalid overrides use defaults. */
|
|
16
17
|
export declare function enablePersistence(p: Person, spec?: PersistenceSpec): PursuitState;
|
|
@@ -4,15 +4,19 @@
|
|
|
4
4
|
* unrelated options pay a switching cost (goal shielding: Shah, Friedman & Kruglanski 2002;
|
|
5
5
|
* task switching: Monsell 2003). Unused activation expires (Altmann & Trafton 2002); short
|
|
6
6
|
* interruptions allow resumption (Ovsiankina 1928). See research/projects-and-persistence.md.
|
|
7
|
-
* Maximum-duration release
|
|
7
|
+
* Maximum-duration release keeps one refractory entry, for the most recent release. With defaults
|
|
8
|
+
* (maxMinutes 240, restMinutes 20), two releases cannot fall within one interval; with a shorter
|
|
9
|
+
* maximum, a later release can replace an earlier unexpired interval.
|
|
8
10
|
* These are shapes, not fitted values; all coefficients and time bounds are engineering defaults.
|
|
9
11
|
* This is a bounded bias plus a switching cost, not a planner. It chooses no goals, executes no
|
|
10
12
|
* plans, reorders no steps and changes no agenda valuation. It imports no projects, uses no RNG,
|
|
11
13
|
* LLM or faith lens. Needs and fear gate both terms; commitment options never pay the switch cost.
|
|
14
|
+
* Behaviour 2 halves both terms with time since the pursued goal last advanced (or adoption),
|
|
15
|
+
* including after release and re-formation. Absent behaviour retains the 2.14.0 undecaying pull.
|
|
12
16
|
* Without the slice, it adds no state or terms.
|
|
13
17
|
*/
|
|
14
18
|
import { emotionLevel } from '../affect/index.js';
|
|
15
|
-
import { clamp01, cmpStr, isNum, isObj, round } from '../core/index.js';
|
|
19
|
+
import { clamp01, cmpStr, dpow, isNum, isObj, round } from '../core/index.js';
|
|
16
20
|
import { MAX_MINUTE, PHYSIOLOGICAL_NEEDS } from '../types.js';
|
|
17
21
|
/** Engineering defaults, relative to cognition's need scale 4, pleasure .6 and curiosity .8/.15.
|
|
18
22
|
* Continuation and switching together outweigh mild relaxed distraction; urgency and fear gate both.
|
|
@@ -26,6 +30,7 @@ export const PERSISTENCE_DEFAULTS = {
|
|
|
26
30
|
slackMinutes: 20,
|
|
27
31
|
maxMinutes: 240,
|
|
28
32
|
restMinutes: 20,
|
|
33
|
+
stallHalfLifeMinutes: 30,
|
|
29
34
|
};
|
|
30
35
|
const KEYS = [
|
|
31
36
|
'continueScale',
|
|
@@ -35,6 +40,7 @@ const KEYS = [
|
|
|
35
40
|
'slackMinutes',
|
|
36
41
|
'maxMinutes',
|
|
37
42
|
'restMinutes',
|
|
43
|
+
'stallHalfLifeMinutes',
|
|
38
44
|
];
|
|
39
45
|
function cleanSpec(input) {
|
|
40
46
|
const out = {};
|
|
@@ -42,8 +48,12 @@ function cleanSpec(input) {
|
|
|
42
48
|
return out;
|
|
43
49
|
for (const key of KEYS) {
|
|
44
50
|
const x = input[key];
|
|
45
|
-
const
|
|
46
|
-
|
|
51
|
+
const duration = key === 'slackMinutes' ||
|
|
52
|
+
key === 'maxMinutes' ||
|
|
53
|
+
key === 'restMinutes' ||
|
|
54
|
+
key === 'stallHalfLifeMinutes';
|
|
55
|
+
const min = key === 'stallHalfLifeMinutes' ? 1 : 0;
|
|
56
|
+
if (isNum(x) && x >= min && x <= (duration ? 1440 : 1))
|
|
47
57
|
out[key] = x;
|
|
48
58
|
}
|
|
49
59
|
return out;
|
|
@@ -53,7 +63,7 @@ const identifier = (x) => typeof x === 'string' && x.length > 0 && x.length <= 5
|
|
|
53
63
|
const active = (p, id) => p.agenda.goals.find((g) => g.id === id && g.status === 'active');
|
|
54
64
|
/** Enable or reconfigure, retaining a current pursuit. Invalid overrides use defaults. */
|
|
55
65
|
export function enablePersistence(p, spec = {}) {
|
|
56
|
-
const out = {};
|
|
66
|
+
const out = { behaviour: 2 };
|
|
57
67
|
const clean = cleanSpec(spec);
|
|
58
68
|
if (Object.keys(clean).length)
|
|
59
69
|
out.spec = clean;
|
|
@@ -126,8 +136,13 @@ export function beginPursuit(p, terms) {
|
|
|
126
136
|
export function persistenceTerms(p, terms, ctx) {
|
|
127
137
|
const s = p.pursuit;
|
|
128
138
|
const c = s?.current;
|
|
129
|
-
if (!s || !c
|
|
139
|
+
if (!s || !c)
|
|
140
|
+
return [];
|
|
141
|
+
const goal = active(p, c.goalId);
|
|
142
|
+
if (!goal)
|
|
130
143
|
return [];
|
|
144
|
+
const stall = Math.max(0, p.now - (goal.lastAdvancedAt ?? goal.adoptedAt));
|
|
145
|
+
const decay = s.behaviour === 2 ? dpow(0.5, stall / setting(s, 'stallHalfLifeMinutes')) : 1;
|
|
131
146
|
let urgency = 0;
|
|
132
147
|
for (const n of ctx.needs)
|
|
133
148
|
if (n.id === 'safety' || PHYSIOLOGICAL_NEEDS.includes(n.id))
|
|
@@ -137,7 +152,7 @@ export function persistenceTerms(p, terms, ctx) {
|
|
|
137
152
|
return [
|
|
138
153
|
{
|
|
139
154
|
source: `pursuit:${c.goalId}`,
|
|
140
|
-
value: round(Math.min(setting(s, 'cap'), setting(s, 'continueScale') * c.strength * gate)),
|
|
155
|
+
value: round(Math.min(setting(s, 'cap'), setting(s, 'continueScale') * c.strength * gate) * decay),
|
|
141
156
|
},
|
|
142
157
|
];
|
|
143
158
|
if (terms.some((t) => t.source.startsWith('commitment:')))
|
|
@@ -145,7 +160,7 @@ export function persistenceTerms(p, terms, ctx) {
|
|
|
145
160
|
return [
|
|
146
161
|
{
|
|
147
162
|
source: 'pursuit:switch',
|
|
148
|
-
value: -round(Math.min(setting(s, 'cap'), setting(s, 'switchScale') * c.strength * gate)),
|
|
163
|
+
value: -round(Math.min(setting(s, 'cap'), setting(s, 'switchScale') * c.strength * gate) * decay),
|
|
149
164
|
},
|
|
150
165
|
];
|
|
151
166
|
}
|
|
@@ -154,7 +169,11 @@ const minute = (x, now) => isNum(x) && Math.abs(x) <= MAX_MINUTE && x <= now;
|
|
|
154
169
|
export function sanitizePursuit(input, now) {
|
|
155
170
|
if (!isObj(input) || (input.spec !== undefined && !isObj(input.spec)))
|
|
156
171
|
return undefined;
|
|
172
|
+
if (input.behaviour !== undefined && input.behaviour !== 2)
|
|
173
|
+
return undefined;
|
|
157
174
|
const out = {};
|
|
175
|
+
if (input.behaviour === 2)
|
|
176
|
+
out.behaviour = 2;
|
|
158
177
|
if (input.spec !== undefined) {
|
|
159
178
|
const spec = cleanSpec(input.spec);
|
|
160
179
|
if (Object.keys(spec).length !== Object.keys(input.spec).length)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Commitment, Episode, Person, Skill } from './types.ts';
|
|
2
|
+
export interface PersonaEpisodeMap {
|
|
3
|
+
id: string;
|
|
4
|
+
action?: string;
|
|
5
|
+
actorId?: string;
|
|
6
|
+
targetId?: string;
|
|
7
|
+
placeId?: string;
|
|
8
|
+
voiceId?: string;
|
|
9
|
+
/** Tags are semantic keys too; omitted means no portable tags. */
|
|
10
|
+
tags?: string[];
|
|
11
|
+
}
|
|
12
|
+
export interface PersonaCommitmentMap {
|
|
13
|
+
id: string;
|
|
14
|
+
actions: string[];
|
|
15
|
+
toId?: string;
|
|
16
|
+
targetId?: string;
|
|
17
|
+
normId?: string;
|
|
18
|
+
chain?: string;
|
|
19
|
+
/** Explicit target-calendar window. Duration and recurrence must remain identical. */
|
|
20
|
+
from: number;
|
|
21
|
+
until: number;
|
|
22
|
+
}
|
|
23
|
+
export interface PersonaProjection {
|
|
24
|
+
version: string;
|
|
25
|
+
skills: Record<string, string>;
|
|
26
|
+
episodes?: Record<string, PersonaEpisodeMap>;
|
|
27
|
+
/** Every pending or continuing recurring obligation requires a named binding. */
|
|
28
|
+
commitments?: Record<string, PersonaCommitmentMap>;
|
|
29
|
+
}
|
|
30
|
+
export interface PersonaProjectionSnapshot {
|
|
31
|
+
format: 'hf-persona-state/1';
|
|
32
|
+
mappingVersion: string;
|
|
33
|
+
source: {
|
|
34
|
+
schema: number;
|
|
35
|
+
engine: string;
|
|
36
|
+
id: string;
|
|
37
|
+
now: number;
|
|
38
|
+
};
|
|
39
|
+
name: string;
|
|
40
|
+
life: Person['life'];
|
|
41
|
+
rng: Person['rng'];
|
|
42
|
+
traits: Person['traits'];
|
|
43
|
+
values: Person['values'];
|
|
44
|
+
health: {
|
|
45
|
+
health: number;
|
|
46
|
+
fitness: number;
|
|
47
|
+
circadianPeak: number;
|
|
48
|
+
};
|
|
49
|
+
family: Person['family'] | null;
|
|
50
|
+
bonds: Person['bonds'] | null;
|
|
51
|
+
skills: Record<string, Skill>;
|
|
52
|
+
skillRetention: Person['skillRetention'] | null;
|
|
53
|
+
episodes: Episode[];
|
|
54
|
+
/** Already bound to the target calendar by the explicitly versioned host mapping. */
|
|
55
|
+
commitments: Commitment[];
|
|
56
|
+
limitations: string[];
|
|
57
|
+
}
|
|
58
|
+
export interface PersonaAdmission {
|
|
59
|
+
mappingVersion: string;
|
|
60
|
+
sourceId: string;
|
|
61
|
+
targetId: string;
|
|
62
|
+
}
|
|
63
|
+
export interface PersonaAdmissionResult {
|
|
64
|
+
person: Person;
|
|
65
|
+
report: {
|
|
66
|
+
mappingVersion: string;
|
|
67
|
+
offset: number;
|
|
68
|
+
limitations: string[];
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** Validates untrusted portable data without restoring defaults or accepting extra live fields. */
|
|
72
|
+
export declare function validatePersonaProjection(input: unknown): PersonaProjectionSnapshot;
|
|
73
|
+
/** Hosts must migrate supported old saves first. No random draws or mutation occur here. */
|
|
74
|
+
export declare function projectPersona(source: Person, mapping: PersonaProjection): PersonaProjectionSnapshot;
|
|
75
|
+
/** Return a detached target opening; a later shelf change cannot affect it. Host owns cast/branch/death authority. */
|
|
76
|
+
export declare function admitPersona(input: unknown, target: Person, policy: PersonaAdmission): PersonaAdmissionResult;
|
package/dist/persona.js
ADDED
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCOPE: opt-in, deterministic projection between host-authored chapter openings. Copies exact temperament,
|
|
3
|
+
* values and RNG, explicitly mapped skills, episodes and commitments, with declared clock offsets. Checks
|
|
4
|
+
* identity, age, family and health compatibility before returning a new Person. Neither input is mutated.
|
|
5
|
+
* This is not a world migration, identity registry, transport, hash, authentication or general calendar mapper.
|
|
6
|
+
* v1 admits zero-duration travel and healthy compatible bodies only; family/bonds remain host-authored and must
|
|
7
|
+
* match source provenance. Pregnancy and nonempty temporal bonds are unsupported in v1. All other world-dependent stores stay with the source and are disclosed as omitted.
|
|
8
|
+
*/
|
|
9
|
+
import { hasUnresolvedMakeUps, sameChain } from './agenda/agenda.js';
|
|
10
|
+
import { createPerson } from './person.js';
|
|
11
|
+
import { restore, snapshot } from './restore.js';
|
|
12
|
+
import { ENGINE_VERSION, MAX_MINUTE, PERSON_SCHEMA } from './types.js';
|
|
13
|
+
function fail(reason) {
|
|
14
|
+
throw new Error(`persona: ${reason}`);
|
|
15
|
+
}
|
|
16
|
+
function obj(x) {
|
|
17
|
+
if (!x || typeof x !== 'object' || Array.isArray(x))
|
|
18
|
+
fail('expected object');
|
|
19
|
+
}
|
|
20
|
+
function keys(x, allowed, required = allowed) {
|
|
21
|
+
obj(x);
|
|
22
|
+
if (Object.keys(x).some((k) => !allowed.includes(k)) || required.some((k) => !Object.hasOwn(x, k)))
|
|
23
|
+
fail('unsupported or missing field');
|
|
24
|
+
}
|
|
25
|
+
function text(x) {
|
|
26
|
+
if (typeof x !== 'string' ||
|
|
27
|
+
!x.length ||
|
|
28
|
+
x.length > 1024 ||
|
|
29
|
+
['__proto__', 'constructor', 'prototype'].includes(x))
|
|
30
|
+
fail('invalid text/reference');
|
|
31
|
+
}
|
|
32
|
+
function num(x, min = -MAX_MINUTE, max = MAX_MINUTE) {
|
|
33
|
+
if (typeof x !== 'number' || !Number.isFinite(x) || x < min || x > max)
|
|
34
|
+
fail('invalid number/timestamp');
|
|
35
|
+
}
|
|
36
|
+
function texts(x, max = 128) {
|
|
37
|
+
if (!Array.isArray(x) || x.length > max)
|
|
38
|
+
fail('invalid list');
|
|
39
|
+
for (const value of x)
|
|
40
|
+
text(value);
|
|
41
|
+
}
|
|
42
|
+
function canonical(x) {
|
|
43
|
+
return JSON.stringify(x, (_key, value) => value && typeof value === 'object' && !Array.isArray(value)
|
|
44
|
+
? Object.fromEntries(Object.entries(value).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))
|
|
45
|
+
: value);
|
|
46
|
+
}
|
|
47
|
+
function same(a, b) {
|
|
48
|
+
return canonical(a) === canonical(b);
|
|
49
|
+
}
|
|
50
|
+
function compatibleBody(p) {
|
|
51
|
+
if (!p.body.alive)
|
|
52
|
+
fail('dead person cannot travel or be revived');
|
|
53
|
+
if (p.body.health <= 0 ||
|
|
54
|
+
p.body.injuries.length ||
|
|
55
|
+
p.body.illnesses.length ||
|
|
56
|
+
p.body.downed ||
|
|
57
|
+
Object.keys(p.body.exposures ?? {}).length)
|
|
58
|
+
fail('unsupported health condition');
|
|
59
|
+
}
|
|
60
|
+
function boundRecord(x) {
|
|
61
|
+
obj(x);
|
|
62
|
+
if (Object.keys(x).length > 128)
|
|
63
|
+
fail('too many mappings');
|
|
64
|
+
for (const key of Object.keys(x))
|
|
65
|
+
text(key);
|
|
66
|
+
}
|
|
67
|
+
function episode(x) {
|
|
68
|
+
keys(x, [
|
|
69
|
+
'id',
|
|
70
|
+
'at',
|
|
71
|
+
'kind',
|
|
72
|
+
'action',
|
|
73
|
+
'actorId',
|
|
74
|
+
'targetId',
|
|
75
|
+
'placeId',
|
|
76
|
+
'valence',
|
|
77
|
+
'salience',
|
|
78
|
+
'summary',
|
|
79
|
+
'tags',
|
|
80
|
+
'voiceId',
|
|
81
|
+
'recalledAt',
|
|
82
|
+
], ['id', 'at', 'kind', 'valence', 'salience', 'summary', 'tags']);
|
|
83
|
+
for (const key of ['id', 'kind', 'summary', 'action', 'actorId', 'targetId', 'placeId', 'voiceId'])
|
|
84
|
+
if (x[key] !== undefined)
|
|
85
|
+
text(x[key]);
|
|
86
|
+
num(x.at);
|
|
87
|
+
num(x.valence, -1, 1);
|
|
88
|
+
num(x.salience, 0, 1);
|
|
89
|
+
texts(x.tags);
|
|
90
|
+
if (x.recalledAt !== undefined)
|
|
91
|
+
num(x.recalledAt);
|
|
92
|
+
}
|
|
93
|
+
function commitment(x) {
|
|
94
|
+
keys(x, [
|
|
95
|
+
'id',
|
|
96
|
+
'kind',
|
|
97
|
+
'toId',
|
|
98
|
+
'actions',
|
|
99
|
+
'targetId',
|
|
100
|
+
'from',
|
|
101
|
+
'until',
|
|
102
|
+
'normId',
|
|
103
|
+
'importance',
|
|
104
|
+
'status',
|
|
105
|
+
'recurEvery',
|
|
106
|
+
'label',
|
|
107
|
+
'chain',
|
|
108
|
+
], ['id', 'kind', 'actions', 'from', 'until', 'importance', 'status']);
|
|
109
|
+
for (const key of ['id', 'toId', 'targetId', 'normId', 'label', 'chain'])
|
|
110
|
+
if (x[key] !== undefined)
|
|
111
|
+
text(x[key]);
|
|
112
|
+
if (!['promise', 'duty', 'appointment', 'worship', 'job'].includes(String(x.kind)) ||
|
|
113
|
+
x.status !== 'pending')
|
|
114
|
+
fail('unsupported obligation');
|
|
115
|
+
texts(x.actions);
|
|
116
|
+
if (!x.actions.length)
|
|
117
|
+
fail('empty obligation actions');
|
|
118
|
+
num(x.from);
|
|
119
|
+
num(x.until);
|
|
120
|
+
if (x.until < x.from)
|
|
121
|
+
fail('invalid obligation window');
|
|
122
|
+
num(x.importance, 0, 1);
|
|
123
|
+
if (x.recurEvery !== undefined)
|
|
124
|
+
num(x.recurEvery, 1);
|
|
125
|
+
}
|
|
126
|
+
function boundedJson(input) {
|
|
127
|
+
const pending = [{ value: input, depth: 0 }];
|
|
128
|
+
const seen = new Set();
|
|
129
|
+
let nodes = 0;
|
|
130
|
+
while (pending.length) {
|
|
131
|
+
const item = pending.pop();
|
|
132
|
+
if (!item)
|
|
133
|
+
break;
|
|
134
|
+
if (++nodes > 8192 || item.depth > 16)
|
|
135
|
+
fail('JSON bounds exceeded');
|
|
136
|
+
const { value, depth } = item;
|
|
137
|
+
if (value === null || typeof value === 'boolean')
|
|
138
|
+
continue;
|
|
139
|
+
if (typeof value === 'string') {
|
|
140
|
+
if (value.length > 1024)
|
|
141
|
+
fail('text bounds exceeded');
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (typeof value === 'number') {
|
|
145
|
+
num(value);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (typeof value !== 'object' || seen.has(value))
|
|
149
|
+
fail('non-JSON or cyclic input');
|
|
150
|
+
seen.add(value);
|
|
151
|
+
if (!Array.isArray(value) &&
|
|
152
|
+
Object.getPrototypeOf(value) !== Object.prototype &&
|
|
153
|
+
Object.getPrototypeOf(value) !== null)
|
|
154
|
+
fail('non-JSON object');
|
|
155
|
+
for (const [key, child] of Object.entries(value)) {
|
|
156
|
+
if (['__proto__', 'prototype', 'constructor'].includes(key))
|
|
157
|
+
fail('unsafe reference');
|
|
158
|
+
pending.push({ value: child, depth: depth + 1 });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/** Validates untrusted portable data without restoring defaults or accepting extra live fields. */
|
|
163
|
+
export function validatePersonaProjection(input) {
|
|
164
|
+
boundedJson(input);
|
|
165
|
+
keys(input, [
|
|
166
|
+
'format',
|
|
167
|
+
'mappingVersion',
|
|
168
|
+
'source',
|
|
169
|
+
'name',
|
|
170
|
+
'life',
|
|
171
|
+
'rng',
|
|
172
|
+
'traits',
|
|
173
|
+
'values',
|
|
174
|
+
'health',
|
|
175
|
+
'family',
|
|
176
|
+
'bonds',
|
|
177
|
+
'skills',
|
|
178
|
+
'skillRetention',
|
|
179
|
+
'episodes',
|
|
180
|
+
'commitments',
|
|
181
|
+
'limitations',
|
|
182
|
+
]);
|
|
183
|
+
if (input.format !== 'hf-persona-state/1')
|
|
184
|
+
fail('unsupported format');
|
|
185
|
+
text(input.mappingVersion);
|
|
186
|
+
text(input.name);
|
|
187
|
+
keys(input.source, ['schema', 'engine', 'id', 'now']);
|
|
188
|
+
if (input.source.schema !== PERSON_SCHEMA || input.source.engine !== ENGINE_VERSION)
|
|
189
|
+
fail('unsupported source schema/engine; migrate the source before projection');
|
|
190
|
+
text(input.source.id);
|
|
191
|
+
num(input.source.now);
|
|
192
|
+
keys(input.life, ['bornAt', 'sex']);
|
|
193
|
+
num(input.life.bornAt);
|
|
194
|
+
if (!['female', 'male'].includes(String(input.life.sex)))
|
|
195
|
+
fail('invalid sex');
|
|
196
|
+
keys(input.rng, ['a', 'b', 'c', 'd']);
|
|
197
|
+
for (const [key, value] of Object.entries(input.rng)) {
|
|
198
|
+
num(value, key === 'a' ? -0x80000000 : 0, key === 'a' ? 0x7fffffff : 0xffffffff);
|
|
199
|
+
if (!Number.isInteger(value))
|
|
200
|
+
fail('invalid RNG');
|
|
201
|
+
}
|
|
202
|
+
const base = createPerson({ id: 'validation', name: 'validation', seed: 0, bornAt: 0, sex: 'female' });
|
|
203
|
+
for (const slice of ['traits', 'values']) {
|
|
204
|
+
keys(input[slice], Object.keys(base[slice]));
|
|
205
|
+
for (const value of Object.values(input[slice]))
|
|
206
|
+
num(value, 0, 1);
|
|
207
|
+
}
|
|
208
|
+
keys(input.health, ['health', 'fitness', 'circadianPeak']);
|
|
209
|
+
num(input.health.health, Number.MIN_VALUE, 1);
|
|
210
|
+
num(input.health.fitness, 0, 1);
|
|
211
|
+
num(input.health.circadianPeak, 0, 1440);
|
|
212
|
+
boundRecord(input.skills);
|
|
213
|
+
for (const value of Object.values(input.skills)) {
|
|
214
|
+
keys(value, ['level', 'practice', 'lastPracticed']);
|
|
215
|
+
num(value.level, 0, 1);
|
|
216
|
+
num(value.practice, 0);
|
|
217
|
+
num(value.lastPracticed);
|
|
218
|
+
if (value.lastPracticed > input.source.now)
|
|
219
|
+
fail('future skill evidence');
|
|
220
|
+
}
|
|
221
|
+
if (input.skillRetention !== null) {
|
|
222
|
+
keys(input.skillRetention, ['consolidationHours']);
|
|
223
|
+
num(input.skillRetention.consolidationHours, Number.MIN_VALUE);
|
|
224
|
+
}
|
|
225
|
+
if (input.family !== null) {
|
|
226
|
+
obj(input.family);
|
|
227
|
+
if (input.family.pregnancy !== undefined)
|
|
228
|
+
fail('unsupported temporal family state: pregnancy');
|
|
229
|
+
}
|
|
230
|
+
if (input.bonds !== null) {
|
|
231
|
+
obj(input.bonds);
|
|
232
|
+
if (input.bonds.mourning !== undefined ||
|
|
233
|
+
(Array.isArray(input.bonds.courtships) && input.bonds.courtships.length > 0) ||
|
|
234
|
+
(Array.isArray(input.bonds.marriages) && input.bonds.marriages.length > 0)) {
|
|
235
|
+
fail('unsupported temporal bonds state');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
for (const field of ['family', 'bonds']) {
|
|
239
|
+
if (input[field] !== null) {
|
|
240
|
+
obj(input[field]);
|
|
241
|
+
const check = snapshot(base);
|
|
242
|
+
Object.assign(check, { [field]: input[field] });
|
|
243
|
+
if (!same(restore(check)[field], input[field]))
|
|
244
|
+
fail(`invalid ${field} provenance`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (!Array.isArray(input.episodes) ||
|
|
248
|
+
input.episodes.length > 128 ||
|
|
249
|
+
!Array.isArray(input.commitments) ||
|
|
250
|
+
input.commitments.length > 128)
|
|
251
|
+
fail('invalid portable lists');
|
|
252
|
+
for (const value of input.episodes) {
|
|
253
|
+
episode(value);
|
|
254
|
+
if (value.at > input.source.now ||
|
|
255
|
+
(value.recalledAt !== undefined && value.recalledAt > input.source.now))
|
|
256
|
+
fail('future episode evidence');
|
|
257
|
+
}
|
|
258
|
+
for (const value of input.commitments)
|
|
259
|
+
commitment(value);
|
|
260
|
+
for (const list of [input.episodes, input.commitments])
|
|
261
|
+
if (new Set(list.map((x) => x.id)).size !== list.length)
|
|
262
|
+
fail('duplicate portable id');
|
|
263
|
+
texts(input.limitations, 256);
|
|
264
|
+
return structuredClone(input);
|
|
265
|
+
}
|
|
266
|
+
/** Hosts must migrate supported old saves first. No random draws or mutation occur here. */
|
|
267
|
+
export function projectPersona(source, mapping) {
|
|
268
|
+
const p = restore(snapshot(source));
|
|
269
|
+
compatibleBody(p);
|
|
270
|
+
if (p.activity)
|
|
271
|
+
fail('running activity');
|
|
272
|
+
if (p.agenda.goals.some((g) => g.status === 'active') || p.projects?.currentGoalId)
|
|
273
|
+
fail('active goal/project');
|
|
274
|
+
if (hasUnresolvedMakeUps(p) || p.agenda.lapse)
|
|
275
|
+
fail('unmapped owed obligation');
|
|
276
|
+
text(mapping.version);
|
|
277
|
+
boundRecord(mapping.skills);
|
|
278
|
+
const skills = Object.create(null);
|
|
279
|
+
for (const [from, to] of Object.entries(mapping.skills).sort()) {
|
|
280
|
+
text(to);
|
|
281
|
+
const skill = p.skills[from];
|
|
282
|
+
if (!Object.hasOwn(p.skills, from) || !skill || Object.hasOwn(skills, to))
|
|
283
|
+
fail('missing/duplicate skill mapping');
|
|
284
|
+
skills[to] = structuredClone(skill);
|
|
285
|
+
}
|
|
286
|
+
const episodes = [];
|
|
287
|
+
for (const [id, map] of Object.entries(mapping.episodes ?? {}).sort()) {
|
|
288
|
+
const old = p.memory.episodes.find((e) => e.id === id);
|
|
289
|
+
if (!old)
|
|
290
|
+
fail('missing approved episode');
|
|
291
|
+
keys(map, ['id', 'action', 'actorId', 'targetId', 'placeId', 'voiceId', 'tags'], ['id']);
|
|
292
|
+
// Every existing semantic reference needs a declared mapping; omission must never preserve host-local ids.
|
|
293
|
+
for (const ref of ['action', 'actorId', 'targetId', 'placeId', 'voiceId'])
|
|
294
|
+
if (old[ref] !== undefined && map[ref] === undefined)
|
|
295
|
+
fail(`unmapped episode ${ref}`);
|
|
296
|
+
const copy = {
|
|
297
|
+
id: map.id,
|
|
298
|
+
at: old.at,
|
|
299
|
+
kind: old.kind,
|
|
300
|
+
valence: old.valence,
|
|
301
|
+
salience: old.salience,
|
|
302
|
+
summary: old.summary,
|
|
303
|
+
tags: map.tags ?? [],
|
|
304
|
+
};
|
|
305
|
+
for (const ref of ['action', 'actorId', 'targetId', 'placeId', 'voiceId'])
|
|
306
|
+
if (map[ref] !== undefined)
|
|
307
|
+
copy[ref] = map[ref];
|
|
308
|
+
if (old.recalledAt !== undefined)
|
|
309
|
+
copy.recalledAt = old.recalledAt;
|
|
310
|
+
episodes.push(copy);
|
|
311
|
+
}
|
|
312
|
+
const commitments = [];
|
|
313
|
+
const bindings = mapping.commitments ?? {};
|
|
314
|
+
for (const old of p.agenda.commitments) {
|
|
315
|
+
// Closed history no longer owns the chain once an actual later instance exists, including a release.
|
|
316
|
+
if (old.status !== 'pending' &&
|
|
317
|
+
p.agenda.commitments.some((next) => next.from > old.from && sameChain(old, next)))
|
|
318
|
+
continue;
|
|
319
|
+
const ongoing = old.status === 'pending' ||
|
|
320
|
+
(old.recurEvery !== undefined &&
|
|
321
|
+
old.status !== 'released' &&
|
|
322
|
+
(old.recurUntil === undefined || old.recurUntil > p.now));
|
|
323
|
+
if (!ongoing)
|
|
324
|
+
continue;
|
|
325
|
+
const map = Object.hasOwn(bindings, old.id) ? bindings[old.id] : undefined;
|
|
326
|
+
if (!map || old.status !== 'pending')
|
|
327
|
+
fail(`unmapped obligation ${old.id}`);
|
|
328
|
+
keys(map, ['id', 'actions', 'toId', 'targetId', 'normId', 'chain', 'from', 'until'], ['id', 'actions', 'from', 'until']);
|
|
329
|
+
if (old.until - old.from !== map.until - map.from)
|
|
330
|
+
fail('obligation duration changed');
|
|
331
|
+
for (const ref of ['toId', 'targetId', 'normId', 'chain'])
|
|
332
|
+
if (old[ref] !== undefined && map[ref] === undefined)
|
|
333
|
+
fail(`unmapped obligation ${ref}`);
|
|
334
|
+
const copy = {
|
|
335
|
+
id: map.id,
|
|
336
|
+
kind: old.kind,
|
|
337
|
+
actions: [...map.actions],
|
|
338
|
+
from: map.from,
|
|
339
|
+
until: map.until,
|
|
340
|
+
importance: old.importance,
|
|
341
|
+
status: 'pending',
|
|
342
|
+
};
|
|
343
|
+
for (const ref of ['toId', 'targetId', 'normId', 'chain'])
|
|
344
|
+
if (map[ref] !== undefined)
|
|
345
|
+
copy[ref] = map[ref];
|
|
346
|
+
if (old.recurEvery !== undefined)
|
|
347
|
+
copy.recurEvery = old.recurEvery;
|
|
348
|
+
if (old.label !== undefined)
|
|
349
|
+
copy.label = old.label;
|
|
350
|
+
if (old.exempt ||
|
|
351
|
+
old.makeUpOf ||
|
|
352
|
+
old.recurUntil !== undefined ||
|
|
353
|
+
old.violatedBy ||
|
|
354
|
+
old.firm ||
|
|
355
|
+
old.exemptWhen)
|
|
356
|
+
fail('unsupported obligation semantics');
|
|
357
|
+
commitments.push(copy);
|
|
358
|
+
}
|
|
359
|
+
for (const id of Object.keys(bindings))
|
|
360
|
+
if (!p.agenda.commitments.some((c) => c.id === id && c.status === 'pending'))
|
|
361
|
+
fail('missing obligation binding');
|
|
362
|
+
const limitations = [
|
|
363
|
+
'source transient body/needs/affect/ambient omitted; target arrival policy',
|
|
364
|
+
'source habits/methods/beliefs/social/conscience omitted; target authored state',
|
|
365
|
+
'source optional behaviour stores and decision history omitted; target authored state',
|
|
366
|
+
'family/bonds provenance checked; target authored state',
|
|
367
|
+
];
|
|
368
|
+
for (const key of Object.keys(p.skills).sort())
|
|
369
|
+
if (!Object.hasOwn(mapping.skills, key))
|
|
370
|
+
limitations.push(`skill:${key}`);
|
|
371
|
+
for (const e of p.memory.episodes)
|
|
372
|
+
if (!Object.hasOwn(mapping.episodes ?? {}, e.id))
|
|
373
|
+
limitations.push(`episode:${e.id}`);
|
|
374
|
+
return validatePersonaProjection({
|
|
375
|
+
format: 'hf-persona-state/1',
|
|
376
|
+
mappingVersion: mapping.version,
|
|
377
|
+
source: { schema: source.schema, engine: p.engine, id: p.id, now: p.now },
|
|
378
|
+
name: p.name,
|
|
379
|
+
life: p.life,
|
|
380
|
+
rng: p.rng,
|
|
381
|
+
traits: p.traits,
|
|
382
|
+
values: p.values,
|
|
383
|
+
health: { health: p.body.health, fitness: p.body.fitness, circadianPeak: p.body.circadianPeak },
|
|
384
|
+
family: p.family ?? null,
|
|
385
|
+
bonds: p.bonds ?? null,
|
|
386
|
+
skills,
|
|
387
|
+
skillRetention: p.skillRetention ?? null,
|
|
388
|
+
episodes,
|
|
389
|
+
commitments,
|
|
390
|
+
limitations,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
/** Return a detached target opening; a later shelf change cannot affect it. Host owns cast/branch/death authority. */
|
|
394
|
+
export function admitPersona(input, target, policy) {
|
|
395
|
+
const projection = validatePersonaProjection(input);
|
|
396
|
+
const p = restore(snapshot(target));
|
|
397
|
+
if (projection.mappingVersion !== policy.mappingVersion)
|
|
398
|
+
fail('unsupported mapping');
|
|
399
|
+
if (projection.source.id !== policy.sourceId || p.id !== policy.targetId)
|
|
400
|
+
fail('identity binding conflict');
|
|
401
|
+
compatibleBody(p);
|
|
402
|
+
const offset = p.now - projection.source.now;
|
|
403
|
+
const shift = (at) => {
|
|
404
|
+
const mapped = at + offset;
|
|
405
|
+
num(mapped);
|
|
406
|
+
return mapped;
|
|
407
|
+
};
|
|
408
|
+
if (p.life.sex !== projection.life.sex ||
|
|
409
|
+
p.life.bornAt !== shift(projection.life.bornAt) ||
|
|
410
|
+
p.name !== projection.name)
|
|
411
|
+
fail('identity/age conflict');
|
|
412
|
+
if (!same(p.family ?? null, projection.family) || !same(p.bonds ?? null, projection.bonds))
|
|
413
|
+
fail('family/bonds conflict');
|
|
414
|
+
if (!same({ health: p.body.health, fitness: p.body.fitness, circadianPeak: p.body.circadianPeak }, projection.health))
|
|
415
|
+
fail('health compatibility conflict');
|
|
416
|
+
if (p.activity)
|
|
417
|
+
fail('target already active');
|
|
418
|
+
p.traits = projection.traits;
|
|
419
|
+
p.values = projection.values;
|
|
420
|
+
p.rng = projection.rng;
|
|
421
|
+
// A sparse projection replaces target priors for portable skill/history stores, never blends evidence.
|
|
422
|
+
p.skills = projection.skills;
|
|
423
|
+
if (projection.skillRetention)
|
|
424
|
+
p.skillRetention = projection.skillRetention;
|
|
425
|
+
else
|
|
426
|
+
delete p.skillRetention;
|
|
427
|
+
for (const skill of Object.values(p.skills))
|
|
428
|
+
skill.lastPracticed = shift(skill.lastPracticed);
|
|
429
|
+
p.memory.episodes = projection.episodes;
|
|
430
|
+
for (const e of p.memory.episodes) {
|
|
431
|
+
e.at = shift(e.at);
|
|
432
|
+
if (e.recalledAt !== undefined)
|
|
433
|
+
e.recalledAt = shift(e.recalledAt);
|
|
434
|
+
}
|
|
435
|
+
p.memory.nextEpisode = Math.max(p.memory.nextEpisode, p.memory.episodes.length + 1);
|
|
436
|
+
for (const e of p.memory.episodes) {
|
|
437
|
+
if (/^e\d+$/.test(e.id)) {
|
|
438
|
+
const next = Number(e.id.slice(1)) + 1;
|
|
439
|
+
num(next, 0);
|
|
440
|
+
p.memory.nextEpisode = Math.max(p.memory.nextEpisode, next);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
for (const c of projection.commitments) {
|
|
444
|
+
if (c.until < p.now ||
|
|
445
|
+
p.agenda.commitments.some((old) => old.id === c.id || (c.chain && old.chain === c.chain)))
|
|
446
|
+
fail('target obligation conflict');
|
|
447
|
+
p.agenda.commitments.push(c);
|
|
448
|
+
}
|
|
449
|
+
p.agenda.nextId = Math.max(p.agenda.nextId, p.agenda.commitments.length + 1);
|
|
450
|
+
const restored = restore(p);
|
|
451
|
+
if (!same(restored, p))
|
|
452
|
+
fail('admitted state exceeds supported bounds');
|
|
453
|
+
return {
|
|
454
|
+
person: restored,
|
|
455
|
+
report: { mappingVersion: policy.mappingVersion, offset, limitations: [...projection.limitations] },
|
|
456
|
+
};
|
|
457
|
+
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* models a pull to continue, not a Zeigarnik memory advantage. See research/projects-and-persistence.md.
|
|
8
8
|
* All thresholds and coefficients are engineering defaults. This owns only p.projects; agenda owns
|
|
9
9
|
* goals. All kinds serve competence and leisure: making is chosen as a free-time pursuit, not duty.
|
|
10
|
+
* Behaviour 2 credits only new perceived highs; event-credited projects cannot observe reversals and stay unchanged.
|
|
10
11
|
* Restore recovers one active project, keeping the newest (id breaks ties) and abandoning extras.
|
|
11
12
|
* No invented prospects, host task interpretation, world search, persistence, shielding, faith lens,
|
|
12
13
|
* LLM or RNG. Without the slice, perception and minute bookkeeping write nothing.
|
|
@@ -70,7 +71,7 @@ function cleanProspect(x) {
|
|
|
70
71
|
p.placeId = x.placeId;
|
|
71
72
|
if (x.progress !== undefined)
|
|
72
73
|
p.progress = x.progress;
|
|
73
|
-
// A long list is kept, not rejected: the first eight distinct ids in the host's order (
|
|
74
|
+
// A long list is kept, not rejected: the first eight distinct ids in the host's order (host rerun, 2026-10-20).
|
|
74
75
|
if (x.materialIds !== undefined)
|
|
75
76
|
p.materialIds = [...new Set(x.materialIds)].slice(0, 8);
|
|
76
77
|
return p;
|
|
@@ -78,6 +79,10 @@ function cleanProspect(x) {
|
|
|
78
79
|
/** Enable or reconfigure without discarding retained prospects and history. */
|
|
79
80
|
export function enableProjects(p, spec = {}) {
|
|
80
81
|
const state = p.projects ?? { prospects: [], history: [] };
|
|
82
|
+
for (const g of p.agenda.goals)
|
|
83
|
+
if (g.status === 'active' && g.project?.perceivedProgress && g.project.peak === undefined)
|
|
84
|
+
g.project.peak = g.progress;
|
|
85
|
+
state.behaviour = 2;
|
|
81
86
|
const clean = cleanSpec(spec);
|
|
82
87
|
delete state.spec;
|
|
83
88
|
if (Object.keys(clean).length)
|
|
@@ -228,6 +233,8 @@ export function considerProjects(p, now) {
|
|
|
228
233
|
}, now);
|
|
229
234
|
if (prospect.progress !== undefined)
|
|
230
235
|
perceiveProjectProgress(p, prospect.id, prospect.progress, now);
|
|
236
|
+
if (s.behaviour === 2 && g.project)
|
|
237
|
+
g.project.peak = g.progress;
|
|
231
238
|
s.lastAdoptedAt = now;
|
|
232
239
|
s.currentGoalId = g.id;
|
|
233
240
|
}
|
|
@@ -235,7 +242,11 @@ export function considerProjects(p, now) {
|
|
|
235
242
|
export function sanitizeProjects(input, now) {
|
|
236
243
|
if (!isObj(input) || !Array.isArray(input.prospects) || !Array.isArray(input.history))
|
|
237
244
|
return undefined;
|
|
245
|
+
if (input.behaviour !== undefined && input.behaviour !== 2)
|
|
246
|
+
return undefined;
|
|
238
247
|
const out = { prospects: [], history: [] };
|
|
248
|
+
if (input.behaviour === 2)
|
|
249
|
+
out.behaviour = 2;
|
|
239
250
|
if (input.spec !== undefined) {
|
|
240
251
|
if (!isObj(input.spec))
|
|
241
252
|
return undefined;
|
package/dist/types.d.ts
CHANGED
|
@@ -592,6 +592,8 @@ export interface Goal {
|
|
|
592
592
|
project?: {
|
|
593
593
|
prospectId: string;
|
|
594
594
|
perceivedProgress: boolean;
|
|
595
|
+
/** Highest perceived progress since adoption; presence opts this goal into net credit. */
|
|
596
|
+
peak?: Unit;
|
|
595
597
|
};
|
|
596
598
|
id: string;
|
|
597
599
|
label: string;
|
|
@@ -894,7 +896,7 @@ export declare const PERSON_SCHEMA = "human/person@1";
|
|
|
894
896
|
* and an activity that would cover a closing stretch is reviewed when the stretch begins (`isUnderWay`,
|
|
895
897
|
* `dutyReviewAt`). Person shape unchanged.
|
|
896
898
|
*/
|
|
897
|
-
export declare const ENGINE_VERSION = "2.14.
|
|
899
|
+
export declare const ENGINE_VERSION = "2.14.1";
|
|
898
900
|
/** A pregnancy carried by this person (host-driven; see `family/`). */
|
|
899
901
|
export interface Pregnancy {
|
|
900
902
|
fatherId: PersonId;
|
|
@@ -2003,6 +2005,8 @@ export interface ProjectHistory {
|
|
|
2003
2005
|
}
|
|
2004
2006
|
/** Optional bounded JSON state owned by projects/. */
|
|
2005
2007
|
export interface ProjectsState {
|
|
2008
|
+
/** Absent retains 2.14.0 credit. Re-enabling, including on every load, opts into net credit. */
|
|
2009
|
+
behaviour?: 2;
|
|
2006
2010
|
spec?: ProjectsSpec;
|
|
2007
2011
|
prospects: (Prospect & {
|
|
2008
2012
|
lastSeenAt: Minute;
|
|
@@ -2015,6 +2019,8 @@ export interface ProjectsState {
|
|
|
2015
2019
|
}
|
|
2016
2020
|
/** Bounded engineering overrides for persistence/. Invalid overrides use defaults. */
|
|
2017
2021
|
export interface PersistenceSpec {
|
|
2022
|
+
/** Half-life without goal progress, in minutes (default 30; valid 1..1440). */
|
|
2023
|
+
stallHalfLifeMinutes?: number;
|
|
2018
2024
|
continueScale?: number;
|
|
2019
2025
|
switchScale?: number;
|
|
2020
2026
|
cap?: number;
|
|
@@ -2033,9 +2039,13 @@ export interface Pursuit {
|
|
|
2033
2039
|
}
|
|
2034
2040
|
/** Optional JSON state owned by persistence/: one current pursuit and one refractory goal. */
|
|
2035
2041
|
export interface PursuitState {
|
|
2042
|
+
/** Absent retains the 2.14.0 pull. Re-enabling, including on every load, opts into decay. */
|
|
2043
|
+
behaviour?: 2;
|
|
2036
2044
|
spec?: PersistenceSpec;
|
|
2037
2045
|
current?: Pursuit;
|
|
2038
|
-
/**
|
|
2046
|
+
/** Most recent maximum-duration release, withheld until this minute; agenda terms stay unchanged.
|
|
2047
|
+
* Defaults (maxMinutes 240, restMinutes 20) prevent two releases within one interval.
|
|
2048
|
+
* A shorter maximum can let a later release replace an earlier unexpired interval. */
|
|
2039
2049
|
rested?: {
|
|
2040
2050
|
goalId: string;
|
|
2041
2051
|
until: Minute;
|
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.14.
|
|
123
|
+
export const ENGINE_VERSION = '2.14.1';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adam0white/human-framework",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.15.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",
|