quill/oracle Public Public: anyone can read this; nobody can change it.

workbenchthis machine
Files main
130 lines · 5.3 KB · ~1.6k tok Raw History
1
/**
2
 * The oracle's mind. A question in, a verdict out, and nothing random about
3
 * it: the same question always receives the same answer, because an oracle
4
 * that equivocates is a weather vane. The verdict is derived from a hash of
5
 * the question's normalized form, so conviction is reproducible and the
6
 * ledger can be scored honestly.
7
 */
8
9
export interface Verdict {
10
  /** What the oracle says. */
11
  stance: string;
12
  /** How sure it claims to be, 52–98. It never admits to a coin flip. */
13
  confidence: number;
14
  /** The sign it read on the way to the answer. */
15
  auspice: string;
16
}
17
18
/**
19
 * Questions that differ only in shouting, spacing or punctuation are one
20
 * question — otherwise "will it rain?" and "Will it rain" would get two
21
 * verdicts and the oracle would look like it hedges.
22
 */
23
export function normalize(question: string): string {
24
  return question
25
    .toLowerCase()
26
    .replace(/\s+/g, " ")
27
    .trim()
28
    .replace(/[?!.,;:…]+$/u, "")
29
    .trim();
30
}
31
32
const STANCES: readonly string[] = [
33
  "Yes. Stop hunting for a reason not to.",
34
  "No — and the part of you that already knew is the part worth listening to.",
35
  "Yes, but later than you want and smaller than you pictured.",
36
  "Not yet. The piece you are missing arrives on its own.",
37
  "Yes, if you say it out loud to one person before the week turns.",
38
  "No. You are asking about the wrong half of the problem.",
39
  "It happens twice. The second time is the one that counts.",
40
  "Yes, at a price you will call fair afterwards and unfair throughout.",
41
  "No, and it is a mercy wearing a loss for a coat.",
42
  "Yes — you began already, which is why you are asking.",
43
  "The answer changes if you wait a week. Wait the week.",
44
  "No. Do the smaller version: it is the same wish, honestly sized.",
45
  "Yes, and it will be boring, which is how you will know it worked.",
46
  "Ask again once you have slept. Tonight's answer belongs to fear.",
47
  "Yes. The obstacle you keep naming is a door with a dull handle.",
48
  "No — but the person you would become by trying is worth the no.",
49
];
50
51
const AUSPICES: readonly string[] = [
52
  "a crow lands on the north gate and does not look back",
53
  "steam off a cup, drifting left",
54
  "three lights out on the same street",
55
  "the tide goes further out than the almanac promised",
56
  "a dog barks once, then thinks better of it",
57
  "wet ink on a folded page",
58
  "the last seat on a full train",
59
  "a moth at the window, patient",
60
  "salt spilled and swept in the same minute",
61
  "two clocks in the house disagree by four minutes",
62
  "a key that turns the wrong way first",
63
  "bread risen past the rim of the tin",
64
  "a letter delivered to the flat below",
65
  "frost on one side of the roof only",
66
  "the radio finds a station between stations",
67
  "a bicycle left unlocked, still there at dusk",
68
];
69
70
/** The verdict a question has always had and will always have. */
71
export function verdictFor(question: string): Verdict {
72
  const digest = new Bun.CryptoHasher("sha256")
73
    .update(normalize(question))
74
    .digest();
75
  const byte = (i: number): number => digest[i] ?? 0;
76
  return {
77
    stance: STANCES[byte(0) % STANCES.length] as string,
78
    // 52–98: enough spread to be scoreable, never a shrug.
79
    confidence: 52 + (byte(1) % 47),
80
    auspice: AUSPICES[byte(2) % AUSPICES.length] as string,
81
  };
82
}
83
84
export type Outcome = "unjudged" | "right" | "wrong";
85
86
export function isOutcome(value: string): value is Outcome {
87
  return value === "unjudged" || value === "right" || value === "wrong";
88
}
89
90
export interface Calibration {
91
  /** Verdicts a person has since scored. */
92
  judged: number;
93
  right: number;
94
  /** Share it actually got right, as a percentage, or undefined at zero. */
95
  accuracy: number | undefined;
96
  /** Mean confidence it claimed on those same verdicts. */
97
  claimed: number | undefined;
98
  /** claimed − accuracy: positive is bluster, negative is modesty. */
99
  gap: number | undefined;
100
}
101
102
/**
103
 * Scoring is only over verdicts a person judged — an unjudged verdict is not
104
 * evidence of anything, and counting it either way would flatter or libel the
105
 * oracle for free.
106
 */
107
export function calibrate(
108
  rows: readonly { confidence: number; outcome: string }[],
109
): Calibration {
110
  const judged = rows.filter((r) => r.outcome === "right" || r.outcome === "wrong");
111
  const right = judged.filter((r) => r.outcome === "right").length;
112
  if (judged.length === 0)
113
    return { judged: 0, right: 0, accuracy: undefined, claimed: undefined, gap: undefined };
114
  const accuracy = Math.round((right / judged.length) * 100);
115
  const claimed = Math.round(
116
    judged.reduce((sum, r) => sum + r.confidence, 0) / judged.length,
117
  );
118
  return { judged: judged.length, right, accuracy, claimed, gap: claimed - accuracy };
119
}
120
121
/** How the oracle is doing, in a sentence, without a chart. */
122
export function verdictOnItself(c: Calibration): string {
123
  if (c.accuracy === undefined || c.claimed === undefined || c.gap === undefined)
124
    return "Nothing judged yet. Score a verdict and the oracle starts keeping itself honest.";
125
  if (c.judged < 5)
126
    return `Right ${c.right} of ${c.judged}. Too few to mean much — keep scoring.`;
127
  if (c.gap > 15) return `Right ${c.accuracy}% while claiming ${c.claimed}%. It talks a better game than it plays.`;
128
  if (c.gap < -15) return `Right ${c.accuracy}% while claiming only ${c.claimed}%. It undersells itself.`;
129
  return `Right ${c.accuracy}%, claimed ${c.claimed}%. Close to honest.`;
130
}
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts