File size: 15,220 Bytes
f2878d0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4d03824
 
f2878d0
 
 
 
 
 
 
 
 
 
 
 
 
4d03824
 
f2878d0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4d03824
f2878d0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
// Builds the conformance fixture every port must pass, from the reference engine (tinydecide.js,
// which matches PyTorch to 3e-6 on ref.json).
//   node make.mjs [S768_dir] [full_dir]
// Writes cases.json (inputs, shared by both builds) and expected.<build>.json (reference outputs).
import { readFileSync, writeFileSync, existsSync } from "node:fs";
import { createHash } from "node:crypto";

const here = new URL(".", import.meta.url).pathname;
// Two layouts: this repo (sdk/js next to sdk/conformance) and the Hugging Face repo (engine at the root).
const HF = !existsSync(here + "../js/tinydecide.js");
const JS = here + (HF ? "../" : "../js/");
const { TinyDecide, WordPiece } = await import(JS + "tinydecide.js");
const { makeProtos } = await import(JS + "corrections.js");
const BUILDS = { S768: process.argv[2] || here + (HF ? ".." : "../../web_S768"), full: process.argv[3] || here + (HF ? "../full" : "../../web_fc") };

// ------------------------------------------------------------------ tokenizer strings
const TOK = [
  "", " ", "Hello, World!", "  leading and trailing  ", "tab\there\nnewline\r\nCRLF",
  "Café crème brûlée, naïve résumé, jalapeño, Ångström", "é à ö combining marks",
  "10 km by the thin space ideographic", "line
separator
paragraph",
  "don’t “quote” ‘single’ «guillemets» — en–dash … ellipsis", "zero​widthbom­soft‍zwj",
  "bell\u0007 escape\u001b null\u0000 del\u007f c1\u0085", "replacement � char",
  "我爱北京 Tokyo 東京タワー ひらがな 한국어", "pizza 🍕 tonight 🎉🎉 👍🏽 family 👨‍👩‍👧",
  "İstanbul ŞEHİR ı i̇ I", "ΣΊΣΥΦΟΣ ελληνικά Ωμέγα", "Straße STRASSE ß ẞ", "file flow ligatures",
  "ABC fullwidth 123 !?", "Привет, мир! Ёлка Йод Їжак ґанок", "مرحبا بالعالم", "שלום עולם",
  "नमस्ते दुनिया", "สวัสดีชาวโลก", "Ελλάδα: Ά Έ Ή Ί Ό Ύ Ώ ΐ ΰ", "DŽ Dž dž LJ titlecase", "ŁÓDŹ Gdańsk Żółć",
  "Øresund Æble Œuvre Ðorđe Þór", "x".repeat(101), "a".repeat(100), "abc" + "é".repeat(99),
  "unbelievably-hyphenated_words/with\\slashes", "3.14 $1,000.50 €20 £5 ¥300 50% #tag @user",
  "email a.b+c@ex-ample.co.uk url https://x.io/a?b=c&d=e#f", "Hey!!! What?!?! ...", "´`^¨~ spacing accents",
  "math 𝐀𝐁𝐂 𝑥𝑦 ℝ ℕ ∑ ∫ √ ≤ ≥ ≠", "© ® ™ § ¶ † ‡ • ° ± × ÷", "private  use", "ꞯ ɐ ʃ IPA ŋ",
  "Ⅻ Ⅳ roman numerals ①②③", "Džungla Lj Nj", "tschüß MÜNCHEN Öl Äpfel", "ÀÁÂÃÄÅàáâãäå ÈÉÊËèéêë",
  "Ċ ċ Ġ ġ Ħ ħ IJ ij Ŀ ŀ ʼn Ŋ ŋ ſ", "Ǎ ǎ Ǐ ǐ Ǒ ǒ Ǔ ǔ Ǖ ǖ", "Ạ ạ Ả ả Ấ ấ Việt Nam tiếng Việt",
];

