oracular 0.12.0 → 0.13.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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.13.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#69](https://github.com/Yuribenjamin/oracular/pull/69) [`02e0c44`](https://github.com/Yuribenjamin/oracular/commit/02e0c44be6e1ac167cf5503a38efd0eb38b1dc2d) Thanks [@Yuribenjamin](https://github.com/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.
8
+
9
+ ## 0.12.1
10
+
11
+ ### Patch Changes
12
+
13
+ - [#64](https://github.com/Yuribenjamin/oracular/pull/64) [`c108ca9`](https://github.com/Yuribenjamin/oracular/commit/c108ca92b4b66b90749664379bc676a230462726) Thanks [@Yuribenjamin](https://github.com/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.
14
+
3
15
  ## 0.12.0
4
16
 
5
17
  ### Minor Changes
package/README.md CHANGED
@@ -356,7 +356,7 @@ localStorage.setItem('oracular', serialize(state));
356
356
  const state = deserialize(saved, { config });
357
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
+ 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. The stored snapshot is then not read at all, so a state written by a newer release, with a config key or value this one does not know, still loads after a rollback; loading without a config runs the stored snapshot instead, and a key the library does not know is then an error.
360
360
 
361
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`:
362
362
 
package/dist/cli.js CHANGED
@@ -363,7 +363,7 @@ function formatIntent(intent) {
363
363
  ["threshold", "reveals", "per user", "precision", "recall", "median lead time"],
364
364
  intent.thresholds.map((t) => [
365
365
  `${t.threshold}${t.configured ? " *" : ""}`,
366
- String(t.reveals),
366
+ t.censored > 0 ? `${t.reveals} (+${t.censored} censored)` : String(t.reveals),
367
367
  num(t.revealsPerUser, 2),
368
368
  pct(t.precision),
369
369
  `${pct(t.recall)} of ${t.outcomes}`,
@@ -371,9 +371,9 @@ function formatIntent(intent) {
371
371
  ])
372
372
  )
373
373
  );
374
- const censored = intent.thresholds.reduce((acc, t) => Math.max(acc, t.censored), 0);
374
+ const censored = intent.thresholds.some((t) => t.censored > 0);
375
375
  out.push(
376
- `* the threshold your config resolves for this intent.${censored > 0 ? ` Reveals whose window runs past the end of the log are not scored (up to ${censored} per threshold).` : ""}`
376
+ `* the threshold your config resolves for this intent.${censored ? " A reveal whose window runs past the end of the log (the +n) is not scored for precision, since its verdict is not in yet; an outcome it preceded still counts as caught for recall." : ""}`
377
377
  );
378
378
  out.push("");
379
379
  out.push(
@@ -1310,9 +1310,22 @@ async function replayCommand(argv, io) {
1310
1310
  if (error instanceof RangeError) throw new UsageError(error.message);
1311
1311
  throw error;
1312
1312
  }
1313
+ warnShortLog(report, io);
1313
1314
  io.stdout(args.json ? formatJson(report) : formatText(report));
1314
1315
  return 0;
1315
1316
  }
1317
+ function warnShortLog(report, io) {
1318
+ if (report.start === null || report.end === null) return;
1319
+ const span = report.end - report.start;
1320
+ for (const intent of report.intents) {
1321
+ if (span < intent.window) {
1322
+ io.stderr(
1323
+ `oracular: warning: ${intent.intent}: the log spans ${formatDuration(span)}, less than its ${formatDuration(intent.window)} scoring window, so every reveal and point is censored and only recall can be counted. Pass --window with a duration shorter than the log, or record for longer than the window.
1324
+ `
1325
+ );
1326
+ }
1327
+ }
1328
+ }
1316
1329
  async function tuneCommand(argv, io) {
1317
1330
  const args = parseTuneArgs(argv);
1318
1331
  if (args.help) {
package/dist/index.cjs CHANGED
@@ -1005,9 +1005,9 @@ function deserialize(json, opts = {}) {
1005
1005
  stored = opts.migrate(parsed, parsed.version);
1006
1006
  }
1007
1007
  }
1008
- const raw = parseState(stored);
1009
- const config = opts.config === void 0 ? raw.config : normalizeConfig(opts.config);
1010
- return requireComputable(reconcile(raw, config));
1008
+ const config = opts.config === void 0 ? void 0 : normalizeConfig(opts.config);
1009
+ const raw = parseState(stored, config);
1010
+ return requireComputable(reconcile(raw, raw.config));
1011
1011
  }
1012
1012
  function typeError2(path, message) {
1013
1013
  throw new TypeError(`oracular: ${path} ${message}`);
@@ -1116,16 +1116,15 @@ function clockState(value, path) {
1116
1116
  behindAt: nullOrFinite(r.behindAt, `${path}.behindAt`)
1117
1117
  };
1118
1118
  }
1119
- function parseState(value) {
1119
+ function parseState(value, config) {
1120
1120
  const r = record(value, "state");
1121
1121
  if (r.version !== STATE_VERSION) {
1122
1122
  throw new RangeError(`oracular: state.version must be ${STATE_VERSION} after migration`);
1123
1123
  }
1124
+ const snapshot = record(r.config, "state.config");
1124
1125
  return {
1125
1126
  version: STATE_VERSION,
1126
- // Without the strict checks: a config this release would refuse at createState still loads
1127
- // when it is only stored, and the config passed in replaces it anyway.
1128
- config: normalizeConfig(r.config, false),
1127
+ config: config ?? normalizeConfig(snapshot, false),
1129
1128
  intents: mapOwn(r.intents, "state.intents", intentState),
1130
1129
  episode: episodeState(r.episode, "state.episode"),
1131
1130
  clock: clockState(r.clock, "state.clock")