oracular 0.10.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/CHANGELOG.md +22 -0
- package/README.md +6 -5
- package/dist/cli.js +7 -3
- package/dist/index.cjs +83 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -10
- package/dist/index.d.ts +34 -10
- package/dist/index.js +83 -25
- package/dist/index.js.map +1 -1
- package/dist/presets.cjs +2 -2
- package/dist/presets.cjs.map +1 -1
- package/dist/presets.d.cts +25 -3
- package/dist/presets.d.ts +25 -3
- package/dist/presets.js +2 -2
- package/dist/presets.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.12.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#62](https://github.com/Yuribenjamin/oracular/pull/62) [`0502b78`](https://github.com/Yuribenjamin/oracular/commit/0502b789049cb8ef8643af587ec94b78ce0c39b1) Thanks [@Yuribenjamin](https://github.com/Yuribenjamin)! - **The clock rule: one glitch cannot move the engine.** 0.10.0 made every write trust the latest event's time, so that one event stamped in the future could no longer hold an intent until the clock caught up. A production re-test found the cost: a clock that steps *backwards* (a phone that wakes up in 1970 before it syncs, a seconds-for-milliseconds bug) pulled every anchor back with it, and the next event decayed the whole gap again and closed the session, recording a miss nobody earned. The two cases are one ambiguity: after one backward step the engine cannot tell whether the new stamp or the old anchor is the wrong clock. It now waits one event. The engine keeps a clock, the latest event time it trusts; an event stamped behind it counts now, at the clock's time, and its stamp is held. If the next event is also behind the clock, the clock was corrected: every anchor ahead of the held stamp (evidence, learned counts, reveals, outcomes, the open episode, the episodes waiting under `attribution`) moves back to it, and decay and cooldowns resume from there. If the next event is at or after the clock, the held stamp was a glitch, and nothing moved. Monotone clocks see no difference at all, and every number in the docs is unchanged. What changes: a clock that was ahead heals two events after it is corrected rather than one, and heals every intent at once rather than only the ones later events touch; a reveal stamped ahead of the clock is pinned by the next event on any intent; a single backward stamp wipes nothing and closes nothing.
|
|
8
|
+
|
|
9
|
+
Two costs, stated. The first event after a correction is processed at the uncorrected time, so the intents it touches are decayed to that time and their episodes waiting under `attribution` can settle as misses before the second event moves everything back. And two bad stamps in a row read as a correction, which costs exactly what 0.10 paid on every backward step: the anchors move to the bad time and the next good event decays the gap again. One case no rule covers: a device that boots in 1970 and syncs forward later makes one monotone jump of decades, indistinguishable from a long absence, so hold `observe` until the device clock is trusted.
|
|
10
|
+
|
|
11
|
+
**State version 4.** The clock is stored, so `STATE_VERSION` is 4 and version 3 states (0.6.0 to 0.11.x) migrate on load: the clock is set to the latest time the stored anchors were written at, nothing is held, and no estimate changes. Under rule 5 of the stability policy, 0.11 reads a version 4 state as its own; what it loses is the clock: a backward stamp awaiting confirmation is forgotten, and the next event is trusted the way 0.11 trusted every event.
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- [#62](https://github.com/Yuribenjamin/oracular/pull/62) [`0502b78`](https://github.com/Yuribenjamin/oracular/commit/0502b789049cb8ef8643af587ec94b78ce0c39b1) Thanks [@Yuribenjamin](https://github.com/Yuribenjamin)! - **Size.** The whole `oracular` entry goes from 7.72 kB to 8.09 kB minified and gzipped with the clock rule and its state version, the quickstart import (`createState`, `observe`, `shouldReveal`, `markRevealed`) from 4.63 kB to 4.85 kB. That crosses the 7.9 kB ceiling the stability page set in 0.8, so the ceiling moves as it did then, to the shipped size plus about 10%: 8.9 kB, with the quickstart budget at 5 kB.
|
|
16
|
+
|
|
17
|
+
- [#62](https://github.com/Yuribenjamin/oracular/pull/62) [`0502b78`](https://github.com/Yuribenjamin/oracular/commit/0502b789049cb8ef8643af587ec94b78ce0c39b1) Thanks [@Yuribenjamin](https://github.com/Yuribenjamin)! - **Presets are read-only in the types too.** `presets` and `presetSources` are `DeepReadonly<...>`, matching the frozen objects: assigning into a preset is a type error, spreading one is unchanged.
|
|
18
|
+
|
|
19
|
+
## 0.11.0
|
|
20
|
+
|
|
21
|
+
### Minor Changes
|
|
22
|
+
|
|
23
|
+
- [#58](https://github.com/Yuribenjamin/oracular/pull/58) [`d6d1e87`](https://github.com/Yuribenjamin/oracular/commit/d6d1e87e176fc0112384e47fb4782e12d6fa1d1a) Thanks [@Yuribenjamin](https://github.com/Yuribenjamin)! - **Learning no longer drifts without outcomes.** The outcome rates a signal is judged by, with it and without it, were Laplace-smoothed, which centres both on 0.5 with one pseudo-hit each: two groups with no outcome at all then differ only by their sizes, so with zero outcomes a common signal drifted negative and a rare one positive, and a weight could change sign on no evidence (a weight of 2 seen in 90 of 100 outcome-free episodes ended at −0.59). Both rates are now shrunk toward the intent's own base rate by two pseudo-episodes, `(hits + 2·q₀) / (n + 2)` with `q₀ = outcomes / episodes`: a signal whose outcome rate equals the base rate learns exactly zero whatever the sample sizes, with no outcome every rate is 0 and no weight moves away from the developer's, and a weight never changes sign without outcomes on at least one side. The association-rule baseline becomes the base rate itself. `tune` fits with the same smoothing, so its weights, intervals and lifts move a little on small samples (a signal seen once with the outcome over a low base rate is worth a little less than before); on large samples the change is in the third decimal. Every learned number in the documentation's generated tables was regenerated. The state format is unchanged, and existing learned counts are kept: only the weight read from them moves.
|
|
24
|
+
|
|
3
25
|
## 0.10.0
|
|
4
26
|
|
|
5
27
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -193,8 +193,9 @@ A signal's weight is then a blend of your prior and the learned estimate:
|
|
|
193
193
|
```
|
|
194
194
|
nWith = hits + misses episodes with the signal
|
|
195
195
|
nWithout = episodes − nWith episodes without it
|
|
196
|
-
|
|
197
|
-
|
|
196
|
+
q₀ = outcomes / episodes the intent's own base rate
|
|
197
|
+
qWith = (hits + 2·q₀) / (nWith + 2) outcome rate with the signal
|
|
198
|
+
qWithout = (outcomes − hits + 2·q₀) / (nWithout + 2) outcome rate without it
|
|
198
199
|
lift = (qWith − qWithout) / (1 − qWithout) if qWith ≥ qWithout, else (qWith − qWithout) / qWithout → in [−1, 1]
|
|
199
200
|
learned = gain × |w₀| × lift 0 until the signal has history
|
|
200
201
|
nEff = (nWith × nWithout) / (nWith + nWithout) 0 if either side is 0
|
|
@@ -269,7 +270,7 @@ serialize(state): string
|
|
|
269
270
|
deserialize(json: string, opts?: { config?: EngineConfig; migrate?: Migrate }): EngineState
|
|
270
271
|
validateConfig(config: unknown): EngineConfig // the checks createState runs, without a state; returns the normalized copy
|
|
271
272
|
ms.seconds(n), ms.minutes(n), ms.hours(n), ms.days(n), ms.weeks(n) // durations in milliseconds
|
|
272
|
-
STATE_VERSION //
|
|
273
|
+
STATE_VERSION // 4
|
|
273
274
|
RECORD_VERSION // 1
|
|
274
275
|
```
|
|
275
276
|
|
|
@@ -339,7 +340,7 @@ interface SignalExplanation {
|
|
|
339
340
|
|
|
340
341
|
### Time
|
|
341
342
|
|
|
342
|
-
`at` is a timestamp. Every duration (`halfLife`, `cooldown`, `episodeTimeout`, `learning.halfLife`) is in the same unit as `at`. Milliseconds from `Date.now()` are the obvious choice, but the library never calls `Date.now()` itself: you pass time in, which keeps every function deterministic and makes tests trivial.
|
|
343
|
+
`at` is a timestamp. Every duration (`halfLife`, `cooldown`, `episodeTimeout`, `learning.halfLife`) is in the same unit as `at`. Milliseconds from `Date.now()` are the obvious choice, but the library never calls `Date.now()` itself: you pass time in, which keeps every function deterministic and makes tests trivial. The engine keeps a clock, the latest event time it trusts, and resolves every stamp against it. An event stamped behind the clock is tolerated: it counts now, at the clock's time, with no decay and no error, and its stamp is held. If the next event is also behind the clock, the clock was corrected (a device whose time was ahead caught up): every anchor ahead of the held stamp moves back to it, and decay and cooldowns resume from the corrected time. If the next event is at or after the clock, the held stamp was one glitch (a phone that woke up in 1970 before it synced) and nothing moved. So one event stamped years ahead holds an intent for at most the two events that follow it, and one stamped years behind wipes nothing and closes no session. A reveal or outcome stamped ahead of the clock counts as having happened now, and the next event pins it to its own time. A device that boots in 1970 and syncs forward later makes one monotone jump that no rule can tell from a long absence, so hold `observe` until its clock is trusted. When `at` is in milliseconds, `ms.days(7)`, `ms.hours(3)` and `ms.minutes(30)` spell the durations; they are plain multiplications.
|
|
343
344
|
|
|
344
345
|
## Persistence
|
|
345
346
|
|
|
@@ -357,7 +358,7 @@ const state = deserialize(saved, { config });
|
|
|
357
358
|
|
|
358
359
|
The new config is validated and swapped in. Intents and signals you no longer declare are dropped (α and β are recomputed from what remains), newly declared intents start at their prior, and everything learned about signals that still exist is kept.
|
|
359
360
|
|
|
360
|
-
States saved by an older version of this library are upgraded on load: `deserialize` migrates the state shapes written by oracular 0.1.0 to 0.
|
|
361
|
+
States saved by an older version of this library are upgraded on load: `deserialize` migrates the state shapes written by oracular 0.1.0 to 0.11.x (`STATE_VERSION` 1 to 3) to the current one on its own. A `migrate` function is consulted only for a version the library does not know, such as a state written by a newer oracular before a rollback; without one, that throws a `RangeError`:
|
|
361
362
|
|
|
362
363
|
```ts
|
|
363
364
|
deserialize(saved, {
|
package/dist/cli.js
CHANGED
|
@@ -437,6 +437,9 @@ import {
|
|
|
437
437
|
function laplace(successes, total) {
|
|
438
438
|
return (successes + 1) / (total + 2);
|
|
439
439
|
}
|
|
440
|
+
function smoothRate(successes, total, rate) {
|
|
441
|
+
return (successes + 2 * rate) / (total + 2);
|
|
442
|
+
}
|
|
440
443
|
function lift(q, b, type = "normalized-difference") {
|
|
441
444
|
if (type === "absolute-difference") return q - b;
|
|
442
445
|
if (type !== "normalized-difference") {
|
|
@@ -528,9 +531,10 @@ function fitSignal(signal, current, tally, strength, priorEpisodes, liftType) {
|
|
|
528
531
|
const nWith = hits + misses;
|
|
529
532
|
const without = Math.max(0, tally.episodes - nWith);
|
|
530
533
|
const outcomesWithout = Math.min(without, Math.max(0, tally.outcomes - hits));
|
|
531
|
-
const
|
|
532
|
-
const
|
|
533
|
-
const
|
|
534
|
+
const base = tally.episodes === 0 ? 0 : tally.outcomes / tally.episodes;
|
|
535
|
+
const qWith = smoothRate(hits, nWith, base);
|
|
536
|
+
const qWithout = smoothRate(outcomesWithout, without, base);
|
|
537
|
+
const baseline = liftType === "association-rule" ? smoothRate(hits + outcomesWithout, nWith + without, base) : qWithout;
|
|
534
538
|
const noContrast = without === 0;
|
|
535
539
|
const rawLift = noContrast ? null : nWith === 0 ? 0 : lift(qWith, baseline, liftType);
|
|
536
540
|
const score = rawLift === null ? null : nWith === 0 ? 0 : learningScore(rawLift, liftType);
|
package/dist/index.cjs
CHANGED
|
@@ -79,8 +79,8 @@ function lowerBound(alpha, beta, z, bound) {
|
|
|
79
79
|
function repeatFactor(n) {
|
|
80
80
|
return Math.log1p(1 / n) / Math.LN2;
|
|
81
81
|
}
|
|
82
|
-
function
|
|
83
|
-
return (successes +
|
|
82
|
+
function smoothRate(successes, total, rate) {
|
|
83
|
+
return (successes + 2 * rate) / (total + 2);
|
|
84
84
|
}
|
|
85
85
|
function lift(q, b, type = "normalized-difference") {
|
|
86
86
|
if (type === "absolute-difference") return q - b;
|
|
@@ -138,8 +138,9 @@ function mapRecord(record2, fn) {
|
|
|
138
138
|
var NONE = { hits: 0, misses: 0 };
|
|
139
139
|
function decayLearned(state, learning, at) {
|
|
140
140
|
if (state.learnedAt === null) return { ...state, learnedAt: at };
|
|
141
|
+
if (!(at > state.learnedAt)) return state;
|
|
141
142
|
const d = learning.halfLife === null ? 1 : decayFactor(at - state.learnedAt, learning.halfLife);
|
|
142
|
-
if (d === 1) return
|
|
143
|
+
if (d === 1) return { ...state, learnedAt: at };
|
|
143
144
|
return {
|
|
144
145
|
...state,
|
|
145
146
|
learnedAt: at,
|
|
@@ -154,12 +155,13 @@ function signalWeights(state, intent, learning, signal) {
|
|
|
154
155
|
}
|
|
155
156
|
const record2 = getOwn(state.learned, signal) ?? NONE;
|
|
156
157
|
const nWith = record2.hits + record2.misses;
|
|
157
|
-
const qWith = laplace(record2.hits, nWith);
|
|
158
158
|
const totalEpisodes = state.episodes.hits + state.episodes.misses;
|
|
159
159
|
const nWithout = Math.max(0, totalEpisodes - nWith);
|
|
160
160
|
const outcomesWithout = Math.min(nWithout, Math.max(0, state.episodes.hits - record2.hits));
|
|
161
|
-
const
|
|
162
|
-
const
|
|
161
|
+
const base = totalEpisodes === 0 ? 0 : state.episodes.hits / totalEpisodes;
|
|
162
|
+
const qWith = smoothRate(record2.hits, nWith, base);
|
|
163
|
+
const qWithout = smoothRate(outcomesWithout, nWithout, base);
|
|
164
|
+
const baseline = learning.lift === "association-rule" ? smoothRate(record2.hits + outcomesWithout, nWith + nWithout, base) : qWithout;
|
|
163
165
|
const score = learningScore(lift(qWith, baseline, learning.lift), learning.lift);
|
|
164
166
|
const learned = nWith === 0 ? 0 : learning.gain * Math.abs(initial) * score;
|
|
165
167
|
const nEff = nWith + nWithout === 0 ? 0 : nWith * nWithout / (nWith + nWithout);
|
|
@@ -250,8 +252,9 @@ function withContributions(state, intent, contributions) {
|
|
|
250
252
|
}
|
|
251
253
|
function decayIntent(state, intent, at) {
|
|
252
254
|
if (state.updatedAt === null) return { ...state, updatedAt: at };
|
|
255
|
+
if (!(at > state.updatedAt)) return state;
|
|
253
256
|
const d = decayFactor(at - state.updatedAt, intent.halfLife);
|
|
254
|
-
if (d === 1) return
|
|
257
|
+
if (d === 1) return { ...state, updatedAt: at };
|
|
255
258
|
const contributions = mapRecord(state.contributions, (c) => ({
|
|
256
259
|
alpha: c.alpha * d,
|
|
257
260
|
beta: c.beta * d
|
|
@@ -569,7 +572,7 @@ function markResolved(episode, intent) {
|
|
|
569
572
|
}
|
|
570
573
|
|
|
571
574
|
// src/version.ts
|
|
572
|
-
var STATE_VERSION =
|
|
575
|
+
var STATE_VERSION = 4;
|
|
573
576
|
var RECORD_VERSION = 1;
|
|
574
577
|
|
|
575
578
|
// src/state.ts
|
|
@@ -579,10 +582,16 @@ function createState(config) {
|
|
|
579
582
|
for (const [name, intent] of Object.entries(normalized.intents)) {
|
|
580
583
|
intents[name] = initIntent(intent);
|
|
581
584
|
}
|
|
582
|
-
return {
|
|
585
|
+
return {
|
|
586
|
+
version: STATE_VERSION,
|
|
587
|
+
config: normalized,
|
|
588
|
+
intents,
|
|
589
|
+
episode: emptyEpisode(),
|
|
590
|
+
clock: { at: null, behindAt: null }
|
|
591
|
+
};
|
|
583
592
|
}
|
|
584
593
|
function requireState(state) {
|
|
585
|
-
if (!isRecord(state) || state.version !== STATE_VERSION || !isRecord(state.config) || !isRecord(state.intents) || !isRecord(state.episode)) {
|
|
594
|
+
if (!isRecord(state) || state.version !== STATE_VERSION || !isRecord(state.config) || !isRecord(state.intents) || !isRecord(state.episode) || !isRecord(state.clock)) {
|
|
586
595
|
throw new TypeError("oracular: state must be an EngineState from createState or deserialize");
|
|
587
596
|
}
|
|
588
597
|
return state;
|
|
@@ -719,8 +728,10 @@ function explain(state, intent, at, opts) {
|
|
|
719
728
|
|
|
720
729
|
// src/observe.ts
|
|
721
730
|
function observe(state, event) {
|
|
722
|
-
|
|
723
|
-
const
|
|
731
|
+
const { name, at: stamp } = validateEvent(event);
|
|
732
|
+
const synced = syncClock(requireState(state), stamp);
|
|
733
|
+
let s = synced.state;
|
|
734
|
+
const at = synced.at;
|
|
724
735
|
const closeAt = timedOutAt(s.episode, s.config.episodeTimeout, at);
|
|
725
736
|
if (closeAt !== null) s = closeEpisode(s, closeAt);
|
|
726
737
|
const learning = resolveLearning(s.config);
|
|
@@ -733,7 +744,6 @@ function observe(state, event) {
|
|
|
733
744
|
let is = getIntentState(s, intentName, intent);
|
|
734
745
|
is = decayIntent(is, intent, at);
|
|
735
746
|
is = settlePending(is, intent, learning, at);
|
|
736
|
-
is = pinAnchors(is, at);
|
|
737
747
|
if (isSignal) {
|
|
738
748
|
const { effective } = signalWeights(is, intent, learning, name);
|
|
739
749
|
const n = occurrences(episode, intentName, name) + 1;
|
|
@@ -754,15 +764,36 @@ function observe(state, event) {
|
|
|
754
764
|
}
|
|
755
765
|
return { ...s, intents, episode };
|
|
756
766
|
}
|
|
757
|
-
function
|
|
758
|
-
const
|
|
759
|
-
|
|
760
|
-
if (
|
|
761
|
-
return {
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
767
|
+
function syncClock(state, stamp) {
|
|
768
|
+
const { at, behindAt } = state.clock;
|
|
769
|
+
if (at === null || stamp >= at) return { state: pinTo(state, stamp), at: stamp };
|
|
770
|
+
if (behindAt !== null) return syncClock(pinTo(state, behindAt), stamp);
|
|
771
|
+
return { state: { ...state, clock: { at, behindAt: stamp } }, at };
|
|
772
|
+
}
|
|
773
|
+
var INTENT_ANCHORS = ["updatedAt", "learnedAt", "lastRevealedAt", "lastOutcomeAt"];
|
|
774
|
+
var EPISODE_ANCHORS = ["startedAt", "lastEventAt"];
|
|
775
|
+
function pinTo(state, t) {
|
|
776
|
+
let intents = state.intents;
|
|
777
|
+
for (const [name, is] of Object.entries(state.intents)) {
|
|
778
|
+
let pinned = pinKeys(is, INTENT_ANCHORS, t);
|
|
779
|
+
if (is.pending.some((p) => p.endedAt > t)) {
|
|
780
|
+
pinned = {
|
|
781
|
+
...pinned,
|
|
782
|
+
pending: is.pending.map((p) => p.endedAt > t ? { ...p, endedAt: t } : p)
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
if (pinned !== is) intents = { ...intents, [name]: pinned };
|
|
786
|
+
}
|
|
787
|
+
const episode = pinKeys(state.episode, EPISODE_ANCHORS, t);
|
|
788
|
+
return { ...state, intents, episode, clock: { at: t, behindAt: null } };
|
|
789
|
+
}
|
|
790
|
+
function pinKeys(obj, keys, t) {
|
|
791
|
+
let out = obj;
|
|
792
|
+
for (const key of keys) {
|
|
793
|
+
const value = obj[key];
|
|
794
|
+
if (typeof value === "number" && value > t) out = { ...out, [key]: t };
|
|
795
|
+
}
|
|
796
|
+
return out;
|
|
766
797
|
}
|
|
767
798
|
function endEpisode(state, at) {
|
|
768
799
|
const s = requireState(state);
|
|
@@ -888,7 +919,8 @@ function createEndRecord(at, opts) {
|
|
|
888
919
|
// src/migrate.ts
|
|
889
920
|
var MIGRATIONS = {
|
|
890
921
|
1: fromV1,
|
|
891
|
-
2: fromV2
|
|
922
|
+
2: fromV2,
|
|
923
|
+
3: fromV3
|
|
892
924
|
};
|
|
893
925
|
function canMigrate(version) {
|
|
894
926
|
return hasOwn(MIGRATIONS, String(version));
|
|
@@ -929,6 +961,9 @@ function fromV2(stored) {
|
|
|
929
961
|
}
|
|
930
962
|
return { ...stored, version: 3, intents };
|
|
931
963
|
}
|
|
964
|
+
function fromV3(stored) {
|
|
965
|
+
return { ...stored, version: 4, clock: { at: null, behindAt: null } };
|
|
966
|
+
}
|
|
932
967
|
|
|
933
968
|
// src/serialize.ts
|
|
934
969
|
function serialize(state) {
|
|
@@ -1074,6 +1109,13 @@ function episodeState(value, path) {
|
|
|
1074
1109
|
intents: mapOwn(r.intents, `${path}.intents`, episodeIntent)
|
|
1075
1110
|
};
|
|
1076
1111
|
}
|
|
1112
|
+
function clockState(value, path) {
|
|
1113
|
+
const r = record(value, path);
|
|
1114
|
+
return {
|
|
1115
|
+
at: nullOrFinite(r.at, `${path}.at`),
|
|
1116
|
+
behindAt: nullOrFinite(r.behindAt, `${path}.behindAt`)
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1077
1119
|
function parseState(value) {
|
|
1078
1120
|
const r = record(value, "state");
|
|
1079
1121
|
if (r.version !== STATE_VERSION) {
|
|
@@ -1085,7 +1127,8 @@ function parseState(value) {
|
|
|
1085
1127
|
// when it is only stored, and the config passed in replaces it anyway.
|
|
1086
1128
|
config: normalizeConfig(r.config, false),
|
|
1087
1129
|
intents: mapOwn(r.intents, "state.intents", intentState),
|
|
1088
|
-
episode: episodeState(r.episode, "state.episode")
|
|
1130
|
+
episode: episodeState(r.episode, "state.episode"),
|
|
1131
|
+
clock: clockState(r.clock, "state.clock")
|
|
1089
1132
|
};
|
|
1090
1133
|
}
|
|
1091
1134
|
function requireComputable(state) {
|
|
@@ -1159,9 +1202,24 @@ function reconcile(raw, config) {
|
|
|
1159
1202
|
startedAt: raw.episode.startedAt,
|
|
1160
1203
|
lastEventAt: raw.episode.startedAt === null ? null : raw.episode.lastEventAt,
|
|
1161
1204
|
intents: episodeIntents
|
|
1162
|
-
}
|
|
1205
|
+
},
|
|
1206
|
+
clock: trustedClock(raw.clock, intents, raw.episode)
|
|
1163
1207
|
};
|
|
1164
1208
|
}
|
|
1209
|
+
function trustedClock(clock, intents, episode) {
|
|
1210
|
+
let at = clock.at;
|
|
1211
|
+
const see = (value) => {
|
|
1212
|
+
if (value !== null && (at === null || value > at)) at = value;
|
|
1213
|
+
};
|
|
1214
|
+
see(episode.startedAt);
|
|
1215
|
+
see(episode.lastEventAt);
|
|
1216
|
+
for (const is of Object.values(intents)) {
|
|
1217
|
+
see(is.updatedAt);
|
|
1218
|
+
see(is.lastOutcomeAt);
|
|
1219
|
+
}
|
|
1220
|
+
const behindAt = clock.behindAt !== null && at !== null && clock.behindAt < at ? clock.behindAt : null;
|
|
1221
|
+
return at === clock.at && behindAt === clock.behindAt ? clock : { at, behindAt };
|
|
1222
|
+
}
|
|
1165
1223
|
|
|
1166
1224
|
// src/units.ts
|
|
1167
1225
|
var ms = {
|