// Seeded fuzz over long-assigned blocks (no lone surrogates, nothing new in Unicode 16/17).
let seed = 1234567;
const rnd = () => ((seed = (seed * 1103515245 + 12345) >>> 0) / 4294967296);
const POOLS = [[0x20, 0x7e], [0xa0, 0x17f], [0x180, 0x24f], [0x250, 0x2ff], [0x300, 0x36f], [0x370, 0x3ff],
  [0x400, 0x4ff], [0x530, 0x58f], [0x5d0, 0x5ea], [0x600, 0x6ff], [0x900, 0x97f], [0xe00, 0xe5b], [0x1e00, 0x1fff],
  [0x2000, 0x206f], [0x2100, 0x214f], [0x2190, 0x23ff], [0x2460, 0x24ff], [0x3000, 0x30ff], [0x4e00, 0x4fff],
  [0xac00, 0xad00], [0xfb00, 0xfb06], [0xff01, 0xff5e], [0x1d400, 0x1d4ff], [0x1f300, 0x1f64f], [0x1f900, 0x1f9ff]];
const RE_CN = /\p{Cn}/u;
for (let i = 0; i < 240; i++) {
  let s = "";
  const n = 3 + Math.floor(rnd() * 30);
  while ([...s].length < n) {
    const [a, b] = rnd() < 0.35 ? POOLS[0] : POOLS[Math.floor(rnd() * POOLS.length)];
    const cp = a + Math.floor(rnd() * (b - a + 1));
    const ch = String.fromCodePoint(cp);
    if (!RE_CN.test(ch)) s += ch;
    if (rnd() < 0.15) s += " ";
  }
  TOK.push(s);
}

// ------------------------------------------------------------------ requests
const LONG = "Dear team, I wanted to give you a full update on the warehouse move before the weekend. " +
  "The trucks are booked for Saturday morning at 6:30 and the first load will leave the old site around 7:15. " +
  "We still need four more volunteers for the afternoon shift, so please reply if you can help. " +
  "Parking at the new building is behind the main entrance, and the code for the side gate is 4471. " +
  "Lunch will be provided by the Italian place on Market Street, and there will be vegetarian options. " +
  "If anything breaks during the move, write it down on the damage sheet next to the loading bay and tell Maria. " +
  "The internet at the new site will not be live until Tuesday, so download anything you need beforehand. " +
  "Thanks again for all the hard work this month, it has been a long one and I really appreciate it.";
const APPS = ["reminders", "music", "calendar", "restaurants", "weather"];
const README_QS = [
  { type: "choice", text: "Which app should handle this?", options: APPS },
  { type: "noul", text: "The message is urgent." },
  { type: "score", text: "How positive is the tone?", options: ["negative", "neutral", "positive"] },
  { type: "span", text: "Extract the time." },
];
const NOTE_Q = { type: "choice", text: "What kind of note is this?", options: ["something to buy", "a task to do", "an event with a date or time", "a phone number or email"] };
const SCORE_Q = { type: "score", text: "How urgent is this?", options: ["not urgent", "somewhat urgent", "very urgent"] };
const NOUL_Q = { type: "noul", text: "The message is about food." };

