@konneal/engine 0.2.155 → 0.2.158

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.
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  handleAsk
3
- } from "./chunk-TXF5VMNM.js";
3
+ } from "./chunk-STDXSVLX.js";
4
4
  import "./chunk-ECOYO7EX.js";
5
- import "./chunk-VYEMFSU5.js";
5
+ import "./chunk-PBHMQZKA.js";
6
6
  import "./chunk-G2RE4XRV.js";
7
7
  import "./chunk-NI4CIK6W.js";
8
8
  import "./chunk-W7J5SFXJ.js";
@@ -271,7 +271,7 @@ function parseNumber(raw) {
271
271
  return Number(s.replace(/,(?=\d{3}\b)/g, ""));
272
272
  }
273
273
  var TOL_SEP_SRC = "\\u00b1|\\bpm\\b|\\+/-";
274
- var NUM_SRC = "[0-9][0-9 ,.]*[0-9]";
274
+ var NUM_SRC = "[0-9]+(?:[0-9 ,.]*[0-9])?";
275
275
  var UNIT_SRC = "%|\\u00b0?[A-Za-z\\u00b5\\u03a9]{0,6}";
276
276
  function statedUncertainty(value, tolRaw, tolUnitRaw, valueUnit) {
277
277
  const tolNum = parseNumber(tolRaw);
@@ -404,6 +404,42 @@ function evaluate(content, query, opts) {
404
404
  const decided = corners.filter((c2) => c2 !== null);
405
405
  if (decided.length && decided.some((c2) => c2 !== result)) {
406
406
  voidReason = voidReason ?? `the stated uncertainty (${un.stated}) on ${first.split(".").pop()} crosses the decision boundary \u2014 the check evaluates ${result ? "true" : "false"} at the stated value but ${decided.find((c2) => c2 !== result) ? "true" : "false"} at the band edge; the question cannot be decided as stated`;
407
+ } else if (decided.length === corners.length && un.tol > 0) {
408
+ const flipsAt = (scale) => {
409
+ for (const dir of [1, -1]) {
410
+ const corner = { ...params, [first]: params[first] + dir * scale * un.tol };
411
+ try {
412
+ if (!!parseAndEval(expression, corner) !== result) return true;
413
+ } catch {
414
+ return true;
415
+ }
416
+ }
417
+ return false;
418
+ };
419
+ if (flipsAt(20)) {
420
+ let lo = 1, hi = 20;
421
+ for (let i = 0; i < 12; i++) {
422
+ const mid = (lo + hi) / 2;
423
+ if (flipsAt(mid)) hi = mid;
424
+ else lo = mid;
425
+ }
426
+ const marginTol = hi * un.tol;
427
+ uncertainty = {
428
+ ...uncertainty,
429
+ margin_tol: marginTol,
430
+ margin_bounded: true,
431
+ coverage: "k=2",
432
+ safety_factor: marginTol / (2 * un.tol)
433
+ };
434
+ } else {
435
+ uncertainty = {
436
+ ...uncertainty,
437
+ margin_tol: 20 * un.tol,
438
+ margin_bounded: false,
439
+ coverage: "k=2",
440
+ safety_factor: 20 * un.tol / (2 * un.tol)
441
+ };
442
+ }
407
443
  }
408
444
  }
409
445
  }
@@ -32,7 +32,7 @@ import {
32
32
  tableRetyped,
33
33
  toolNote,
34
34
  verdictNote
35
- } from "./chunk-VYEMFSU5.js";
35
+ } from "./chunk-PBHMQZKA.js";
36
36
  import {
37
37
  NO_CONTEXT,
38
38
  STAGE_NAMES,
@@ -2914,7 +2914,25 @@ Answer account questions from these records ONLY: name the record when you use i
2914
2914
  ...h.metadata.table_selection ? { sel: h.metadata.table_selection } : {}
2915
2915
  }));
2916
2916
  const tq = await chargeAnswerTokens(env, bucketId, void 0, messageChars(messages), answer.length, probeTokensUsed);
