Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_classifier_phrases.mjs

9.3 KiB, 1 run

created by r2519314175:295, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1// verify_classifier_phrases.mjs — the two Safari phrases through the real classifier, without a
2// browser.
3//
4// THE DEFECT (dev/PERSISTENCE_STUDY.md §3.1). A fetch that dies before its headers reaches
5// JavaScript as the BROWSER's own sentence. Chromium says `Failed to fetch`; WebKit says
6// `Load failed`, and iOS also produces `The network connection was lost`. `isUnreachable` and
7// `failureClass` in www/js/daimond.js used to hold Chromium's wording and not WebKit's, so on
8// iOS a turn that died BEFORE THE FIRST TOKEN fell through to `runTurn`'s terminal catch branch:
9// an error line was written and `J.clearTurn` deleted the turn from the write-ahead log. No badge,
10// no Continue, and `failureClass` counted it `other` so telemetry never showed it.
11//
12// verify_predrop.mjs proves the whole recovery path end to end in a real page. THIS file is its
13// unit companion: it exercises the two classifier FUNCTIONS in isolation, on the exact strings
14// Safari hands them, with nothing running. Read together they cover the seam from the browser's
15// word to the recovery branch.
16//
17// HOW IT STAYS HONEST. It does not paste the regexes; it LIFTS them from www/js/daimond.js at run
18// time — the two shared patterns `CLIENT_ROAD` and `BROWSER_ROAD`, and the bodies of
19// `isUnreachable` and `failureClass` — and runs the real code. If any of those four moves or is
20// renamed, the extraction throws and this file fails loudly rather than testing a stale copy.
21//
22// PROVED AGAINST BROKEN CODE FIRST. `--break wording` rebuilds the two functions from the 2026-08-27
23// patterns (the ones that held `Failed to fetch` and not `Load failed`), and runs the SAME checks.
24// The eight Safari-phrase checks then fail and the controls still pass — which is the proof that
25// those eight assertions actually bite on the wording, not on something incidental:
26//
27// node dev/verify_classifier_phrases.mjs --break wording # the eight phrase checks fail
28// node dev/verify_classifier_phrases.mjs # and then, clean
29//
30// THE COUNT IS ASSERTED. A patch here can DISPLACE an existing case rather than add one, so the
31// number of checks run is pinned: change the set and the last check trips until the number is
32// updated on purpose.
33//
34// node dev/verify_classifier_phrases.mjs
35//
36// Needs nothing running.
37import fs from 'node:fs';
38import path from 'node:path';
39import { fileURLToPath } from 'node:url';
40
41const HERE = path.dirname(fileURLToPath(import.meta.url));
42const SRC = fs.readFileSync(path.join(HERE, '..', 'www', 'js', 'daimond.js'), 'utf8');
43
44const BREAK = (() => {
45 const i = process.argv.indexOf('--break');
46 return i > 0 ? String(process.argv[i + 1] || '') : '';
47})();
48if (BREAK && BREAK !== 'wording') {
49 console.error(`unknown break '${BREAK}'; the only one is 'wording'`);
50 process.exit(2);
51}
52
53let bad = 0, ran = 0;
54const check = (pass, name, detail) => {
55 ran++;
56 if (!pass) bad++;
57 console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : ''));
58};
59
60// ── Lift the real classifier out of the source ───────────────
61
62/// The whole of a `var NAME = …;` statement that fits on one line — used for the two shared
63/// regex patterns, each a single-line literal with no `;` inside it.
64function grabVarLine(name) {
65 const m = SRC.match(new RegExp('^\\s*var ' + name + ' = .*$', 'm'));
66 if (!m) { console.error(`could not find 'var ${name}' in js/daimond.js`); process.exit(2); }
67 return m[0].trim();
68}
69
70/// A function declaration by signature, brace-matched from its opening `{`. These three bodies
71/// carry no braces inside a string, regex or comment, so a plain depth count is exact; a mis-lift
72/// is caught by the sentinel assertions below.
73function grabFn(sig) {
74 const start = SRC.indexOf(sig);
75 if (start < 0) { console.error(`could not find '${sig}' in js/daimond.js`); process.exit(2); }
76 const open = SRC.indexOf('{', start);
77 let depth = 0, i = open;
78 for (; i < SRC.length; i++) {
79 const c = SRC[i];
80 if (c === '{') depth++;
81 else if (c === '}') { depth--; if (depth === 0) { i++; break; } }
82 }
83 return SRC.slice(start, i);
84}
85
86const CLIENT_LIT = grabVarLine('CLIENT_ROAD');
87const BROWSER_LIT = grabVarLine('BROWSER_ROAD');
88const IU_SRC = grabFn('function isUnreachable(');
89const FC_SRC = grabFn('function failureClass(');
90
91// The 2026-08-27 patterns, before `Load failed` and `The network connection was lost` were in
92// either of them — the exact wording restored by verify_predrop.mjs's `--break wording`.
93const OLD_BROWSER = 'var BROWSER_ROAD = /Failed to fetch|network\\s*error|ERR_CONNECTION|ENOTFOUND|ECONNREFUSED|refused|dns/i;';
94const OLD_CLIENT = 'var CLIENT_ROAD = /could not reach|the stream broke|read stream chunk failed/i;';
95
96/// Build the two real functions with a chosen pair of patterns in scope. `isUnreachable` and
97/// `failureClass` both name `CLIENT_ROAD` and `BROWSER_ROAD` and nothing else from the closure,
98/// so this runs the shipped logic verbatim.
99function build(clientLit, browserLit) {
100 const f = new Function(
101 clientLit + '\n' + browserLit + '\n' + IU_SRC + '\n' + FC_SRC + '\n'
102 + 'return { isUnreachable: isUnreachable, failureClass: failureClass };');
103 return f();
104}
105
106const C = BREAK === 'wording'
107 ? build(OLD_CLIENT, OLD_BROWSER)
108 : build(CLIENT_LIT, BROWSER_LIT);
109
110if (BREAK) console.log(`\n*** RUNNING UNDER --break ${BREAK}: the phrase failures below are the point ***\n`);
111
112// ── The shipped regex really carries the phrases ─────────────
113//
114// Read off the FILE (never the break substitute): a guard that the two Safari sentences are in
115// the source that ships, so a green run cannot mean "matched by coincidence".
116console.log('the shipped BROWSER_ROAD literal');
117check(/Load failed/.test(BROWSER_LIT),
118 'the source BROWSER_ROAD contains "Load failed"');
119check(/The network connection was lost/.test(BROWSER_LIT),
120 'the source BROWSER_ROAD contains "The network connection was lost"');
121
122// ── The two functions cannot drift apart ─────────────────────
123//
124// The whole point of the shared patterns is that the recovery branch and the telemetry class read
125// the SAME evidence. If a later edit gives one of them its own regex, these two trip.
126console.log('the two readers share the two patterns');
127check(/CLIENT_ROAD\.test\(s\)\s*\|\|\s*BROWSER_ROAD\.test\(s\)/.test(IU_SRC),
128 'isUnreachable tests CLIENT_ROAD || BROWSER_ROAD and nothing else');
129check(/CLIENT_ROAD\.test\(s\)/.test(FC_SRC) && /BROWSER_ROAD\.test\(s\)/.test(FC_SRC),
130 "failureClass's first branch reads both shared patterns");
131
132// ── The Safari phrases land in the offline branch ────────────
133//
134// Bare, and wrapped the way the engine actually hands them up: `friendlyError`/`offline` strip the
135// fe2o3 source frames before testing, so the wrapped forms are what reaches the regex in the field.
136const SAFARI = [
137 ['Load failed', 'WebKit, bare'],
138 ['The network connection was lost', 'iOS, bare'],
139 ['LLM: fetch failed: TypeError: Load failed', 'WebKit, as the engine wraps it'],
140 ['TypeError: The network connection was lost', 'iOS, wrapped'],
141];
142console.log('the Safari phrases classify as offline');
143for (const [phrase, where] of SAFARI) {
144 check(C.isUnreachable(phrase) === true,
145 `isUnreachable("${phrase}") is true (${where})`,
146 C.isUnreachable(phrase) ? '' : 'FELL THROUGH — this is the deleted-turn path');
147 check(C.failureClass(phrase) === 'offline',
148 `failureClass("${phrase}") is 'offline' (${where})`,
149 C.failureClass(phrase) === 'offline' ? '' : `got '${C.failureClass(phrase)}'`);
150}
151
152// ── A genuine non-transport error must NOT read as offline ───
153//
154// The control that makes the above mean something: if everything read offline, the fix would be a
155// wire that shorts every turn into "recoverable". Each of these is a real provider or tool failure,
156// and each must keep its own class — 'offline' is specifically the road, not a catch-all.
157const CONTROLS = [
158 ['HTTP 400 Bad Request', 'other', 'a malformed request'],
159 ['HTTP 401 Unauthorized', 'refused', 'a bad key'],
160 ['HTTP 429 Too Many Requests', 'rate_limited', 'a rate limit'],
161 ['HTTP 500 Internal Server Error', 'server_error', 'a provider fault'],
162 ['file_edit: old_string not found', 'other', 'a tool refusal'],
163];
164console.log('a real error keeps its own class, not offline');
165for (const [phrase, klass, where] of CONTROLS) {
166 check(C.isUnreachable(phrase) === false,
167 `isUnreachable("${phrase}") is false (${where})`,
168 C.isUnreachable(phrase) ? 'MISREAD AS THE ROAD' : '');
169 check(C.failureClass(phrase) !== 'offline' && C.failureClass(phrase) === klass,
170 `failureClass("${phrase}") is '${klass}', not 'offline' (${where})`,
171 `got '${C.failureClass(phrase)}'`);
172}
173
174// ── The count is pinned ──────────────────────────────────────
175//
176// EXPECTED is the number of checks above. A case added or displaced changes it, so a silent swap
177// cannot pass. Update this deliberately when the set changes.
178const EXPECTED = 22;
179const ranBefore = ran;
180check(ranBefore === EXPECTED,
181 `exactly ${EXPECTED} checks ran — a displaced case trips this`,
182 `ran ${ranBefore}`);
183
184console.log(bad ? `\n${bad} check(s) FAILED` : '\nall checks passed');
185process.exit(bad ? 1 : 0);