const REQ = [
  { name: "readme", state: "Book a table for 4 at an Italian place near the station on Friday at 7:30", questions: README_QS },
  { name: "multi7", state: "Hey, can you remind me to call Dr. Patel at 555-0142 tomorrow at 9am? It's about my test results.",
    questions: [
      { type: "choice", text: "Which app should handle this?", options: ["reminders", "music", "calendar", "phone", "weather"] },
      { type: "noul", text: "The message is urgent." },
      { type: "noul", text: "The message mentions a phone number." },
      { type: "score", text: "How worried does the writer sound?", options: ["calm", "a little worried", "very worried"] },
      { type: "span", text: "Extract the phone number." },
      { type: "span", text: "Extract the time." },
      { type: "choice", text: "What is the topic?", options: ["health", "money", "travel", "work", "family", "sport", "food", "weather"] },
    ] },
  { name: "long_truncated", state: LONG, questions: [
      { type: "choice", text: "What is this message about?", options: ["a move", "a party", "a complaint", "a job offer"] },
      { type: "span", text: "Extract the gate code." },
      { type: "span", text: "Extract when the trucks leave." },
      { type: "noul", text: "The writer is thankful." } ] },
  { name: "empty_state", state: "", questions: [{ type: "noul", text: "The message is empty." }, { type: "span", text: "Extract the name." },
      { type: "choice", text: "Pick one.", options: ["yes", "no"] }] },
  { name: "unicode_state", state: "Réservez une table pour 2 au Café Müller — demain à 19h30, s’il vous plaît! 🍷 Tél: +33 6 12 34 56 78",
    questions: [ { type: "choice", text: "Which language is this?", options: ["French", "German", "English", "Spanish"] },
      { type: "span", text: "Extract the phone number." }, { type: "span", text: "Extract the restaurant name." },
      { type: "noul", text: "The writer is polite." } ] },
  { name: "unicode_question", state: "Привет! Купи молоко и хлеб, пожалуйста. Встреча в 18:00 у метро «Киевская».",
    questions: [ { type: "choice", text: "Qu’est-ce que c’est — “shopping” or “meeting”?", options: ["achats 🛒", "réunion", "naïve idée"] },
      { type: "span", text: "Extract the time." }, { type: "span", text: "Extract the place." } ] },
  { name: "cjk_emoji", state: "明天下午3点在東京駅见面 🚄 don't be late!!", questions: [
      { type: "noul", text: "This is about meeting someone." }, { type: "span", text: "Extract the time." },
      { type: "choice", text: "Which script is mostly used?", options: ["Chinese characters", "Latin letters", "Cyrillic"] } ] },
  { name: "room", state: "The meeting moved to room 4B.", questions: [
      { type: "noul", text: "The message gives a room." }, { type: "span", text: "Extract the room." } ] },
  { name: "choice32", state: "I'd like a large pepperoni pizza delivered to 12 Oak Lane please.", questions: [
      { type: "choice", text: "What does the person want?", options: Array.from({ length: 32 }, (_, i) => ["pizza", "sushi", "tacos", "a taxi", "a doctor", "a plumber", "flowers", "a haircut"][i % 8] + " " + (i + 1)) } ] },
  { name: "score5", state: "This is the worst service I've ever had. Never again.", questions: [
      { type: "score", text: "How satisfied is the customer?", options: ["very unhappy", "unhappy", "neutral", "happy", "very happy"] },
      { type: "noul", text: "The customer will come back." } ] },
  { name: "span_numbers", state: "Order #A-77812 shipped on 2026-10-03, total $149.99, tracking 1Z999AA10123456784.", questions: [
      { type: "span", text: "Extract the order number." }, { type: "span", text: "Extract the total." },
      { type: "span", text: "Extract the date." }, { type: "span", text: "Extract the tracking number." }, { type: "span", text: "Extract the email address." } ] },
  { name: "whitespace", state: "  \t buy milk \n\n and
eggs  ", questions: [NOTE_Q, { type: "span", text: "Extract the items." }] },
  { name: "too_long_question", state: "hello", questions: [{ type: "choice", text: "Which one? " + "very ".repeat(200), options: ["a", "b"] }] },
  { name: "one_option", state: "hello there", questions: [{ type: "choice", text: "Pick one.", options: ["only"] }] },
  { name: "score_no_levels", state: "hello there", questions: [{ type: "noul", text: "It is a greeting." }, { type: "score", text: "How formal is it?", options: [] }] },
  { name: "choice33", state: "hello there", questions: [{ type: "choice", text: "Which number?", options: Array.from({ length: 33 }, (_, i) => String(i + 1)) }] },
  { name: "noul_with_options", state: "The meeting moved to room 4B.", questions: [{ type: "noul", text: "The message gives a room.", options: ["no", "yes"] }] },
  { name: "span_with_options", state: "The meeting moved to room 4B.", questions: [{ type: "span", text: "Extract the room.", options: ["4B", "5C"] }] },
];

// protos cases: examples come from the model's own answers on training notes (built per build below)
const PROTO_TRAIN = {
  note: [["buy oat milk", "get coffee beans and bread"], ["call the plumber", "fix the bike"], ["dentist thursday 4pm"], []],
  score: [["whenever you have time", "no rush"], ["by end of week please"], ["ASAP the server is down", "urgent: fire alarm"]],
  noul: [["the car needs new tyres", "meeting at noon"], ["pizza for dinner", "grab some sushi", "bake a cake"]],
};
const PROTO_ASK = [
  { name: "protos_choice", state: "pick up eggs and butter", q: NOTE_Q, kind: "note" },
  { name: "protos_choice_event", state: "lunch with Sam on friday 1pm", q: NOTE_Q, kind: "note" },
  { name: "protos_score", state: "please reply today, the client is waiting", q: SCORE_Q, kind: "score" },
  { name: "protos_noul", state: "make pasta tonight", q: NOUL_Q, kind: "noul" },
  { name: "protos_one_example", state: "buy stamps", q: NOTE_Q, kind: "note", only: 1 },
  { name: "protos_no_center", state: "pick up eggs and butter", q: NOTE_Q, kind: "note", noCenter: true },
  { name: "protos_noul_no_center", state: "make pasta tonight", q: NOUL_Q, kind: "noul", noCenter: true },
];

// ------------------------------------------------------------------ run
const r6 = (x) => Math.round(x * 1e6) / 1e6;
const arr = (a) => Array.from(a, r6);
function outAnswer(a) {
  if (a.type === "choice" || a.type === "score") {
    const o = { type: a.type, probs: arr(a.probs), pick: a.pick, confidence: r6(a.confidence), z0: arr(a.z0), qvec: arr(a.qvec) };
    if (a.type === "score") o.score = r6(a.score);
    return o;
  }
  if (a.type === "noul") return { type: "noul", p: r6(a.p), z0: arr(a.z0), qvec: arr(a.qvec) };
  return { type: "span", p_present: r6(a.p_present), p_span: r6(a.p_span), tok: a.tok, text: a.text };
}
const ser = (p) => p && { vec: arr(p.vec), cnt: Array.from(p.cnt), center: p.center ? arr(p.center) : null, lam: p.lam };

const cases = { version: 1, engine_sha256: sha(readFileSync(JS + "tinydecide.js")), tokenizer: TOK, requests: [], protos: [] };
function sha(b) { return createHash("sha256").update(b).digest("hex"); }

let first = true;
for (const [build, dir] of Object.entries(BUILDS)) {
  const meta = JSON.parse(readFileSync(dir + "/meta.json", "utf8"));
  const bin = readFileSync(dir + "/model.bin");
  const m = new TinyDecide(meta, bin.buffer.slice(bin.byteOffset, bin.byteOffset + bin.byteLength));
  const tok = new WordPiece(meta.tokenizer);
  const exp = { build, model_sha256: sha(bin), meta_sha256: sha(readFileSync(dir + "/meta.json")), tokenizer: [], requests: [], protos: [] };

  for (const s of TOK) exp.tokenizer.push(tok.encode(s).ids);

  // ref.json inputs are the same for both builds (the Hugging Face repo keeps one, at the root)
  const ref = JSON.parse(readFileSync(BUILDS.S768 + "/ref.json", "utf8"));
  const all = [...REQ, ...ref.map((r, i) => ({ name: "ref" + i, state: r.state, questions: r.questions }))];
  for (const r of all) {
    if (first) cases.requests.push({ name: r.name, state: r.state, questions: r.questions });
    try {
      const got = m.answer({ state: r.state, questions: r.questions });
      exp.requests.push({ name: r.name, ids: got.ids, truncated: got.truncated, tokens: got.tokens, answers: got.answers.map(outAnswer) });
    } catch (e) {
      exp.requests.push({ name: r.name, error: e.message });
    }
  }

  // corrections: examples -> protos (makeProtos) -> answers with protos
  for (const c of PROTO_ASK) {
    const lists = PROTO_TRAIN[c.kind].map((l, k) => (c.only !== undefined ? (k === 0 ? l.slice(0, c.only) : []) : l).map((st) => {
      const a = m.answer({ state: st, questions: [c.q] }).answers[0];
      return { v: arr(a.qvec), z: arr(a.z0) };
    }));
    let p = makeProtos(c.q.type, lists, meta.beta);
    if (c.noCenter) p = { vec: p.vec, cnt: p.cnt, center: null, lam: p.lam };
    if (first) cases.protos.push({ name: c.name, state: c.state, question: c.q, no_center: !!c.noCenter, examples_from: PROTO_TRAIN[c.kind], only: c.only ?? null });
    const got = m.answer({ state: c.state, questions: [c.q] }, [p]);
    exp.protos.push({ name: c.name, examples: lists, protos: ser(p), ids: got.ids, answers: got.answers.map(outAnswer) });
  }
  writeFileSync(here + `expected.${build}.json`, JSON.stringify(exp));
  console.log(`${build}: ${exp.tokenizer.length} tokenizer strings, ${exp.requests.length} requests ` +
    `(${exp.requests.filter((r) => r.error).length} errors, ${exp.requests.filter((r) => r.truncated).length} truncated), ${exp.protos.length} protos cases`);
  first = false;
}
writeFileSync(here + "cases.json", JSON.stringify(cases, null, 1));