oracular 0.3.0 → 0.5.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 CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#24](https://github.com/Yuribenjamin/oracular/pull/24) [`7809c6a`](https://github.com/Yuribenjamin/oracular/commit/7809c6af5c26ec836e43b76eac647bcf85c42514) Thanks [@Yuribenjamin](https://github.com/Yuribenjamin)! - **`npx oracular tune <log>... --config <file>`** seeds a config from a shadow log. Grouped by install, it reads what the engine itself counts, hits, misses and episode totals, off the state after every record, so it cannot disagree with what a device would learn from the same events. The base rate becomes the prior; the spread of per-install base rates becomes the prior strength (method of moments on a Beta, once the spread that sampling alone would produce is taken out; kept as it was below 10 installs); each declared signal's weight is `priorStrength × lift` over the episodes without it, with a 90% Wilson interval and the sample sizes on both sides; and the half-life that predicts the log best (lowest Brier score under `replay`) wins a grid search over 1h to 30d plus the current value. It prints the fit and the config to paste, which `replay --config` takes as it is.
8
+
9
+ Like replay, it counts shadow periods only: an episode in which any record was live is left out, a mixed log gets a warning, a live-only log is refused unless `--include-live` is passed. `--all-events` also measures every other event name in the log as a candidate signal; `--half-lives 12h,1d,3d` sets the grid; `--window` sets the scoring window when the intent has no cooldown. Every number is a starting point: the engine corrects the weights per user.
10
+
11
+ `oracular --help` now lists both commands; `oracular replay --help` and `oracular tune --help` have the options.
12
+
13
+ ## 0.4.0
14
+
15
+ ### Minor Changes
16
+
17
+ - [#22](https://github.com/Yuribenjamin/oracular/pull/22) [`0bb5841`](https://github.com/Yuribenjamin/oracular/commit/0bb5841fb93d2f16dcac74601f1ee218b70dda0e) Thanks [@Yuribenjamin](https://github.com/Yuribenjamin)! - Three small ergonomics for writing configs.
18
+
19
+ - **`validateConfig(config)`** runs the checks `createState` and `deserialize` run, without building a state, and returns the normalized copy they would store. It accepts `unknown`, so parsed JSON can be passed as it is; use it to fail fast at startup or in a test.
20
+ - **`ms`**: `ms.seconds(n)`, `ms.minutes(n)`, `ms.hours(n)`, `ms.days(n)` and `ms.weeks(n)` spell durations in milliseconds, so `halfLife: ms.days(7)` replaces `7 * 24 * 60 * 60 * 1000`. Plain multiplications, nothing else.
21
+ - **A JSON Schema for the config**, shipped in the package as `oracular/config.schema.json` and served at https://yuribenjamin.github.io/oracular/config.schema.json, for completion and checks on a config kept as JSON. `oracular replay --config file.json` now ignores a top-level `$schema` key, so a file can point at the schema and still be the config the engine runs.
22
+
3
23
  ## 0.3.0
4
24
 
5
25
  ### Minor Changes
package/README.md CHANGED
@@ -73,7 +73,7 @@ That block is copied verbatim into the test suite, so it always runs.
73
73
  - [API](#api)
74
74
  - [Persistence](#persistence)
75
75
  - [Explainability](#explainability)
76
- - [Shadow mode & replay](#shadow-mode--replay)
76
+ - [Shadow mode, replay and tune](#shadow-mode-replay-and-tune)
77
77
  - [Design rules](#design-rules)
78
78
  - [Roadmap](#roadmap)
79
79
  - [Contributing](#contributing)
@@ -250,6 +250,8 @@ createRecord(state, event, { installId, mode }): EventRecord // one recorder l
250
250
  createEndRecord(at, { installId, mode }): EndRecord // one recorder line: the episode ended
251
251
  serialize(state): string
252
252
  deserialize(json: string, opts?: { config?: EngineConfig; migrate?: Migrate }): EngineState
253
+ validateConfig(config: unknown): EngineConfig // the checks createState runs, without a state; returns the normalized copy
254
+ ms.seconds(n), ms.minutes(n), ms.hours(n), ms.days(n), ms.weeks(n) // durations in milliseconds
253
255
  STATE_VERSION // 2
254
256
  RECORD_VERSION // 1
255
257
  ```
@@ -286,7 +288,7 @@ interface RevealOptions {
286
288
  }
287
289
  ```
288
290
 
289
- Reveal options resolve per call → intent → engine → defaults. Invalid config throws a `TypeError` or `RangeError` with the offending path in the message.
291
+ Reveal options resolve per call → intent → engine → defaults. Invalid config throws a `TypeError` or `RangeError` with the offending path in the message. `validateConfig(config)` runs those checks without building a state and returns the normalized copy `createState` stores, so a bad weight fails at startup or in a test rather than on first use. For a config kept as JSON, the package ships a [JSON Schema](./schema/config.schema.json) as `oracular/config.schema.json`, also served at <https://yuribenjamin.github.io/oracular/config.schema.json>, for editor completion; `oracular replay` ignores the `$schema` key such a file carries, the engine does not.
290
292
 
291
293
  ### Results
292
294
 
@@ -317,7 +319,7 @@ interface SignalExplanation {
317
319
 
318
320
  ### Time
319
321
 
320
- `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. A clock that jumps backwards is tolerated (no decay, no error).
322
+ `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. A clock that jumps backwards is tolerated (no decay, no error). When `at` is in milliseconds, `ms.days(7)`, `ms.hours(3)` and `ms.minutes(30)` spell the durations; they are plain multiplications.
321
323
 
322
324
  ## Persistence
323
325
 
@@ -361,7 +363,7 @@ explain(state, 'upgrade', Date.now());
361
363
 
362
364
  Rows are sorted by the size of their impact. `limit.hit` has no history yet, so its effective weight is exactly the initial one. `pricing.viewed` was followed by an upgrade in six of its seven sessions while the three sessions without it saw one upgrade, so it learned a weight above the initial 3. `export.clicked` appeared in every session: it has no counterfactual, so whatever its hit rate, its weight stands at exactly the initial 1 until a session happens without it. See [Episodes & learning](https://yuribenjamin.github.io/oracular/guide/concepts/episodes-and-learning) for why learning needs contrast.
363
365
 
364
- ## Shadow mode & replay
366
+ ## Shadow mode, replay and tune
365
367
 
366
368
  Run oracular silently first. Record what it decides, export the log, and measure a config offline before any reveal is shown:
367
369
 
@@ -374,7 +376,13 @@ log.append(JSON.stringify(createRecord(state, event, { installId, mode: 'shadow'
374
376
  npx oracular replay shadow.ndjson --config oracular.config.json
375
377
  ```
376
378
 
377
- `replay` reports precision, recall, lead time and reveals per user for every intent and threshold, and a reliability table with a Brier score. It scores only the periods in which reveals were not shown, because once they are, the reveal itself causes outcomes. `installId` is a random id your app generates once and stores; it is never derived from the device. The log holds event names and times, never `props`, and leaves the device only if your app sends it. The [Shadow mode guide](https://yuribenjamin.github.io/oracular/guide/shadow-mode) has the workflow and the record format.
379
+ `replay` reports precision, recall, lead time and reveals per user for every intent and threshold, and a reliability table with a Brier score. It scores only the periods in which reveals were not shown, because once they are, the reveal itself causes outcomes. `installId` is a random id your app generates once and stores; it is never derived from the device. The log holds event names and times, never `props`, and leaves the device only if your app sends it.
380
+
381
+ ```sh
382
+ npx oracular tune shadow.ndjson --config oracular.config.json
383
+ ```
384
+
385
+ `tune` seeds a config from the same log: grouped by install, the base rate becomes the prior, the spread of per-install base rates the prior strength, each signal's lift over the episodes without it its weight, and the half-life that predicts the log best wins a grid search scored by `replay`. The counts come from the engine itself, so tune cannot disagree with what a device would learn. It prints sample sizes and a 90% interval per weight, and the config to paste; the engine corrects it per user from there. The [Shadow mode guide](https://yuribenjamin.github.io/oracular/guide/shadow-mode) has the workflow, the record format and both commands.
378
386
 
379
387
  ## Design rules
380
388
 
@@ -385,7 +393,7 @@ npx oracular replay shadow.ndjson --config oracular.config.json
385
393
 
386
394
  ## Roadmap
387
395
 
388
- Next is 0.3, themed **proof**: a recorder format for shadow mode, `npx oracular replay` to measure a config offline, per-episode saturation, and state robustness tests. Then cross-episode attribution and `tune` in 0.4, and a written stability policy before 1.0. Framework bindings wait for a third integration. The full list, with the reasoning and what was declined, is on the [roadmap page](https://yuribenjamin.github.io/oracular/guide/roadmap).
396
+ Now is 0.4, themed **learning that matches how people behave**: cross-episode attribution, so a session's signals can earn credit for an outcome days later, and presets. Shipped so far: `npx oracular tune` to seed a config from a shadow log, and small ergonomics (`validateConfig`, `ms`, a JSON Schema for the config). 0.3 shipped shadow mode, `npx oracular replay` and per-episode saturation. Then a written stability policy before 1.0. Framework bindings wait for a third integration. The full list, with the reasoning and what was declined, is on the [roadmap page](https://yuribenjamin.github.io/oracular/guide/roadmap).
389
397
 
390
398
  ## Contributing
391
399