2917
- return json({ ...out, context: contextOut, read: readAs(), quota: { ...quota, ...tq, credits: await creditBudget(env, bucketId), credit_estimate: creditPreview } }, 200, { ...corsHeaders(req), "server-timing": serverTiming() });
2917
+ const manifestOut = body?.manifest === true ? { manifest: {
2918
+ index_version: env.INDEX_VERSION,
2919
+ corpus_gen: gen,
2920
+ model,
2921
+ effort: answerEffort(env),
2922
+ fresh,
2923
+ citations: used.length,
2924
+ retrieval: used.map((h, i) => ({
2925
+ rank: i + 1,
2926
+ doc_id: h.metadata.doc_id,
2927
+ clause_anchor: h.metadata.clause_anchor ?? null,
2928
+ score: h.score,
2929
+ rerank_score: h.rerank_score ?? null
2930
+ })),
2931
+ stages: stageTiming,
2932
+ generate_retries: generateRetries,
2933
+ total_ms: Date.now() - tStart
2934
+ } } : {};
2935
+ return json({ ...out, context: contextOut, read: readAs(), ...manifestOut, quota: { ...quota, ...tq, credits: await creditBudget(env, bucketId), credit_estimate: creditPreview } }, 200, { ...corsHeaders(req), "server-timing": serverTiming() });
2918
2936
  };
