@adam0white/human-framework 2.1.4 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -52,7 +52,7 @@ Each [GitHub release](https://github.com/adam0white/human-framework/releases) al
52
52
  installs by URL:
53
53
 
54
54
  ```sh
55
- npm install https://github.com/adam0white/human-framework/releases/download/v2.1.4/adam0white-human-framework-2.1.4.tgz
55
+ npm install https://github.com/adam0white/human-framework/releases/download/v2.2.0/adam0white-human-framework-2.2.0.tgz
56
56
  ```
57
57
 
58
58
  or build one from a checkout:
@@ -177,13 +177,13 @@ reference host: `createVillage`.
177
177
 
178
178
  There are two version numbers, and they move independently.
179
179
 
180
- - **The release version** is the package version (`package.json`, now `2.0.0`) and `FRAMEWORK_VERSION`, the same
180
+ - **The release version** is the package version (`package.json`, now `2.2.0`) and `FRAMEWORK_VERSION`, the same
181
181
  string compiled into the build; a test keeps them equal. It versions the public API under
182
182
  [semver](https://semver.org/): a breaking change to an exported name or signature bumps the major, a new
183
183
  faculty or export the minor, a fix the patch. Each release is a tag `vX.Y.Z` and a GitHub release whose notes
184
184
  are that version's section of the root [CHANGELOG.md](../../CHANGELOG.md), with the package tarball attached
185
185
  (`npm run release`, run on a clean, pushed `main`).
186
- - **`ENGINE_VERSION`** (exported; `2.0.0` at release 2.0.0) versions simulation behaviour and the save format. It
186
+ - **`ENGINE_VERSION`** (exported; `2.1.0` at release 2.2.0) versions simulation behaviour and the save format. It
187
187
  changes when the same seed and inputs would give different decisions, or when person state changes shape.
188
188
  `restore` upgrades saves from engine 1.4.0 and later through `migrate` and refuses older ones. Every release's notes name the
189
189
  `ENGINE_VERSION` it ships, so a host can tell whether its saves still restore.
@@ -274,3 +274,31 @@ that place lose appeal until the fear fades. Groups and factions themselves stay
274
274
 
275
275
  MIT; see [LICENSE](LICENSE). The repository's `docs/` and `research/`, which this README links to, are
276
276
  CC BY 4.0.
277
+
278
+ ## Learning a demonstrated conditional step
279
+
280
+ Use observations the person actually received; the host owns what those observations mean. Matching is on categorical cue strings, not hidden world state or a per-object correct-action table.
281
+
282
+ ```ts
283
+ receiveMethodCues(learner, {
284
+ contextId: 'observed-example',
285
+ cues: [{ cue: 'route', value: 'north' }, { cue: 'signal', value: 'clear' }],
286
+ });
287
+ demonstrateMethod(learner, {
288
+ id: 'routing-north', demonstrationId: 'example-1',
289
+ contextId: 'observed-example', sourceId: 'instructor',
290
+ conditions: [{ cue: 'route', value: 'north' }, { cue: 'signal', value: 'clear' }],
291
+ action: 'send-north', outcome: 'success',
292
+ });
293
+ // A new parcel can share the learned observation structure.
294
+ receiveMethodCues(learner, {
295
+ contextId: 'new-parcel',
296
+ cues: [{ cue: 'route', value: 'north' }, { cue: 'signal', value: 'clear' }],
297
+ });
298
+ const evidence = methodEvidence(learner, 'send-north', 'new-parcel');
299
+ // Offers for that parcel carry methodContext: 'new-parcel'.
300
+ // Ordinary decide() includes the acquired method term alongside other motives.
301
+ clearMethodContext(learner); // when leaving or switching the task
302
+ ```
303
+
304
+ 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; conflicting examples reduce confidence. A successful lecture does not certify reliable execution. [The headless control](examples/method-control.ts) demonstrates changed teaching, withheld teaching, a scalar-skill control and acquired-state ablation with two methods and an exception.
@@ -31,6 +31,7 @@ import { normTerms } from '../conscience/index.js';
31
31
  import { clamp01, cmpStr, dexp, minuteOfDay, round } from '../core/index.js';
32
32
  import { habitEase, habitPull } from '../habits/index.js';
33
33
  import { expectedEffect } from '../memory/index.js';
34
+ import { methodEvidence } from '../methods/index.js';
34
35
  import { urgencyOf } from '../needs/index.js';
35
36
  import { partneringTerms } from '../partnering/index.js';
36
37
  import { companionSteadiness, socialTerms } from '../social/index.js';
@@ -238,6 +239,10 @@ export function consider(p, aff, ctx) {
238
239
  if (expected.gist) {
239
240
  push('memory', K.expectationScale * expected.gist.valence * expected.gist.weight * (1 - expected.confidence));
240
241
  }
242
+ // Acquired cue-conditioned methods remain one pull among ordinary motives.
243
+ if (aff.methodContext !== undefined)
244
+ for (const t of methodEvidence(p, aff.action, aff.methodContext).terms)
245
+ push(t.source, t.value);
241
246
  // Norms, commitments and goals. Planning terms scale with developmental maturity.
242
247
  let refractoryCache;
243
248
  const refractory = () => {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Human Framework v1 public surface. */
2
- export declare const FRAMEWORK_VERSION = "2.1.4";
2
+ export declare const FRAMEWORK_VERSION = "2.2.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';
@@ -20,6 +20,8 @@ export * from './impression.ts';
20
20
  export * from './lifecourse/index.ts';
21
21
  export * from './longrun.ts';
22
22
  export * from './memory/index.ts';
23
+ export type { MethodDemonstration, MethodEvidence } from './methods/index.ts';
24
+ export { clearMethodContext, demonstrateMethod, METHOD_DEFAULTS, methodEvidence, receiveMethodCues, } from './methods/index.ts';
23
25
  export type { MigrationStep } from './migrate.ts';
24
26
  export { MIGRATIONS, migratableVersions, migrate } from './migrate.ts';
25
27
  export * from './narrate/index.ts';
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Human Framework v1 public surface. */
2
- export const FRAMEWORK_VERSION = '2.1.4';
2
+ export const FRAMEWORK_VERSION = '2.2.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';
@@ -19,6 +19,7 @@ export * from './impression.js';
19
19
  export * from './lifecourse/index.js';
20
20
  export * from './longrun.js';
21
21
  export * from './memory/index.js';
22
+ export { clearMethodContext, demonstrateMethod, METHOD_DEFAULTS, methodEvidence, receiveMethodCues, } from './methods/index.js';
22
23
  export { MIGRATIONS, migratableVersions, migrate } from './migrate.js';
23
24
  export * from './narrate/index.js';
24
25
  export * from './needs/index.js';
@@ -0,0 +1 @@
1
+ export * from './methods.ts';
@@ -0,0 +1 @@
1
+ export * from './methods.js';
@@ -0,0 +1,50 @@
1
+ import type { MethodCue, Person, Term } from '../types.ts';
2
+ export declare const METHOD_DEFAULTS: {
3
+ maxRules: number;
4
+ maxConditions: number;
5
+ maxCues: number;
6
+ maxEvidence: number;
7
+ maxRecentDemonstrations: number;
8
+ maxIdentifierLength: number;
9
+ maxValueLength: number;
10
+ contextMinutes: number;
11
+ evidenceHalfLife: number;
12
+ /** Two fully attended ordinary examples raise support to 0.5 before source trust. */
13
+ evidencePrior: number;
14
+ utilityScale: number;
15
+ };
16
+ export interface MethodDemonstration {
17
+ /** Name of the explained method; action/conditions distinguish its variants. */
18
+ id: string;
19
+ /** Stable event id: repeats within the retained 256-example horizon cannot strengthen it. */
20
+ demonstrationId: string;
21
+ contextId: string;
22
+ conditions: readonly MethodCue[];
23
+ action: string;
24
+ sourceId: string;
25
+ outcome: 'success' | 'failure';
26
+ /** Attended fraction of this example, 0..1; zero acquires nothing. */
27
+ attention?: number;
28
+ }
29
+ export interface MethodEvidence {
30
+ /** Net matching evidence for this action, -1..1; conflicts reduce it. */
31
+ support: number;
32
+ terms: Term[];
33
+ matched: {
34
+ id: string;
35
+ action: string;
36
+ sourceId: string;
37
+ support: number;
38
+ }[];
39
+ }
40
+ /** Replace current received observations. This is an explicit host reception event, not host truth. */
41
+ export declare function receiveMethodCues(p: Person, received: {
42
+ contextId: string;
43
+ cues: readonly MethodCue[];
44
+ }): boolean;
45
+ /** Drop current observations while retaining learned methods. Call when leaving/switching a task. */
46
+ export declare function clearMethodContext(p: Person): void;
47
+ /** Acquire one explained step only when its learner actually received the matching observations. */
48
+ export declare function demonstrateMethod(p: Person, example: MethodDemonstration): boolean;
49
+ /** Read acquired evidence only. No draw, state mutation, hidden fault access or fixed action policy. */
50
+ export declare function methodEvidence(p: Person, action: string, contextId: string): MethodEvidence;
@@ -0,0 +1,235 @@
1
+ /**
2
+ * SCOPE: bounded acquisition of conditional action methods from attended examples.
3
+ * A learner must have received every categorical cue in an explained conjunction.
4
+ * Successful and unsuccessful demonstrations become person-owned evidence; the
5
+ * same reader supplies host queries and ordinary cognition terms. Unknown cues
6
+ * do not mean false. Context identity prevents one task's observations being used
7
+ * for another task. Contradictory examples weaken support without choosing a
8
+ * privileged correct answer. Attention, source trust and recency grade evidence.
9
+ * These are explicit engineering choices, not fitted human learning parameters.
10
+ * This module does not infer cues, invent actions, plan sequences, certify skill,
11
+ * access host truth, compute religious worth, or guarantee that a person obeys.
12
+ */
13
+ import { trustOf } from '../beliefs/index.js';
14
+ import { clamp01, cmpStr, decay, isNum, isObj } from '../core/index.js';
15
+ import { MAX_MINUTE, MINUTES_PER_DAY } from '../types.js';
16
+ export const METHOD_DEFAULTS = {
17
+ maxRules: 24,
18
+ maxConditions: 8,
19
+ maxCues: 24,
20
+ maxEvidence: 16,
21
+ maxRecentDemonstrations: 256,
22
+ maxIdentifierLength: 96,
23
+ maxValueLength: 128,
24
+ contextMinutes: 120,
25
+ evidenceHalfLife: 180 * MINUTES_PER_DAY,
26
+ /** Two fully attended ordinary examples raise support to 0.5 before source trust. */
27
+ evidencePrior: 2,
28
+ utilityScale: 1.2,
29
+ };
30
+ const validId = (x) => typeof x === 'string' &&
31
+ x.length > 0 &&
32
+ x.length <= METHOD_DEFAULTS.maxIdentifierLength &&
33
+ x !== '__proto__' &&
34
+ x !== 'constructor' &&
35
+ x !== 'prototype';
36
+ const validValue = (x) => typeof x === 'string' && x.length > 0 && x.length <= METHOD_DEFAULTS.maxValueLength;
37
+ const validMinute = (x) => isNum(x) && Math.abs(x) <= MAX_MINUTE;
38
+ function cleanCues(input, maximum) {
39
+ if (!Array.isArray(input) || input.length === 0 || input.length > maximum)
40
+ return undefined;
41
+ const out = [];
42
+ for (const c of input) {
43
+ if (!isObj(c) || !validId(c.cue) || !validValue(c.value))
44
+ return undefined;
45
+ // Ambiguous duplicate observations do not silently choose the last value.
46
+ if (out.some((v) => v.cue === c.cue))
47
+ return undefined;
48
+ out.push({ cue: c.cue, value: c.value });
49
+ }
50
+ return out.sort((a, b) => cmpStr(a.cue, b.cue));
51
+ }
52
+ function available(p) {
53
+ return p.body.alive && !p.body.asleep && !p.body.downed;
54
+ }
55
+ /** Replace current received observations. This is an explicit host reception event, not host truth. */
56
+ export function receiveMethodCues(p, received) {
57
+ const cues = cleanCues(received.cues, METHOD_DEFAULTS.maxCues);
58
+ // Bad/new unreadable input must not leave stale cues available.
59
+ clearMethodContext(p);
60
+ if (!available(p) || !validId(received.contextId) || !cues)
61
+ return false;
62
+ p.methods ??= { rules: [], recentDemonstrations: [] };
63
+ p.methods.context = { id: received.contextId, at: p.now, cues };
64
+ return true;
65
+ }
66
+ /** Drop current observations while retaining learned methods. Call when leaving/switching a task. */
67
+ export function clearMethodContext(p) {
68
+ if (p.methods)
69
+ delete p.methods.context;
70
+ }
71
+ function matches(conditions, cues) {
72
+ return conditions.every((c) => cues.some((v) => v.cue === c.cue && v.value === c.value));
73
+ }
74
+ function currentContext(p, id) {
75
+ const context = p.methods?.context;
76
+ if (!available(p) ||
77
+ !context ||
78
+ context.id !== id ||
79
+ p.now < context.at ||
80
+ p.now - context.at > METHOD_DEFAULTS.contextMinutes)
81
+ return undefined;
82
+ return context;
83
+ }
84
+ /** Acquire one explained step only when its learner actually received the matching observations. */
85
+ export function demonstrateMethod(p, example) {
86
+ const conditions = cleanCues(example.conditions, METHOD_DEFAULTS.maxConditions);
87
+ const context = currentContext(p, example.contextId);
88
+ const attention = example.attention === undefined ? 1 : example.attention;
89
+ if (!context ||
90
+ !conditions ||
91
+ !validId(example.id) ||
92
+ !validId(example.action) ||
93
+ !validId(example.sourceId) ||
94
+ !validId(example.demonstrationId) ||
95
+ (example.outcome !== 'success' && example.outcome !== 'failure') ||
96
+ !isNum(attention) ||
97
+ attention <= 0 ||
98
+ attention > 1 ||
99
+ !matches(conditions, context.cues))
100
+ return false;
101
+ const state = p.methods;
102
+ if (!state)
103
+ return false;
104
+ if (state.recentDemonstrations.includes(example.demonstrationId) ||
105
+ state.rules.some((r) => r.evidence.some((e) => e.id === example.demonstrationId)))
106
+ return false;
107
+ state.recentDemonstrations.push(example.demonstrationId);
108
+ if (state.recentDemonstrations.length > METHOD_DEFAULTS.maxRecentDemonstrations)
109
+ state.recentDemonstrations.shift();
110
+ let rule = state.rules.find((r) => r.action === example.action && JSON.stringify(r.conditions) === JSON.stringify(conditions));
111
+ if (!rule) {
112
+ rule = { id: example.id, action: example.action, conditions, evidence: [] };
113
+ state.rules.push(rule);
114
+ if (state.rules.length > METHOD_DEFAULTS.maxRules)
115
+ state.rules.shift();
116
+ }
117
+ rule.evidence.push({
118
+ id: example.demonstrationId,
119
+ sourceId: example.sourceId,
120
+ at: p.now,
121
+ attention: clamp01(attention),
122
+ outcome: example.outcome,
123
+ });
124
+ if (rule.evidence.length > METHOD_DEFAULTS.maxEvidence)
125
+ rule.evidence.shift();
126
+ return true;
127
+ }
128
+ /** Read acquired evidence only. No draw, state mutation, hidden fault access or fixed action policy. */
129
+ export function methodEvidence(p, action, contextId) {
130
+ const result = { support: 0, terms: [], matched: [] };
131
+ const context = currentContext(p, contextId);
132
+ if (!context)
133
+ return result;
134
+ const rules = p.methods?.rules ?? [];
135
+ for (const rule of rules) {
136
+ if (!matches(rule.conditions, context.cues))
137
+ continue;
138
+ const sources = [...new Set(rule.evidence.map((e) => e.sourceId))].sort(cmpStr);
139
+ for (const sourceId of sources) {
140
+ let positive = 0;
141
+ let negative = 0;
142
+ for (const e of rule.evidence) {
143
+ if (e.sourceId !== sourceId || e.at > p.now)
144
+ continue;
145
+ const weight = decay(e.attention, p.now - e.at, METHOD_DEFAULTS.evidenceHalfLife);
146
+ if (e.outcome === 'success')
147
+ positive += weight;
148
+ else
149
+ negative += weight;
150
+ }
151
+ const voice = p.will.voices.find((v) => v.voiceId === sourceId);
152
+ const receivedTrust = voice?.trust ?? trustOf(p, sourceId);
153
+ const trust = isNum(receivedTrust) ? clamp01(receivedTrust) : 0.5;
154
+ const support = (trust * (positive - negative)) / (positive + negative + METHOD_DEFAULTS.evidencePrior);
155
+ result.matched.push({ id: rule.id, action: rule.action, sourceId, support });
156
+ }
157
+ }
158
+ // Comparable alternative actions reduce confidence; they never become a hidden answer override.
159
+ const strengths = new Map();
160
+ for (const match of result.matched)
161
+ strengths.set(match.action, (strengths.get(match.action) ?? 0) + match.support);
162
+ const own = strengths.get(action) ?? 0;
163
+ const rival = Math.max(0, ...[...strengths].filter(([a]) => a !== action).map(([, v]) => v));
164
+ const total = Math.max(1, ...[...strengths.values()].map((v) => Math.abs(v)));
165
+ const net = own > 0 ? Math.max(0, own - rival) : own;
166
+ result.support = Math.max(-1, Math.min(1, net / total));
167
+ const scale = own === 0 ? 0 : result.support / own;
168
+ for (const match of result.matched) {
169
+ if (match.action !== action || match.support === 0)
170
+ continue;
171
+ result.terms.push({
172
+ source: `method:${match.id}:${match.sourceId}`,
173
+ value: METHOD_DEFAULTS.utilityScale * match.support * scale,
174
+ });
175
+ }
176
+ return result;
177
+ }
178
+ /** @internal Strict bounded restore; no unrecognized fields survive. */
179
+ export function sanitizeMethods(input) {
180
+ if (!isObj(input) || !Array.isArray(input.rules))
181
+ return undefined;
182
+ const rules = [];
183
+ const seen = new Set();
184
+ for (const row of input.rules.slice(-METHOD_DEFAULTS.maxRules)) {
185
+ if (!isObj(row) || !validId(row.id) || !validId(row.action) || !Array.isArray(row.evidence))
186
+ continue;
187
+ const conditions = cleanCues(row.conditions, METHOD_DEFAULTS.maxConditions);
188
+ if (!conditions)
189
+ continue;
190
+ const evidence = [];
191
+ for (const e of row.evidence.slice(-METHOD_DEFAULTS.maxEvidence)) {
192
+ if (!isObj(e) ||
193
+ !validId(e.id) ||
194
+ !validId(e.sourceId) ||
195
+ !validMinute(e.at) ||
196
+ !isNum(e.attention) ||
197
+ e.attention <= 0 ||
198
+ e.attention > 1 ||
199
+ (e.outcome !== 'success' && e.outcome !== 'failure') ||
200
+ seen.has(e.id))
201
+ continue;
202
+ seen.add(e.id);
203
+ evidence.push({ id: e.id, sourceId: e.sourceId, at: e.at, attention: e.attention, outcome: e.outcome });
204
+ }
205
+ if (evidence.length) {
206
+ const same = rules.find((r) => r.action === row.action && JSON.stringify(r.conditions) === JSON.stringify(conditions));
207
+ if (same)
208
+ same.evidence = [...same.evidence, ...evidence].slice(-METHOD_DEFAULTS.maxEvidence);
209
+ else
210
+ rules.push({ id: row.id, action: row.action, conditions, evidence });
211
+ }
212
+ }
213
+ const recentDemonstrations = [];
214
+ if (Array.isArray(input.recentDemonstrations))
215
+ for (const id of input.recentDemonstrations.slice(-METHOD_DEFAULTS.maxRecentDemonstrations))
216
+ if (validId(id) && !recentDemonstrations.includes(id))
217
+ recentDemonstrations.push(id);
218
+ // Only a missing history is reconstructed. Appending older evidence to a full
219
+ // valid horizon would evict genuinely newer event ids and alter a live save.
220
+ if (input.recentDemonstrations === undefined)
221
+ for (const rule of rules)
222
+ for (const e of rule.evidence)
223
+ if (!recentDemonstrations.includes(e.id))
224
+ recentDemonstrations.push(e.id);
225
+ const out = {
226
+ rules,
227
+ recentDemonstrations: recentDemonstrations.slice(-METHOD_DEFAULTS.maxRecentDemonstrations),
228
+ };
229
+ if (isObj(input.context) && validId(input.context.id) && validMinute(input.context.at)) {
230
+ const cues = cleanCues(input.context.cues, METHOD_DEFAULTS.maxCues);
231
+ if (cues)
232
+ out.context = { id: input.context.id, at: input.context.at, cues };
233
+ }
234
+ return out;
235
+ }
package/dist/migrate.js CHANGED
@@ -7,7 +7,8 @@
7
7
  * optional fields that are absent by default), so those steps only stamp the version; 1.9.0 to 2.0.0 renames one
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
- * the slices as for any save. A migrated save continues under the current engine's rules: it restores and runs,
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. A migrated save continues under the current engine's rules: it restores and runs,
11
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
12
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.
13
14
  *
@@ -57,6 +58,11 @@ export const MIGRATIONS = {
57
58
  note: 'the surroundings field `ambient.now` is renamed `ambient.percept`; per-person retention is optional and starts absent; behaviour unchanged',
58
59
  apply: renameAmbient,
59
60
  },
61
+ '2.0.0': {
62
+ to: '2.1.0',
63
+ note: 'conditional method state and received cues are optional and start absent; existing host decisions unchanged',
64
+ apply: stamp,
65
+ },
60
66
  };
61
67
  /** The step upgrading from `v`, looked up as an own property only (a version of "__proto__" matches nothing). */
62
68
  const stepFrom = (v) => Object.hasOwn(MIGRATIONS, v) ? MIGRATIONS[v] : undefined;
package/dist/restore.js CHANGED
@@ -16,6 +16,7 @@ import { sanitizeAmbient } from './environment/index.js';
16
16
  import { sanitizeFamily } from './family/index.js';
17
17
  import { HABIT_DEFAULTS } from './habits/index.js';
18
18
  import { GIST_DEFAULTS, MEMORY_DEFAULTS, sanitizeGists } from './memory/index.js';
19
+ import { sanitizeMethods } from './methods/index.js';
19
20
  import { migrate } from './migrate.js';
20
21
  import { sanitizeBonds } from './partnering/index.js';
21
22
  import { cleanRetention, createPerson, traceLimit } from './person.js';
@@ -57,6 +58,7 @@ const OPTIONAL_KEYS = new Set([
57
58
  'bonds',
58
59
  'ambient',
59
60
  'retention',
61
+ 'methods',
60
62
  ]);
61
63
  /** Kind of a JSON value for the per-field type check in `restore`. */
62
64
  const kindOf = (x) => (Array.isArray(x) ? 'array' : x === null ? 'null' : typeof x);
@@ -168,6 +170,7 @@ export function restore(input) {
168
170
  keepValid(out, 'bonds', sanitizeBonds);
169
171
  keepValid(out, 'ambient', sanitizeAmbient);
170
172
  keepValid(out, 'retention', cleanRetention);
173
+ keepValid(out, 'methods', sanitizeMethods);
171
174
  boundLists(out);
172
175
  return out;
173
176
  }
package/dist/types.d.ts CHANGED
@@ -787,9 +787,9 @@ export interface Activity {
787
787
  }
788
788
  export declare const PERSON_SCHEMA = "human/person@1";
789
789
  /**
790
- * Current: 2.0.0 (2026-10-04): `AmbientState.now` renamed `percept` (migrated); per-person `retention` (optional,
791
- * absent until set); `restore` validates every slice by range and bound and refuses minutes it cannot step. No valid
792
- * run changes. The current version comes first, then earlier engines oldest first; the API doc shows this first line.
790
+ * Current: 2.1.0 (2026-10-10): optional acquired conditional methods and explicit received cue contexts.
791
+ * Normal cognition reads learned method evidence only on offers carrying the matching context.
792
+ * Existing hosts with no method state keep their previous decisions; 2.0.0 saves migrate by stamping the version.
793
793
  *
794
794
  * Earlier engines (oldest first):
795
795
  * 1.1.0 (2026-10-03): joint activities, omission/distrust rules, reactance, voice history.
@@ -818,7 +818,7 @@ export declare const PERSON_SCHEMA = "human/person@1";
818
818
  * and an activity that would cover a closing stretch is reviewed when the stretch begins (`isUnderWay`,
819
819
  * `dutyReviewAt`). Person shape unchanged.
820
820
  */
821
- export declare const ENGINE_VERSION = "2.0.0";
821
+ export declare const ENGINE_VERSION = "2.1.0";
822
822
  /** A pregnancy carried by this person (host-driven; see `family/`). */
823
823
  export interface Pregnancy {
824
824
  fatherId: PersonId;
@@ -891,6 +891,34 @@ export interface AmbientState {
891
891
  /** Minute these surroundings began. */
892
892
  since: Minute;
893
893
  }
894
+ /** A received categorical observation; missing values never stand for a negative observation. */
895
+ export interface MethodCue {
896
+ cue: string;
897
+ value: string;
898
+ }
899
+ export interface MethodRule {
900
+ id: string;
901
+ action: string;
902
+ conditions: MethodCue[];
903
+ evidence: {
904
+ id: string;
905
+ sourceId: string;
906
+ at: Minute;
907
+ attention: Unit;
908
+ outcome: 'success' | 'failure';
909
+ }[];
910
+ }
911
+ /** Optional conditional action learning. Owned by methods/, not a host answer table. */
912
+ export interface MethodsState {
913
+ rules: MethodRule[];
914
+ /** Last 256 acquired example ids, independent of rule/evidence eviction. */
915
+ recentDemonstrations: string[];
916
+ context?: {
917
+ id: string;
918
+ at: Minute;
919
+ cues: MethodCue[];
920
+ };
921
+ }
894
922
  export interface Person {
895
923
  schema: typeof PERSON_SCHEMA;
896
924
  /** Engine version that last wrote this state. */
@@ -908,6 +936,8 @@ export interface Person {
908
936
  conscience: ConscienceState;
909
937
  affect: AffectState;
910
938
  skills: Record<string, Skill>;
939
+ /** Acquired conditional method evidence and current received cues; absent until used. */
940
+ methods?: MethodsState;
911
941
  /** Practice consolidation (1.8.0, opt-in via `enableSkillRetention`). Owned by `skills/`. */
912
942
  skillRetention?: {
913
943
  consolidationHours: number;
@@ -968,6 +998,8 @@ export interface Affordance {
968
998
  /** Verb/action class shared across offers: 'eat', 'sleep', 'farm', 'talk', 'pray', 'steal', ... */
969
999
  action: string;
970
1000
  label: string;
1001
+ /** Received method context this offer belongs to; absent = no method term. */
1002
+ methodContext?: string;
971
1003
  targetId?: EntityId;
972
1004
  placeId?: EntityId;
973
1005
  /** Other people taking part. */
package/dist/types.js CHANGED
@@ -84,9 +84,9 @@ export const EMOTIONS = [
84
84
  // ---------------------------------------------------------------------------------------------
85
85
  export const PERSON_SCHEMA = 'human/person@1';
86
86
  /**
87
- * Current: 2.0.0 (2026-10-04): `AmbientState.now` renamed `percept` (migrated); per-person `retention` (optional,
88
- * absent until set); `restore` validates every slice by range and bound and refuses minutes it cannot step. No valid
89
- * run changes. The current version comes first, then earlier engines oldest first; the API doc shows this first line.
87
+ * Current: 2.1.0 (2026-10-10): optional acquired conditional methods and explicit received cue contexts.
88
+ * Normal cognition reads learned method evidence only on offers carrying the matching context.
89
+ * Existing hosts with no method state keep their previous decisions; 2.0.0 saves migrate by stamping the version.
90
90
  *
91
91
  * Earlier engines (oldest first):
92
92
  * 1.1.0 (2026-10-03): joint activities, omission/distrust rules, reactance, voice history.
@@ -115,4 +115,4 @@ export const PERSON_SCHEMA = 'human/person@1';
115
115
  * and an activity that would cover a closing stretch is reviewed when the stretch begins (`isUnderWay`,
116
116
  * `dutyReviewAt`). Person shape unchanged.
117
117
  */
118
- export const ENGINE_VERSION = '2.0.0';
118
+ export const ENGINE_VERSION = '2.1.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adam0white/human-framework",
3
- "version": "2.1.4",
3
+ "version": "2.2.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",