Skip to content

Releases: Yuribenjamin/oracular

oracular@0.17.0

Choose a tag to compare

@Yuribenjamin Yuribenjamin released this 21 Oct 17:37
9ecfa1f

Minor Changes

  • #75 bdce2bd Thanks @Yuribenjamin! - Six lift measures are deprecated. 0.9 added learning.lift with seven association measures. A sweep of all seven on the clickstream case study found none that beats the default, 'normalized-difference', on either split, and 1.0 commits every value in the schema for the whole of 1.x. So 'absolute-difference', 'relative-lift', 'risk-ratio', 'log-lift', 'odds-ratio' and 'association-rule' are marked @deprecated in LiftType, named in the JSON Schema's description, and removed in 1.0. Nothing changes in this release: every value keeps working exactly as it did, a config that sets one keeps loading, and the type checker shows the mark. learning.lift itself stays, accepting the default, so that a measure can come back as a minor when a real integration shows it should.

oracular@0.16.0

Choose a tag to compare

@Yuribenjamin Yuribenjamin released this 21 Oct 17:27
b1a8acf

Minor Changes

  • #74 7e6b6fc Thanks @Yuribenjamin! - The outcome record keeps the estimate the reset wipes. createRecord takes the state after observe, so on the record of an intent's own outcome event p is what the engine decides right after it, with resetOnOutcome the prior (0.150 where it had been 0.506 one event earlier). Replay recomputes everything from the events and never reads it, but people read the log, and the number that matters most on an outcome line was gone. createRecord(state, event, options, previous) takes the state from before the event as an optional fourth argument; the decision of each intent whose outcome the event is then gains before, its p and lower at event.at on that state. Every other record is byte for byte what it was. RECORD_VERSION stays 1: a new optional field does not change it, readers ignore fields they do not know, and replay and tune never read decisions, so a log with before replays on every release that reads version 1. Found by the second production integration.

oracular@0.15.0

Choose a tag to compare

@Yuribenjamin Yuribenjamin released this 21 Oct 17:21
8f7552b

Minor Changes

  • #73 3363311 Thanks @Yuribenjamin! - An episode per outcome, for intents shaped as attempts. The episode is the session, shared by every intent, so for an upload that fails, completes, and completes again in one session the engine counted one episode: the second completion, with nothing seen before it, was a duplicate under 0.10's rule, and a second attempt that failed and never completed stayed neutral at the close. The failure signal never saw an outcome without it, which is the contrast learning needs, and tune printed "1 counted episode" where the app saw two. IntentConfig.episodePerOutcome: true makes each outcome an episode of its own for that intent: a later outcome in the same session counts as another episode even with nothing seen since, and signals seen after an outcome take a miss at the session's close, or wait under attribution. Everything else is as before, resetOnOutcome is independent, and tune follows the engine's counts. Off unless set: every state and every number without it is exactly what it was. With it set, a duplicated outcome event counts twice, so dedupe such events in the app for these intents. The JSON Schema lists it. Found by the second production integration, whose workaround, endEpisode on the outcome, ends the session for every other intent too.

    Rollback. A stored config that sets episodePerOutcome loads in 0.13 and later, which do not read the stored snapshot when the app passes its config; 0.12 and earlier throw a TypeError on the unknown key, so remove it from the saved config before rolling a deploy back that far. The state version is unchanged.

oracular@0.14.0

Choose a tag to compare

@Yuribenjamin Yuribenjamin released this 21 Oct 16:58
48a713b

Minor Changes

  • #71 d9e9769 Thanks @Yuribenjamin! - A cooldown of its own after the outcome. The outcome event paused reveals for the reveal cooldown: after a "stuck" intent's outcome, help was suppressed for the hour the cooldown named, and the next attempt that went wrong, exactly when the help should be back, got nothing; the only way out, cooldown: 0, disabled the reveal cooldown too. reveal.outcomeCooldown is the pause after the outcome, resolved per call → intent → engine like the other reveal options, 0 allowed; it defaults to the resolved cooldown, so nothing moves unless it is set. The JSON Schema lists it. Found by the second production integration.

    Rollback. A stored config that sets outcomeCooldown loads in 0.13 and later, which do not read the stored snapshot when the app passes its config; 0.12 and earlier throw a TypeError on the unknown key, so remove it from the saved config before rolling a deploy back that far. The state version is unchanged.

oracular@0.13.0

Choose a tag to compare

@Yuribenjamin Yuribenjamin released this 21 Oct 16:46
582ff74