2919
2937
  if (liveResponse) {
2920
2938
  void pipeline(live, q).catch(() => {
@@ -0,0 +1,29 @@
1
+ /** The model's raw response → the situating context: trimmed, the
2
+ * wrapping quotes stripped, capped at 400 chars. Empty string = the
3
+ * model had nothing to say (the deterministic-empty class). */
4
+ export declare function normalizeContext(raw: unknown): string;
5
+ export type EnrichOutcome = {
6
+ action: "serve";
7
+ context: string;
8
+ cached: boolean;
9
+ } | {
10
+ action: "serve-raw";
11
+ } | {
12
+ action: "fail";
13
+ error: string;
14
+ };
15
+ /** The per-chunk decision, given the cache read and the generation.
16
+ * - a context (cached or generated) serves enriched;
17
+ * - an EMPTY generation in the production flow serves the chunk RAW
18
+ * (the deterministic-empty class, 2026-10-20: real corpus content —
19
+ * B 18's thin terms sections, two R 134-1 clauses — must not vanish
20
+ * from the index because its enrichment has nothing to say; a later
21
+ * forced re-enrich overwrites the vector in place);
22
+ * - an empty generation in mode:context / mode:ab FAILS: those
23
+ * builders want the signal, not a side-effecting write. */
24
+ export declare function enrichOutcome(opts: {
25
+ cached: string | null;
26
+ generated: string;
27
+ contextOnly: boolean;
28
+ abMode: boolean;
29
+ }): EnrichOutcome;
@@ -4,7 +4,7 @@ import {
4
4
  parseToolCall,
5
5
  runTool,
6
6
  toolNote
7
- } from "./chunk-VYEMFSU5.js";
7
+ } from "./chunk-PBHMQZKA.js";
8
8
  import "./chunk-NI4CIK6W.js";
9
9
  import "./chunk-4NXH5ZI6.js";
10
10
  import "./chunk-VT7DR6NQ.js";
package/dist/verdict.d.ts CHANGED
@@ -4,7 +4,13 @@ export interface MachineCheck {
4
4
  values: Record<string, number>;
5
5
  result: boolean | null;
6
6
  /** present when the question states a tolerance on a symbol this
7
- * check reads: the band's corner evaluations beside the nominal */
7
+ * check reads: the band's corner evaluations beside the nominal,
8
+ * and — when the band is robust (every corner agrees with the
9
+ * nominal) — the GUM-style budget: the tolerance magnitude that
10
+ * would flip the decision (bisected up to 20× the stated band;
11
+ * margin_bounded false when even 20× does not flip), and the safety
12
+ * factor of that margin over the k=2 expanded uncertainty
13
+ * (U = 2u, the ≈95% coverage a metrologist reports). */
8
14
  uncertainty?: {
9
15
  symbol: string;
10
16
  stated: string;
@@ -12,6 +18,10 @@ export interface MachineCheck {
12
18
  unit?: string;
13
19
  nominal: boolean;
14
20
  corners: (boolean | null)[];
21
+ margin_tol?: number;
22
+ margin_bounded?: boolean;
23
+ coverage?: "k=2";
24
+ safety_factor?: number;
15
25
  };
16
26
  }
17
27
  export interface Verdict {
@@ -4,13 +4,13 @@ import {
4
4
  handleMemories,
5
5
  scoreFaithfulness,
6
6
  scoreJudge
7
- } from "../../chunk-TXF5VMNM.js";
7
+ } from "../../chunk-STDXSVLX.js";
8
8
  import "../../chunk-ECOYO7EX.js";
9
9
  import {
10
10
  TOOLS_REGISTRY,
11
11
  bindModelNode,
12
12
  modelGroundingBlock
13
- } from "../../chunk-VYEMFSU5.js";
13
+ } from "../../chunk-PBHMQZKA.js";
14
14
  import {
15
15
  handleSearch
16
16
  } from "../../chunk-OL7KKGEO.js";
@@ -351,6 +351,18 @@ async function handleGetShared(env, slug) {
351
351
  // workers/worker_public/prompts/enrichment.md
352
352
  var enrichment_default = "You write a retrieval context for a passage from {{CORPUS_KIND}}. The context is prepended to the passage before embedding so a semantic search can locate the passage even when the query uses different vocabulary than the passage itself.\n\nWrite ONE concise sentence (at most 40 words) that situates the passage: name the publication by its exact {{PUBLISHER_NAME}} identifier (including part or annex when applicable) and what the passage covers \u2014 paraphrasing the topic in words DIFFERENT from the passage's own. Do not copy the passage verbatim, do not add facts that are not derivable from the passage or its header, do not answer or explain the content. Reply with the context sentence only \u2014 no quotes, no preamble.\n";
353
353
 
354
+ // workers/worker_public/src/enrichPolicy.ts
355
+ function normalizeContext(raw) {
356
+ return typeof raw === "string" ? raw.trim().replace(/^["\']|["']$/g, "").slice(0, 400) : "";
357
+ }
358
+ function enrichOutcome(opts) {
359
+ const fromCache = typeof opts.cached === "string" && opts.cached;
360
+ if (fromCache) return { action: "serve", context: fromCache, cached: true };
361
+ if (opts.generated) return { action: "serve", context: opts.generated, cached: false };
362
+ if (opts.contextOnly || opts.abMode) return { action: "fail", error: "empty enrichment" };
363
+ return { action: "serve-raw" };
364
+ }
365
+
354
366
  // workers/worker_public/prompts/doc-summary.md
355
367
  var doc_summary_default = "You write the document-level summary for a legal-metrology publication in {{PUBLISHER_NAME}}'s retrieval index. The section summaries below cover the publication clause by clause. Write one compact abstract, three to five sentences: what the publication regulates or provides, its scope and applicability, and the structure a reader should know (parts, annexes, where the normative core sits). Use the publication's own terms exactly. No preamble, no headings.\n";
356
368
 
@@ -408,10 +420,11 @@ ${c.text.slice(0, 1500)}` }
408
420
  reasoning_effort: abMode ? effort : "high"
409
421
  });
410
422
  const raw = typeof res?.response === "string" && res.response.trim() ? res.response : res?.choices?.[0]?.message?.content;
411
- context = typeof raw === "string" ? raw.trim().replace(/^["\']|[\"']$/g, "").slice(0, 400) : "";
423
+ context = normalizeContext(raw);
412
424
  if (!context) {
413
425
  console.log("enrich raw keys:", Object.keys(res ?? {}), "sample:", JSON.stringify(res).slice(0, 300));
414
- if (contextOnly || abMode) return { id: c.id, ok: false, error: "empty enrichment" };
426
+ const outcome = enrichOutcome({ cached: null, generated: "", contextOnly, abMode });
427
+ if (outcome.action === "fail") return { id: c.id, ok: false, error: outcome.error };
415
428
  const rawText = typeof c.metadata.chunk_text === "string" && c.metadata.chunk_text ? c.metadata.chunk_text : c.text;
416
429
  const rawVector = await embed(portModelRunner(env), MODELS.embed, rawText.slice(0, 6e3));
417
430
  await env.VECTORIZE.upsert([{ id: c.id, values: rawVector, metadata: { ...c.metadata, chunk_text: rawText } }]);
@@ -1105,13 +1118,13 @@ async function callToolOnce(env, ctx, _req, tier, key, name, args, t0) {
1105
1118
  // stream:false forces the JSON lane (anon defaults to SSE)
1106
1119
  body: JSON.stringify({ ...args, stream: false })
1107
1120
  });
1108
- const { runTool, TOOLS_REGISTRY: TOOLS_REGISTRY2 } = await import("../../tools-CWPQQTBQ.js");
1121
+ const { runTool, TOOLS_REGISTRY: TOOLS_REGISTRY2 } = await import("../../tools-W5HGME7W.js");
1109
1122
  let payload;
1110
1123
  if (TOOLS_REGISTRY2.some((t) => t.name === name && t.audiences.includes("mcp"))) {
1111
1124
  const r = await runTool({ ...env, DB: env.DB }, { name, args }, "mcp");
1112
1125
  payload = r ? { tool: r.name, query: r.query, result: r.output } : { error: { message: `tool ${name} returned nothing for the given arguments` } };
1113
1126
  } else {
1114
- const res = name === "ask" ? await (await import("../../ask-MRVXR7CY.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-GAU5SEZI.js")).handleSearch(env, ctx, inner, tier, key);
1127
+ const res = name === "ask" ? await (await import("../../ask-7GRQQEBN.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-GAU5SEZI.js")).handleSearch(env, ctx, inner, tier, key);
1115
1128
  payload = await res.json().catch(() => ({ error: { message: "tool transport failed", status: res.status } }));
1116
1129
  }
1117
1130
  telemetry(env, ctx, tier, `mcp:${name}`, null, !payload?.error, JSON.stringify(payload).length, await sha256Hex(`${name}:${JSON.stringify(args ?? {})}`), void 0, void 0, { durationMs: Date.now() - t0 });
package/package.json CHANGED
@@ -27,7 +27,7 @@
27
27
  "playwright": "^1.62.1",
28
28
  "yaml": "^2.9.1"
29
29
  },
30
- "version": "0.2.155",
30
+ "version": "0.2.158",
31
31
  "description": "The Konneal engine: the publisher-agnostic build pipeline and API plane for standards intelligence (retrieval, answer contract, verdicts, evaluation).",
32
32
  "license": "BSD-3-Clause",
33
33
  "type": "module",
@@ -6,6 +6,7 @@ import { embed } from "./ai";
6
6
  import { err, json, corsHeaders, readJson, authenticate } from "./lib/http";
7
7
  import type { Env } from "./env";
8
8
  import enrichmentPrompt from "../prompts/enrichment.md";
9
+ import { normalizeContext, enrichOutcome } from "./enrichPolicy.ts";
9
10
  import docSummaryPrompt from "../prompts/doc-summary.md";
10
11
  import familySummaryPrompt from "../prompts/family-summary.md";
11
12
  import sectionSummaryPrompt from "../prompts/section-summary.md";
@@ -75,19 +76,14 @@ export async function handleEnrich(env: Env, ctx: ExecutionContext, req: Request
75
76
  const raw = typeof res?.response === "string" && res.response.trim()
76
77
  ? res.response
77
78
  : res?.choices?.[0]?.message?.content;
78
- context = typeof raw === "string" ? raw.trim().replace(/^["\']|[\"']$/g, "").slice(0, 400) : "";
79
+ context = normalizeContext(raw);
79
80
  if (!context) {
80
81
  console.log("enrich raw keys:", Object.keys(res ?? {}), "sample:", JSON.stringify(res).slice(0, 300));
81
- // The deterministic-empty class (observed 2026-10-20 on B 18's
82
- // thin terms sections and two R 134-1 clauses — empty under
83
- // BOTH the default and the alternate enrichment model): the
84
- // chunk's own text is real corpus content, so the production
85
- // flow serves it RAW rather than leaving a hole in the index
86
- // — embed the original text without the context band, upsert
87
- // without the ctx marker; a later forced re-enrich overwrites
88
- // the vector in place. mode:"context" and "ab" keep failing:
89
- // their builders want the signal, not a side-effecting write.
90
- if (contextOnly || abMode) return { id: c.id, ok: false, error: "empty enrichment" };
82
+ // serve-raw: the chunk's own text is real corpus content —
83
+ // embed it without the context band, upsert without the ctx
84
+ // marker; a later forced re-enrich overwrites in place.
85
+ const outcome = enrichOutcome({ cached: null, generated: "", contextOnly, abMode });
86
+ if (outcome.action === "fail") return { id: c.id, ok: false, error: outcome.error };
91
87
  const rawText = typeof c.metadata.chunk_text === "string" && c.metadata.chunk_text ? c.metadata.chunk_text : c.text;
92
88
  const rawVector = await embed(portModelRunner(env), MODELS.embed, rawText.slice(0, 6000));
93
89
  await env.VECTORIZE.upsert([{ id: c.id, values: rawVector, metadata: { ...c.metadata, chunk_text: rawText } }]);
@@ -1969,7 +1969,33 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1969
1969
  ...((h.metadata as any).table_selection ? { sel: (h.metadata as any).table_selection } : {}),
1970
1970
  }));
1971
1971
  const tq = await chargeAnswerTokens(env, bucketId, undefined, messageChars(messages!), answer!.length, probeTokensUsed);
1972
- return json({ ...out, context: contextOut, read: readAs(), quota: { ...quota, ...tq, credits: await creditBudget(env, bucketId), credit_estimate: creditPreview } }, 200, { ...corsHeaders(req), "server-timing": serverTiming() });
1972
+ // the reproducibility manifest (TODO.sota/14.6, v1): an opt-in, one-object
1973
+ // derivation record — the served snapshot identity, the answer model, the
1974
+ // retrieval anatomy (rank, corpus score, rerank score per used passage),
1975
+ // the stage timings, and the enforcement counters. Everything here is
1976
+ // already computed; the flag only ASSEMBLES it for the harness and for
1977
+ // external auditors. v2 names the CRAG grades per passage.
1978
+ const manifestOut = body?.manifest === true
1979
+ ? { manifest: {
1980
+ index_version: env.INDEX_VERSION,
1981
+ corpus_gen: gen,
1982
+ model,
1983
+ effort: answerEffort(env),
1984
+ fresh,
1985
+ citations: used.length,
1986
+ retrieval: used.map((h: Hit, i: number) => ({
1987
+ rank: i + 1,
1988
+ doc_id: h.metadata.doc_id,
1989
+ clause_anchor: h.metadata.clause_anchor ?? null,
1990
+ score: h.score,
1991
+ rerank_score: h.rerank_score ?? null,
1992
+ })),
1993
+ stages: stageTiming,
1994
+ generate_retries: generateRetries,
1995
+ total_ms: Date.now() - tStart,
1996
+ } }
1997
+ : {};
1998
+ return json({ ...out, context: contextOut, read: readAs(), ...manifestOut, quota: { ...quota, ...tq, credits: await creditBudget(env, bucketId), credit_estimate: creditPreview } }, 200, { ...corsHeaders(req), "server-timing": serverTiming() });
1973
1999
  }
1974
2000
 
1975
2001
  // THE DISPATCH: the streamed lane's response went out at the top — the
@@ -0,0 +1,40 @@
1
+ // The enrichment route's decision policy, dependency-free so plain node
2
+ // can load it for unit tests (admin.ts imports .md prompts that only
3
+ // the bundler resolves — the serve-raw policy rode the live
4
+ // verification alone until this extraction). The handler keeps the
5
+ // I/O: cache, generation, embedding, upsert; this module owns the
6
+ // DECISION table — what a chunk's outcome is, given what came back.
7
+
8
+ /** The model's raw response → the situating context: trimmed, the
9
+ * wrapping quotes stripped, capped at 400 chars. Empty string = the
10
+ * model had nothing to say (the deterministic-empty class). */
11
+ export function normalizeContext(raw: unknown): string {
12
+ return typeof raw === "string" ? raw.trim().replace(/^["\']|["']$/g, "").slice(0, 400) : "";
13
+ }
14
+
15
+ export type EnrichOutcome =
16
+ | { action: "serve"; context: string; cached: boolean }
17
+ | { action: "serve-raw" }
18
+ | { action: "fail"; error: string };
19
+
20
+ /** The per-chunk decision, given the cache read and the generation.
21
+ * - a context (cached or generated) serves enriched;
22
+ * - an EMPTY generation in the production flow serves the chunk RAW
23
+ * (the deterministic-empty class, 2026-10-20: real corpus content —
24
+ * B 18's thin terms sections, two R 134-1 clauses — must not vanish
25
+ * from the index because its enrichment has nothing to say; a later
26
+ * forced re-enrich overwrites the vector in place);
27
+ * - an empty generation in mode:context / mode:ab FAILS: those
28
+ * builders want the signal, not a side-effecting write. */
29
+ export function enrichOutcome(opts: {
30
+ cached: string | null;
31
+ generated: string;
32
+ contextOnly: boolean;
33
+ abMode: boolean;
34
+ }): EnrichOutcome {
35
+ const fromCache = typeof opts.cached === "string" && opts.cached;
36
+ if (fromCache) return { action: "serve", context: fromCache, cached: true };
37
+ if (opts.generated) return { action: "serve", context: opts.generated, cached: false };
38
+ if (opts.contextOnly || opts.abMode) return { action: "fail", error: "empty enrichment" };
39
+ return { action: "serve-raw" };
40
+ }
@@ -20,7 +20,13 @@ export interface MachineCheck {
20
20
  values: Record<string, number>;
21
21
  result: boolean | null; // null: could not evaluate (missing params)
22
22
  /** present when the question states a tolerance on a symbol this
23
- * check reads: the band's corner evaluations beside the nominal */
23
+ * check reads: the band's corner evaluations beside the nominal,
24
+ * and — when the band is robust (every corner agrees with the
25
+ * nominal) — the GUM-style budget: the tolerance magnitude that
26
+ * would flip the decision (bisected up to 20× the stated band;
27
+ * margin_bounded false when even 20× does not flip), and the safety
28
+ * factor of that margin over the k=2 expanded uncertainty
29
+ * (U = 2u, the ≈95% coverage a metrologist reports). */
24
30
  uncertainty?: {
25
31
  symbol: string;
26
32
  stated: string;
@@ -28,6 +34,10 @@ export interface MachineCheck {
28
34
  unit?: string;
29
35
  nominal: boolean;
30
36
  corners: (boolean | null)[];
37
+ margin_tol?: number;
38
+ margin_bounded?: boolean;
39
+ coverage?: "k=2";
40
+ safety_factor?: number;
31
41
  };
32
42
  }
33
43
 
@@ -223,7 +233,10 @@ export interface StatedUncertainty {
223
233
  }
224
234
 
225
235
  const TOL_SEP_SRC = "\\u00b1|\\bpm\\b|\\+/-";
226
- const NUM_SRC = "[0-9][0-9 ,.]*[0-9]";
236
+ // single digits bind too — a metrologist's "± 5 v" is a first-class
237
+ // tolerance (the two-digit minimum silently dropped every one-digit
238
+ // value and tolerance; found by the GUM-budget tests, 2026-10-20)
239
+ const NUM_SRC = "[0-9]+(?:[0-9 ,.]*[0-9])?";
227
240
  const UNIT_SRC = "%|\\u00b0?[A-Za-z\\u00b5\\u03a9]{0,6}";
228
241
 
229
242
  function statedUncertainty(
@@ -392,6 +405,43 @@ export function evaluate(content: unknown, query: string, opts?: { dimensions?:
392
405
  `the check evaluates ${result ? "true" : "false"} at the stated value but ` +
393
406
  `${decided.find((c) => c !== result) ? "true" : "false"} at the band edge; the question cannot be decided as stated`
394
407
  );
408
+ } else if (decided.length === corners.length && un.tol > 0) {
409
+ // the band is robust: report the GUM-style budget — bisect
410
+ // the tolerance scaling for the flip point (both directions,
411
+ // up to 20× the stated band), then state the safety factor
412
+ // over the k=2 expanded uncertainty (U = 2u)
413
+ const flipsAt = (scale: number): boolean => {
414
+ for (const dir of [1, -1]) {
415
+ const corner = { ...params, [first]: params[first]! + dir * scale * un.tol };
416
+ try {
417
+ if (!!parseAndEval(expression, corner) !== result) return true;
418
+ } catch { return true; }
419
+ }
420
+ return false;
421
+ };
422
+ if (flipsAt(20)) {
423
+ let lo = 1, hi = 20;
424
+ for (let i = 0; i < 12; i++) {
425
+ const mid = (lo + hi) / 2;
426
+ if (flipsAt(mid)) hi = mid; else lo = mid;
427
+ }
428
+ const marginTol = hi * un.tol;
429
+ uncertainty = {
430
+ ...uncertainty!,
431
+ margin_tol: marginTol,
432
+ margin_bounded: true,
433
+ coverage: "k=2",
434
+ safety_factor: marginTol / (2 * un.tol),
435
+ };
436
+ } else {
437
+ uncertainty = {
438
+ ...uncertainty!,
439
+ margin_tol: 20 * un.tol,
440
+ margin_bounded: false,
441
+ coverage: "k=2",
442
+ safety_factor: (20 * un.tol) / (2 * un.tol),
443
+ };
444
+ }
395
445
  }
396
446
  }
397
447
  }