oracular 0.11.0 → 0.12.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/dist/index.d.cts CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * Version of the serialized state shape, stored in every state as `state.version`. Bumped only
3
- * when `EngineState` changes incompatibly, and every bump ships its migration: {@link deserialize}
3
+ * when `EngineState` changes shape, and every bump ships its migration: {@link deserialize}
4
4
  * upgrades states saved by earlier versions of this library on its own (version 1, written by
5
- * oracular 0.1.0 to 0.2.x, and version 2, written by 0.3.0 to 0.5.x, become version 3), reads a
6
- * state one version newer than its own as its own (a version bump only adds fields, so a rollback
7
- * by one release keeps the user's history), and calls your `migrate` function only for a version
8
- * it does not know.
5
+ * oracular 0.1.0 to 0.2.x, version 2, written by 0.3.0 to 0.5.x, and version 3, written by 0.6.0
6
+ * to 0.11.x, become version 4), reads a state one version newer than its own as its own (a
7
+ * version bump only adds fields, so a rollback by one release keeps the user's history), and
8
+ * calls your `migrate` function only for a version it does not know.
9
9
  *
10
10
  * @example
11
11
  * ```ts
@@ -21,7 +21,7 @@
21
21
  * });
22
22
  * ```
23
23
  */
24
- declare const STATE_VERSION: 3;
24
+ declare const STATE_VERSION: 4;
25
25
  /**
26
26
  * Version of the recorder format: the `v` field of every record that {@link createRecord} and
27
27
  * {@link createEndRecord} produce. A new optional field does not change it; readers ignore fields
@@ -230,6 +230,20 @@ interface EpisodeState {
230
230
  lastEventAt: number | null;
231
231
  intents: Record<string, EpisodeIntentRecord>;
232
232
  }
233
+ /**
234
+ * The engine's clock: the latest event time it trusts, and one event stamped behind it that is
235
+ * not yet confirmed as a clock correction. See `observe` for the rule.
236
+ */
237
+ interface ClockState {
238
+ /** The latest event time trusted so far; null before the first event. */
239
+ at: number | null;
240
+ /**
241
+ * The stamp of the last event that arrived behind `at`, held until the next event decides: a
242
+ * second event behind `at` confirms the clock was corrected and every anchor ahead of this
243
+ * time moves back to it; an event at or after `at` makes it a glitch that changed nothing.
244
+ */
245
+ behindAt: number | null;
246
+ }
233
247
  /**
234
248
  * The whole engine state: plain JSON with no functions, class instances or hidden fields.
235
249
  * Persist it with `serialize`, restore it with `deserialize`.
@@ -247,6 +261,7 @@ interface EngineState {
247
261
  config: EngineConfig;
248
262
  intents: Record<string, IntentState>;
249
263
  episode: EpisodeState;
264
+ clock: ClockState;
250
265
  }
251
266
  /**
252
267
  * What the app feeds to `observe`. Any event name is accepted; only names that appear in some
@@ -359,7 +374,7 @@ interface Evaluation {
359
374
  /**
360
375
  * Turns a stored state of a version this library cannot migrate itself into a current
361
376
  * `EngineState`. Receives the parsed JSON as-is and the version it declares. States saved by
362
- * oracular 0.1.0 to 0.5.x (versions 1 and 2) never reach it, nor does a state one version newer
377
+ * oracular 0.1.0 to 0.11.x (versions 1 to 3) never reach it, nor does a state one version newer
363
378
  * than `STATE_VERSION`: `deserialize` handles both on its own.
364
379
  *
365
380
  * @example
@@ -384,7 +399,7 @@ interface DeserializeOptions {
384
399
  config?: EngineConfig;
385
400
  /**
386
401
  * Called when the stored version is neither `STATE_VERSION`, nor one this library migrates
387
- * itself (states from oracular 0.1.0 to 0.5.x are upgraded without it), nor the next version,
402
+ * itself (states from oracular 0.1.0 to 0.11.x are upgraded without it), nor the next version,
388
403
  * which it reads as its own.
389
404
  */
390
405
  migrate?: Migrate;
@@ -637,6 +652,15 @@ declare function explain(state: EngineState, intent: string, at: number, opts?:
637
652
  * or extends the current episode; an idle gap longer than `config.episodeTimeout` closes the
638
653
  * previous episode first.
639
654
  *
655
+ * The clock. An event at or after the latest event time moves the engine's clock there, and
656
+ * anything stamped ahead of it (a reveal stamped by a clock that was ahead) is pinned to it. An
657
+ * event stamped *behind* the clock counts now, at the clock's time, and its stamp is held: if the
658
+ * next event is also behind the clock, the clock was corrected, and every anchor ahead of the
659
+ * held stamp moves back to it so decay and cooldowns resume from the corrected time; if the next
660
+ * event is at or after the clock, the held stamp was one glitch and nothing moved. So one event
661
+ * stamped years ahead cannot hold an intent past the two events that follow it, and one stamped
662
+ * years behind cannot wipe the evidence or close the session.
663
+ *
640
664
  * @param state - The current state. Not mutated.
641
665
  * @param event - `name` is matched against every intent's signals and outcome; `at` is the
642
666
  * event's timestamp, in the same unit as your durations; `props` are accepted and ignored,
@@ -831,7 +855,7 @@ declare function serialize(state: EngineState): string;
831
855
  * before those checks existed still loads),
832
856
  * intents and signals no longer declared are dropped (α and β are recomputed from the kept
833
857
  * contributions), and newly declared intents start at their prior. States saved by oracular
834
- * 0.1.0 to 0.5.x are upgraded to the current shape on their own, and a state one version newer
858
+ * 0.1.0 to 0.11.x are upgraded to the current shape on their own, and a state one version newer
835
859
  * than {@link STATE_VERSION} (written by the next release, read after a rollback) is read as the
836
860
  * current one, ignoring the fields it adds; `migrate` is only for a version this library does
837
861
  * not know.
@@ -952,4 +976,4 @@ declare const ms: Units;
952
976
  */
953
977
  declare function validateConfig(config: unknown): EngineConfig;
954
978
 
955
- export { type DeserializeOptions, type EndRecord, type EngineConfig, type EngineState, type EpisodeIntentRecord, type EpisodeState, type EvaluateOptions, type Evaluation, type EventRecord, type ExplainOptions, type IntentConfig, type IntentEvent, type IntentSnapshot, type IntentState, type LearningConfig, type LiftType, type Migrate, type PendingEpisode, type Prediction, type PresetName, type PresetSource, RECORD_VERSION, type RecordMode, type RecordOptions, type RecordedDecision, type RecorderRecord, type RevealOptions, STATE_VERSION, type SignalContribution, type SignalExplanation, type SignalRecord, type Snapshot, type Units, createEndRecord, createRecord, createState, deserialize, endEpisode, episodeClosed, evaluate, evidenceToReveal, explain, inspect, markRevealed, ms, observe, predict, serialize, shouldReveal, validateConfig };
979
+ export { type ClockState, type DeserializeOptions, type EndRecord, type EngineConfig, type EngineState, type EpisodeIntentRecord, type EpisodeState, type EvaluateOptions, type Evaluation, type EventRecord, type ExplainOptions, type IntentConfig, type IntentEvent, type IntentSnapshot, type IntentState, type LearningConfig, type LiftType, type Migrate, type PendingEpisode, type Prediction, type PresetName, type PresetSource, RECORD_VERSION, type RecordMode, type RecordOptions, type RecordedDecision, type RecorderRecord, type RevealOptions, STATE_VERSION, type SignalContribution, type SignalExplanation, type SignalRecord, type Snapshot, type Units, createEndRecord, createRecord, createState, deserialize, endEpisode, episodeClosed, evaluate, evidenceToReveal, explain, inspect, markRevealed, ms, observe, predict, serialize, shouldReveal, validateConfig };
package/dist/index.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * Version of the serialized state shape, stored in every state as `state.version`. Bumped only
3
- * when `EngineState` changes incompatibly, and every bump ships its migration: {@link deserialize}
3
+ * when `EngineState` changes shape, and every bump ships its migration: {@link deserialize}
4
4
  * upgrades states saved by earlier versions of this library on its own (version 1, written by
5
- * oracular 0.1.0 to 0.2.x, and version 2, written by 0.3.0 to 0.5.x, become version 3), reads a
6
- * state one version newer than its own as its own (a version bump only adds fields, so a rollback
7
- * by one release keeps the user's history), and calls your `migrate` function only for a version
8
- * it does not know.
5
+ * oracular 0.1.0 to 0.2.x, version 2, written by 0.3.0 to 0.5.x, and version 3, written by 0.6.0
6
+ * to 0.11.x, become version 4), reads a state one version newer than its own as its own (a
7
+ * version bump only adds fields, so a rollback by one release keeps the user's history), and
8
+ * calls your `migrate` function only for a version it does not know.
9
9
  *
10
10
  * @example
11
11
  * ```ts
@@ -21,7 +21,7 @@
21
21
  * });
22
22
  * ```
23
23
  */
24
- declare const STATE_VERSION: 3;
24
+ declare const STATE_VERSION: 4;
25
25
  /**
26
26
  * Version of the recorder format: the `v` field of every record that {@link createRecord} and
27
27
  * {@link createEndRecord} produce. A new optional field does not change it; readers ignore fields
@@ -230,6 +230,20 @@ interface EpisodeState {
230
230
  lastEventAt: number | null;
231
231
  intents: Record<string, EpisodeIntentRecord>;
232
232
  }
233
+ /**
234
+ * The engine's clock: the latest event time it trusts, and one event stamped behind it that is
235
+ * not yet confirmed as a clock correction. See `observe` for the rule.
236
+ */
237
+ interface ClockState {
238
+ /** The latest event time trusted so far; null before the first event. */
239
+ at: number | null;
240
+ /**
241
+ * The stamp of the last event that arrived behind `at`, held until the next event decides: a
242
+ * second event behind `at` confirms the clock was corrected and every anchor ahead of this
243
+ * time moves back to it; an event at or after `at` makes it a glitch that changed nothing.
244
+ */
245
+ behindAt: number | null;
246
+ }
233
247
  /**
234
248
  * The whole engine state: plain JSON with no functions, class instances or hidden fields.
235
249
  * Persist it with `serialize`, restore it with `deserialize`.
@@ -247,6 +261,7 @@ interface EngineState {
247
261
  config: EngineConfig;
248
262
  intents: Record<string, IntentState>;
249
263
  episode: EpisodeState;
264
+ clock: ClockState;
250
265
  }
251
266
  /**
252
267
  * What the app feeds to `observe`. Any event name is accepted; only names that appear in some
@@ -359,7 +374,7 @@ interface Evaluation {
359
374
  /**
360
375
  * Turns a stored state of a version this library cannot migrate itself into a current
361
376
  * `EngineState`. Receives the parsed JSON as-is and the version it declares. States saved by
362
- * oracular 0.1.0 to 0.5.x (versions 1 and 2) never reach it, nor does a state one version newer
377
+ * oracular 0.1.0 to 0.11.x (versions 1 to 3) never reach it, nor does a state one version newer
363
378
  * than `STATE_VERSION`: `deserialize` handles both on its own.
364
379
  *
365
380
  * @example
@@ -384,7 +399,7 @@ interface DeserializeOptions {
384
399
  config?: EngineConfig;
385
400
  /**
386
401
  * Called when the stored version is neither `STATE_VERSION`, nor one this library migrates
387
- * itself (states from oracular 0.1.0 to 0.5.x are upgraded without it), nor the next version,
402
+ * itself (states from oracular 0.1.0 to 0.11.x are upgraded without it), nor the next version,
388
403
  * which it reads as its own.
389
404
  */
390
405
  migrate?: Migrate;
@@ -637,6 +652,15 @@ declare function explain(state: EngineState, intent: string, at: number, opts?:
637
652
  * or extends the current episode; an idle gap longer than `config.episodeTimeout` closes the
638
653
  * previous episode first.
639
654
  *
655
+ * The clock. An event at or after the latest event time moves the engine's clock there, and
656
+ * anything stamped ahead of it (a reveal stamped by a clock that was ahead) is pinned to it. An
657
+ * event stamped *behind* the clock counts now, at the clock's time, and its stamp is held: if the
658
+ * next event is also behind the clock, the clock was corrected, and every anchor ahead of the
659
+ * held stamp moves back to it so decay and cooldowns resume from the corrected time; if the next
660
+ * event is at or after the clock, the held stamp was one glitch and nothing moved. So one event
661
+ * stamped years ahead cannot hold an intent past the two events that follow it, and one stamped
662
+ * years behind cannot wipe the evidence or close the session.
663
+ *
640
664
  * @param state - The current state. Not mutated.
641
665
  * @param event - `name` is matched against every intent's signals and outcome; `at` is the
642
666
  * event's timestamp, in the same unit as your durations; `props` are accepted and ignored,
@@ -831,7 +855,7 @@ declare function serialize(state: EngineState): string;
831
855
  * before those checks existed still loads),
832
856
  * intents and signals no longer declared are dropped (α and β are recomputed from the kept
833
857
  * contributions), and newly declared intents start at their prior. States saved by oracular
834
- * 0.1.0 to 0.5.x are upgraded to the current shape on their own, and a state one version newer
858
+ * 0.1.0 to 0.11.x are upgraded to the current shape on their own, and a state one version newer
835
859
  * than {@link STATE_VERSION} (written by the next release, read after a rollback) is read as the
836
860
  * current one, ignoring the fields it adds; `migrate` is only for a version this library does
837
861
  * not know.
@@ -952,4 +976,4 @@ declare const ms: Units;
952
976
  */
953
977
  declare function validateConfig(config: unknown): EngineConfig;
954
978
 
955
- export { type DeserializeOptions, type EndRecord, type EngineConfig, type EngineState, type EpisodeIntentRecord, type EpisodeState, type EvaluateOptions, type Evaluation, type EventRecord, type ExplainOptions, type IntentConfig, type IntentEvent, type IntentSnapshot, type IntentState, type LearningConfig, type LiftType, type Migrate, type PendingEpisode, type Prediction, type PresetName, type PresetSource, RECORD_VERSION, type RecordMode, type RecordOptions, type RecordedDecision, type RecorderRecord, type RevealOptions, STATE_VERSION, type SignalContribution, type SignalExplanation, type SignalRecord, type Snapshot, type Units, createEndRecord, createRecord, createState, deserialize, endEpisode, episodeClosed, evaluate, evidenceToReveal, explain, inspect, markRevealed, ms, observe, predict, serialize, shouldReveal, validateConfig };
979
+ export { type ClockState, type DeserializeOptions, type EndRecord, type EngineConfig, type EngineState, type EpisodeIntentRecord, type EpisodeState, type EvaluateOptions, type Evaluation, type EventRecord, type ExplainOptions, type IntentConfig, type IntentEvent, type IntentSnapshot, type IntentState, type LearningConfig, type LiftType, type Migrate, type PendingEpisode, type Prediction, type PresetName, type PresetSource, RECORD_VERSION, type RecordMode, type RecordOptions, type RecordedDecision, type RecorderRecord, type RevealOptions, STATE_VERSION, type SignalContribution, type SignalExplanation, type SignalRecord, type Snapshot, type Units, createEndRecord, createRecord, createState, deserialize, endEpisode, episodeClosed, evaluate, evidenceToReveal, explain, inspect, markRevealed, ms, observe, predict, serialize, shouldReveal, validateConfig };
package/dist/index.js CHANGED
@@ -136,8 +136,9 @@ function mapRecord(record2, fn) {
136
136
  var NONE = { hits: 0, misses: 0 };
137
137
  function decayLearned(state, learning, at) {
138
138
  if (state.learnedAt === null) return { ...state, learnedAt: at };
139
+ if (!(at > state.learnedAt)) return state;
139
140
  const d = learning.halfLife === null ? 1 : decayFactor(at - state.learnedAt, learning.halfLife);
140
- if (d === 1) return at === state.learnedAt ? state : { ...state, learnedAt: at };
141
+ if (d === 1) return { ...state, learnedAt: at };
141
142
  return {
142
143
  ...state,
143
144
  learnedAt: at,
@@ -249,8 +250,9 @@ function withContributions(state, intent, contributions) {
249
250
  }
250
251
  function decayIntent(state, intent, at) {
251
252
  if (state.updatedAt === null) return { ...state, updatedAt: at };
253
+ if (!(at > state.updatedAt)) return state;
252
254
  const d = decayFactor(at - state.updatedAt, intent.halfLife);
253
- if (d === 1) return at === state.updatedAt ? state : { ...state, updatedAt: at };
255
+ if (d === 1) return { ...state, updatedAt: at };
254
256
  const contributions = mapRecord(state.contributions, (c) => ({
255
257
  alpha: c.alpha * d,
256
258
  beta: c.beta * d
@@ -568,7 +570,7 @@ function markResolved(episode, intent) {
568
570
  }
569
571
 
570
572
  // src/version.ts
571
- var STATE_VERSION = 3;
573
+ var STATE_VERSION = 4;
572
574
  var RECORD_VERSION = 1;
573
575
 
574
576
  // src/state.ts
@@ -578,10 +580,16 @@ function createState(config) {
578
580
  for (const [name, intent] of Object.entries(normalized.intents)) {
579
581
  intents[name] = initIntent(intent);
580
582
  }
581
- return { version: STATE_VERSION, config: normalized, intents, episode: emptyEpisode() };
583
+ return {
584
+ version: STATE_VERSION,
585
+ config: normalized,
586
+ intents,
587
+ episode: emptyEpisode(),
588
+ clock: { at: null, behindAt: null }
589
+ };
582
590
  }
583
591
  function requireState(state) {
584
- if (!isRecord(state) || state.version !== STATE_VERSION || !isRecord(state.config) || !isRecord(state.intents) || !isRecord(state.episode)) {
592
+ if (!isRecord(state) || state.version !== STATE_VERSION || !isRecord(state.config) || !isRecord(state.intents) || !isRecord(state.episode) || !isRecord(state.clock)) {
585
593
  throw new TypeError("oracular: state must be an EngineState from createState or deserialize");
586
594
  }
587
595
  return state;
@@ -718,8 +726,10 @@ function explain(state, intent, at, opts) {
718
726
 
719
727
  // src/observe.ts
720
728
  function observe(state, event) {
721
- let s = requireState(state);
722
- const { name, at } = validateEvent(event);
729
+ const { name, at: stamp } = validateEvent(event);
730
+ const synced = syncClock(requireState(state), stamp);
731
+ let s = synced.state;
732
+ const at = synced.at;
723
733
  const closeAt = timedOutAt(s.episode, s.config.episodeTimeout, at);
724
734
  if (closeAt !== null) s = closeEpisode(s, closeAt);
725
735
  const learning = resolveLearning(s.config);
@@ -732,7 +742,6 @@ function observe(state, event) {
732
742
  let is = getIntentState(s, intentName, intent);
733
743
  is = decayIntent(is, intent, at);
734
744
  is = settlePending(is, intent, learning, at);
735
- is = pinAnchors(is, at);
736
745
  if (isSignal) {
737
746
  const { effective } = signalWeights(is, intent, learning, name);
738
747
  const n = occurrences(episode, intentName, name) + 1;
@@ -753,15 +762,36 @@ function observe(state, event) {
753
762
  }
754
763
  return { ...s, intents, episode };
755
764
  }
756
- function pinAnchors(is, at) {
757
- const reveal = is.lastRevealedAt !== null && is.lastRevealedAt > at;
758
- const outcome = is.lastOutcomeAt !== null && is.lastOutcomeAt > at;
759
- if (!reveal && !outcome) return is;
760
- return {
761
- ...is,
762
- lastRevealedAt: reveal ? at : is.lastRevealedAt,
763
- lastOutcomeAt: outcome ? at : is.lastOutcomeAt
764
- };
765
+ function syncClock(state, stamp) {
766
+ const { at, behindAt } = state.clock;
767
+ if (at === null || stamp >= at) return { state: pinTo(state, stamp), at: stamp };
768
+ if (behindAt !== null) return syncClock(pinTo(state, behindAt), stamp);
769
+ return { state: { ...state, clock: { at, behindAt: stamp } }, at };
770
+ }
771
+ var INTENT_ANCHORS = ["updatedAt", "learnedAt", "lastRevealedAt", "lastOutcomeAt"];
772
+ var EPISODE_ANCHORS = ["startedAt", "lastEventAt"];
773
+ function pinTo(state, t) {
774
+ let intents = state.intents;
775
+ for (const [name, is] of Object.entries(state.intents)) {
776
+ let pinned = pinKeys(is, INTENT_ANCHORS, t);
777
+ if (is.pending.some((p) => p.endedAt > t)) {
778
+ pinned = {
779
+ ...pinned,
780
+ pending: is.pending.map((p) => p.endedAt > t ? { ...p, endedAt: t } : p)
781
+ };
782
+ }
783
+ if (pinned !== is) intents = { ...intents, [name]: pinned };
784
+ }
785
+ const episode = pinKeys(state.episode, EPISODE_ANCHORS, t);
786
+ return { ...state, intents, episode, clock: { at: t, behindAt: null } };
787
+ }
788
+ function pinKeys(obj, keys, t) {
789
+ let out = obj;
790
+ for (const key of keys) {
791
+ const value = obj[key];
792
+ if (typeof value === "number" && value > t) out = { ...out, [key]: t };
793
+ }
794
+ return out;
765
795
  }
766
796
  function endEpisode(state, at) {
767
797
  const s = requireState(state);
@@ -887,7 +917,8 @@ function createEndRecord(at, opts) {
887
917
  // src/migrate.ts
888
918
  var MIGRATIONS = {
889
919
  1: fromV1,
890
- 2: fromV2
920
+ 2: fromV2,
921
+ 3: fromV3
891
922
  };
892
923
  function canMigrate(version) {
893
924
  return hasOwn(MIGRATIONS, String(version));
@@ -928,6 +959,9 @@ function fromV2(stored) {
928
959
  }
929
960
  return { ...stored, version: 3, intents };
930
961
  }
962
+ function fromV3(stored) {
963
+ return { ...stored, version: 4, clock: { at: null, behindAt: null } };
964
+ }
931
965
 
932
966
  // src/serialize.ts
933
967
  function serialize(state) {
@@ -1073,6 +1107,13 @@ function episodeState(value, path) {
1073
1107
  intents: mapOwn(r.intents, `${path}.intents`, episodeIntent)
1074
1108
  };
1075
1109
  }
1110
+ function clockState(value, path) {
1111
+ const r = record(value, path);
1112
+ return {
1113
+ at: nullOrFinite(r.at, `${path}.at`),
1114
+ behindAt: nullOrFinite(r.behindAt, `${path}.behindAt`)
1115
+ };
1116
+ }
1076
1117
  function parseState(value) {
1077
1118
  const r = record(value, "state");
1078
1119
  if (r.version !== STATE_VERSION) {
@@ -1084,7 +1125,8 @@ function parseState(value) {
1084
1125
  // when it is only stored, and the config passed in replaces it anyway.
1085
1126
  config: normalizeConfig(r.config, false),
1086
1127
  intents: mapOwn(r.intents, "state.intents", intentState),
1087
- episode: episodeState(r.episode, "state.episode")
1128
+ episode: episodeState(r.episode, "state.episode"),
1129
+ clock: clockState(r.clock, "state.clock")
1088
1130
  };
1089
1131
  }
1090
1132
  function requireComputable(state) {
@@ -1158,9 +1200,24 @@ function reconcile(raw, config) {
1158
1200
  startedAt: raw.episode.startedAt,
1159
1201
  lastEventAt: raw.episode.startedAt === null ? null : raw.episode.lastEventAt,
1160
1202
  intents: episodeIntents
1161
- }
1203
+ },
1204
+ clock: trustedClock(raw.clock, intents, raw.episode)
1162
1205
  };
1163
1206
  }
1207
+ function trustedClock(clock, intents, episode) {
1208
+ let at = clock.at;
1209
+ const see = (value) => {
1210
+ if (value !== null && (at === null || value > at)) at = value;
1211
+ };
1212
+ see(episode.startedAt);
1213
+ see(episode.lastEventAt);
1214
+ for (const is of Object.values(intents)) {
1215
+ see(is.updatedAt);
1216
+ see(is.lastOutcomeAt);
1217
+ }
1218
+ const behindAt = clock.behindAt !== null && at !== null && clock.behindAt < at ? clock.behindAt : null;
1219
+ return at === clock.at && behindAt === clock.behindAt ? clock : { at, behindAt };
1220
+ }
1164
1221
 
1165
1222
  // src/units.ts
1166
1223
  var ms = {