Minor Changes

  • #69 02e0c44 Thanks @Yuribenjamin! - A rollback no longer trips over a config key it does not know. deserialize validated the config snapshot stored in the state even when the app passed its current config, which replaces it. So a state written by a release that set a new option (bound in 0.8, learning.lift in 0.9) threw a TypeError in the release before it, the changelogs asked for the key to be removed from the saved config before rolling a deploy back, and in @oracular/react the load was a 'parse' failure that blocked writes until clear(). With a config passed, the stored snapshot is now not read at all: a key or a value this release does not know is no obstacle, and every number the state holds comes through as before. Without a config, the stored snapshot runs the engine and is read as it was: without the checks that refuse a config that cannot work, and with a key the library does not know still an error, since the engine would otherwise run without an option the state was built under. Nothing changes for a state that loaded before. Under rule 6 of the stability policy, states now move between releases in both directions for the config snapshot as they already did for the state's fields, and the policy says so.

oracular@0.12.1

Choose a tag to compare

@Yuribenjamin Yuribenjamin released this 21 Oct 15:55
41c720d

Patch Changes

  • #64 c108ca9 Thanks @Yuribenjamin! - replay shows the reveals it could not score. A reveal whose window runs past the end of the log is not scored for precision, since its verdict is not in yet, but an outcome it preceded still counts for recall, so a short log printed reveals 0 beside recall 100.0% of 1, and --json showed reveals: 0, caught: 1, censored: 1. Both numbers were right and the table did not say why. The text table now shows such reveals in the row (0 (+1 censored)) with a footnote that says what recall still counts, and a log shorter than an intent's scoring window, in which every reveal and point is censored, gets a warning on stderr that points at --window. The --json report is unchanged: censored was already there. Found by the second production integration on a one-minute log with the default one-hour window.

oracular@0.12.0

Choose a tag to compare

@Yuribenjamin Yuribenjamin released this 21 Oct 15:19
17883a8

Minor Changes

  • #62 0502b78 Thanks @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.

    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.

    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.

Patch Changes

  • #62 0502b78 Thanks @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.

  • #62 0502b78 Thanks @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.

oracular@0.11.0

Choose a tag to compare

@Yuribenjamin Yuribenjamin released this 21 Oct 05:45
837f9d0

Minor Changes

  • #58 d6d1e87 Thanks @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.

@oracular/react@0.2.2

Choose a tag to compare

@Yuribenjamin Yuribenjamin released this 21 Oct 16:26
282b822

Patch Changes

  • #67 709d782 Thanks @Yuribenjamin! - Hydration with a store that loaded first. The hooks hydrated with the store's current state, so following the cold-start advice, await store.load() before the first screen renders, under server rendering made the client's first render disagree with the markup the server sent: React reported a hydration mismatch and rendered the tree again. useIntent's state, usePrediction and useIntentSelector now hydrate with the fresh state the server rendered, store.getInitialState() (new: the state the store was created with, the same object for its life), and then show the loaded one; usePrediction is computed from that snapshot rather than read from the store, with the same re-rendering as before. The functions useIntent returns go to the current state, as documented, and belong in handlers in a server-rendered tree.

    An adapter that returns undefined counts as an empty key. An adapter written in JavaScript that answered undefined instead of null for a missing key was treated as unreadable data: reported as 'parse', writes blocked until clear(). It is now an empty key, as the shipped adapters already report.

@oracular/react@0.2.1

Choose a tag to compare

@Yuribenjamin Yuribenjamin released this 21 Oct 14:34
29ea661

Patch Changes

  • #60 ac0b2bc Thanks @Yuribenjamin! - A parse failure is reported once and stops the re-reads. Stored data that deserialize rejects blocked writes, as it should, but every debounced save then read it again, parsed it again and reported 'parse' again, for data that cannot parse any better the second time. Saves after a parse failure now read and write nothing; load() reads again and reports only when the stored data changed, and clear() starts over, which is the handling to pass: onError: (error, phase) => { if (phase === 'parse') void store.clear(); }. A read that throws is still retried by every save, since the next attempt may succeed.

    An empty key counts as saved. A store that found nothing stored wrote a fresh state on the first flush or unmount even when nothing had been tracked, and so did a store after clear(). An empty key holds what a fresh state holds, so those writes are gone; the first event writes as before.

    Cold start, in the docs. Before the read settles, reveal decides on the state in memory, not on the stored history. The README and the guide now say to await store.load() before the first screen renders where a nudge there must respect a stored cooldown.