abullard1 commited on
Commit
0db4d7f
·
verified ·
1 Parent(s): 4ca2363

Deploy calibration @ d667e6d-space

Browse files
Files changed (5) hide show
  1. README.md +5 -4
  2. app.js +150 -65
  3. config.js +1 -1
  4. index.html +6 -6
  5. logic.mjs +21 -0
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: LocalGate Audit — Calibration
3
  emoji: ⚖️
4
  colorFrom: gray
5
  colorTo: gray
@@ -11,10 +11,11 @@ hf_oauth: true
11
  hf_oauth_expiration_minutes: 1440
12
  hf_oauth_scopes:
13
  - read-repos
14
- # write-repos, not contribute-repos — see audit-verdicts/README.md: each
15
- # Space is its own OAuth app, so app-scoped contribute-repos cannot write
16
- # the shared results repo created under another Space's app.
17
  - write-repos
 
18
  hf_oauth_authorized_org: localgate
19
  ---
20
 
 
1
  ---
2
+ title: LocalGate Audit — Judge Calibration
3
  emoji: ⚖️
4
  colorFrom: gray
5
  colorTo: gray
 
11
  hf_oauth_expiration_minutes: 1440
12
  hf_oauth_scopes:
13
  - read-repos
14
+ # Union is load-bearing — see audit-verdicts/README.md: write-repos commits
15
+ # to the shared repo regardless of creator; contribute-repos creates it on
16
+ # a first login. Each Space is its own OAuth app.
17
  - write-repos
18
+ - contribute-repos
19
  hf_oauth_authorized_org: localgate
20
  ---
21
 
app.js CHANGED
@@ -24,7 +24,8 @@ import { oauthLoginUrl, oauthHandleRedirectIfPresent, uploadFiles, downloadFile,
24
  // the ?v= stamp — an unversioned nested import could load a stale module.
25
  import { keyOf, latestByKey, mergeEvents, orderItems, firstUngraded as
26
  firstUngradedPure, countComplete, stampClass, nextEpoch,
27
- standsDownTo } from "./logic.mjs?v=d4fa750-space";
 
28
 
29
  // Test seam: Playwright installs window.__testHub (an in-memory hub) before
30
  // any page script runs; production never defines it, so this is one inert
@@ -37,6 +38,9 @@ const hub = () => window.__testHub ?? {
37
 
38
  /* global CONFIG */
39
  const ORG_ID = "6a7af86a89612db0d39b0b14"; // localgate — forces the org grant
 
 
 
40
  const ITEMS_REPO = { type: "dataset", name: "localgate/audit-items" };
41
  const SAVE_EVERY = 3; // phones kill background fetches; keep the window small
42
 
@@ -62,25 +66,33 @@ function storedAuth() {
62
  if (!raw) return null;
63
  const auth = JSON.parse(raw);
64
  if (new Date(auth.accessTokenExpiresAt) <= new Date()) return null;
 
 
 
65
  return auth;
66
  } catch { return null; }
67
  }
68
 
69
  async function ensureAuth() {
70
- let auth = storedAuth();
71
- if (auth) return auth;
72
- const fresh = await hub().oauthHandleRedirectIfPresent();
73
- if (fresh) {
74
- sessionStorage.setItem("oauth", JSON.stringify(fresh));
75
- history.replaceState(null, "", location.pathname); // ?code is single-use; keep it out of history
76
- return fresh;
 
 
77
  }
78
- return null;
79
  }
80
 
81
  async function signIn() {
82
- const url = await hub().oauthLoginUrl(); // reads window.huggingface.variables in a Space
83
- window.location.href = url + "&orgIds=" + ORG_ID; // no param passthrough in the helper; append
 
 
 
84
  }
85
 
86
  // ── state ────────────────────────────────────────────────────────────────────
@@ -91,7 +103,7 @@ const state = {
91
  shownAt: 0, rubricVersion: "", resultsRepo: null, last: null,
92
  breakShownAt: Date.now(), stickyBanner: null, stickyActions: [], stickyKind: null,
93
  gradedSinceBreak: 0, infoNavsLeft: 0,
94
- hbTimer: null, pushing: null, pushQueued: false,
95
  };
96
 
97
  const logKey = () => `audit:${CONFIG.task}:${state.user}`;
@@ -113,22 +125,72 @@ function firstUngraded() {
113
  (ev) => CONFIG.isComplete(ev));
114
  }
115
 
116
- // ── results repo (own namespace; contribute-repos scope) ─────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  async function ensureResultsRepo() {
118
  const name = `${state.user}/localgate-audit-results`;
119
  state.resultsRepo = { type: "dataset", name };
120
- try {
121
- await hub().createRepo({ repo: state.resultsRepo, accessToken: state.auth.accessToken,
122
- private: true });
123
- } catch (err) {
124
- // Already exists (any prior session) is the expected steady state; the hub's
125
- // message says "You already created this dataset repo" with no status code.
126
- if (!/409|exist|already/i.test(String(err))) throw err;
 
 
 
 
 
 
 
 
 
 
 
 
127
  }
 
128
  }
129
 
130
  const remotePath = () => `${CONFIG.task}/${state.user}.jsonl`;
131
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  async function pullRemote() {
133
  const blob = await hub().downloadFile({ repo: state.resultsRepo, path: remotePath(),
134
  accessToken: state.auth.accessToken });
@@ -166,12 +228,14 @@ async function pushOnce() {
166
  parentCommit: parent,
167
  });
168
  } catch (err) {
169
- if (attempt < 3 && /412|precondition/i.test(String(err))) continue;
 
170
  throw err; // someone kept committing, or a real failure
171
  }
172
  // Events recorded while the upload was in flight are still unsaved —
173
  // assigning 0 here would make the pagehide flush skip them.
174
  state.unsaved = state.events.length - snapshot;
 
175
  clearErrorBanner(); // never eats an info banner
176
  renderStatus();
177
  return;
@@ -208,23 +272,23 @@ async function pushGuarded() {
208
  catch (err) {
209
  if (document.visibilityState === "hidden") return; // browser killed the
210
  // fetch on backgrounding — expected on phones; retried on return/boot
 
211
  if (!storedAuth()) {
212
- // The token has a hard 8h expiry and no refresh — a retry can never
213
- // succeed. Re-login reloads the page; localStorage keeps everything.
214
- banner("Your login expired — all your work is saved in this browser. Sign "
215
- + "in again to publish it; you will continue where you left off.", true,
216
- [{ label: "Sign in again", onClick: signIn },
 
217
  { label: "Download your log", onClick: downloadLog }]);
218
  return;
219
  }
220
- if (/403|forbidden/i.test(String(err))) {
221
- // A cached token can carry outdated permissions (e.g. after a scope
222
- // change). A fresh sign-in reissues it; localStorage keeps every event.
223
  banner("Your work is saved in this browser, but publishing was refused "
224
  + "(permissions). Sign in again to refresh your access — you will "
225
  + "continue exactly where you left off.", true,
226
- [{ label: "Sign in again", onClick: () => {
227
- sessionStorage.removeItem("oauth"); signIn(); } },
228
  { label: "Download your log", onClick: downloadLog }]);
229
  return;
230
  }
@@ -303,7 +367,7 @@ function record(item, values, unsure, note) {
303
  cls: stampClass(values) };
304
  state.unsaved += 1;
305
  if (!item.warmup) state.gradedSinceBreak += 1;
306
- if (state.unsaved >= SAVE_EVERY) pushGuarded();
307
  }
308
 
309
  // ── UI ───────────────────────────────────────────────────────────────────────
@@ -541,7 +605,8 @@ function move(delta) {
541
  renderItem();
542
  if (discarded) {
543
  banner("Heads up: the note on the item you just left was not recorded — "
544
- + "notes only save once both of its questions are answered.");
 
545
  }
546
  }
547
 
@@ -683,32 +748,52 @@ async function boot() {
683
  return;
684
  }
685
 
 
686
  try {
687
- await withTimeout(ensureResultsRepo(), 60, "preparing your results dataset");
688
- } catch (err) {
689
- $("#main").replaceChildren(el("p", null,
690
- `Could not prepare your results dataset: ${String(err).slice(0, 140)}`));
691
- const retry = el("button", "signin", "Retry");
692
- retry.addEventListener("click", () => location.reload());
693
- $("#main").append(retry);
694
- return;
 
 
 
 
 
 
 
 
 
 
 
 
695
  }
696
 
697
  // Ordering gate: a task can require another task's completion first (the
698
  // conversion fidelity pass must not open before the blind verdict pass,
699
  // because it reveals which items the filter kept).
700
  if (CONFIG.requires) {
701
- let done = 0;
702
- try {
703
- const blob = await hub().downloadFile({
704
- repo: state.resultsRepo, accessToken: state.auth.accessToken,
705
- path: `${CONFIG.requires.task}/${state.user}.jsonl`,
706
- });
707
- if (blob !== null) {
708
- const rows = (await blob.text()).split("\n").filter(Boolean).map((l) => JSON.parse(l));
709
- done = countComplete(rows, CONFIG.requires.fields);
710
- }
711
- } catch { /* unreadable counts as not done — the gate stays shut */ }
 
 
 
 
 
 
 
712
  if (done < CONFIG.requires.count) {
713
  $("#main").replaceChildren(el("p", null,
714
  `${CONFIG.requires.label} must be finished first — you have graded ` +
@@ -729,26 +814,26 @@ async function boot() {
729
  // wrongly locking is a reload; wrongly unlocking is an invisible validity
730
  // hole (post-unblinding edits are only detectable, not preventable).
731
  if (CONFIG.lockWhen) {
732
- let started = false, unreadable = false;
733
- try {
734
- const blob = await hub().downloadFile({
735
- repo: state.resultsRepo, accessToken: state.auth.accessToken,
736
- path: `${CONFIG.lockWhen.task}/${state.user}.jsonl`,
737
- });
738
- if (blob !== null) {
739
- const rows = (await blob.text()).split("\n").filter(Boolean).map((l) => JSON.parse(l));
740
- started = rows.some((ev) => !ev.warmup);
741
- }
742
- } catch { unreadable = true; }
743
  if (started || unreadable) {
744
  $("#main").replaceChildren(el("p", null, unreadable
745
  ? "Could not verify whether the next pass has already started, so this "
746
- + "one stays closed to be safe. Reload to try again."
 
747
  : `${CONFIG.lockWhen.label} has begun, so this pass is closed for `
748
  + "revision — its blindness window has ended. Every verdict you "
749
  + "recorded here is safe and counted. If a correction is genuinely "
750
  + "needed, tell Samuel; corrections after unblinding are flagged in "
751
  + "the analysis rather than silently applied."));
 
 
 
 
 
 
 
752
  $("#controls").hidden = true;
753
  return;
754
  }
@@ -777,7 +862,7 @@ async function boot() {
777
  // Transient on purpose: mobile browsers reload the tab on every app
778
  // switch, so a sticky notice re-arms forever and reads as stuck.
779
  banner(`Welcome back — ${done} already recorded. `
780
- + "Continuing exactly where you left off.");
781
  }
782
  }
783
 
 
24
  // the ?v= stamp — an unversioned nested import could load a stale module.
25
  import { keyOf, latestByKey, mergeEvents, orderItems, firstUngraded as
26
  firstUngradedPure, countComplete, stampClass, nextEpoch,
27
+ standsDownTo, statusOf, classifyProbe, hasScopes }
28
+ from "./logic.mjs?v=d667e6d-space";
29
 
30
  // Test seam: Playwright installs window.__testHub (an in-memory hub) before
31
  // any page script runs; production never defines it, so this is one inert
 
38
 
39
  /* global CONFIG */
40
  const ORG_ID = "6a7af86a89612db0d39b0b14"; // localgate — forces the org grant
41
+ // write-repos: commit to the shared repo whichever app created it;
42
+ // contribute-repos: create it on a brand-new annotator's first login.
43
+ const REQUIRED_SCOPES = ["read-repos", "write-repos", "contribute-repos"];
44
  const ITEMS_REPO = { type: "dataset", name: "localgate/audit-items" };
45
  const SAVE_EVERY = 3; // phones kill background fetches; keep the window small
46
 
 
66
  if (!raw) return null;
67
  const auth = JSON.parse(raw);
68
  if (new Date(auth.accessTokenExpiresAt) <= new Date()) return null;
69
+ // A token minted before a scope change still "works" for reads but fails
70
+ // at publish — treat it as absent so the user re-consents cleanly.
71
+ if (!hasScopes(auth.scope, REQUIRED_SCOPES)) return null;
72
  return auth;
73
  } catch { return null; }
74
  }
75
 
76
  async function ensureAuth() {
77
+ // A fresh authorization redirect ALWAYS outranks the cache: the user just
78
+ // consented, possibly to new scopes — ignoring it kept stale tokens alive.
79
+ if (new URLSearchParams(location.search).has("code")) {
80
+ const fresh = await hub().oauthHandleRedirectIfPresent();
81
+ if (fresh) {
82
+ sessionStorage.setItem("oauth", JSON.stringify(fresh));
83
+ history.replaceState(null, "", location.pathname); // ?code is single-use
84
+ return fresh;
85
+ }
86
  }
87
+ return storedAuth();
88
  }
89
 
90
  async function signIn() {
91
+ sessionStorage.removeItem("oauth"); // never carry a stale grant
92
+ const url = await hub().oauthLoginUrl(); // reads window.huggingface.variables in a Space
93
+ // prompt=consent re-shows the consent screen (the HF client-side-oauth
94
+ // idiom) so scope changes are actually granted, not silently skipped.
95
+ window.location.href = url + "&orgIds=" + ORG_ID + "&prompt=consent";
96
  }
97
 
98
  // ── state ────────────────────────────────────────────────────────────────────
 
103
  shownAt: 0, rubricVersion: "", resultsRepo: null, last: null,
104
  breakShownAt: Date.now(), stickyBanner: null, stickyActions: [], stickyKind: null,
105
  gradedSinceBreak: 0, infoNavsLeft: 0,
106
+ hbTimer: null, pushing: null, pushQueued: false, publishBlocked: null,
107
  };
108
 
109
  const logKey = () => `audit:${CONFIG.task}:${state.user}`;
 
125
  (ev) => CONFIG.isComplete(ev));
126
  }
127
 
128
+ // ── results repo (own namespace) ─────────────────────────────────────────────
129
+ // GET /api/datasets/{name} with the user's token: 200 = exists and visible.
130
+ async function repoProbe() {
131
+ try {
132
+ const res = await hub().fetch(
133
+ `https://huggingface.co/api/datasets/${state.resultsRepo.name}`,
134
+ { headers: { Authorization: `Bearer ${state.auth.accessToken}` },
135
+ cache: "no-store" });
136
+ return classifyProbe(res.ok ? 200 : res.status);
137
+ } catch { return "network"; }
138
+ }
139
+
140
+ // huggingface_hub's own exist_ok logic, ported: create, treat the
141
+ // already-exists 409 as success, retry the concurrency 409, and on a
142
+ // permission 401/403 probe whether the repo exists anyway (write-repos can
143
+ // commit to it even when creation is not granted). Returns a status object —
144
+ // boot decides policy; grading must never be blocked by this.
145
  async function ensureResultsRepo() {
146
  const name = `${state.user}/localgate-audit-results`;
147
  state.resultsRepo = { type: "dataset", name };
148
+ for (let attempt = 0; attempt < 3; attempt++) {
149
+ try {
150
+ await hub().createRepo({ repo: state.resultsRepo,
151
+ accessToken: state.auth.accessToken, private: true });
152
+ return { ok: true, repo: "created" };
153
+ } catch (err) {
154
+ const status = statusOf(err);
155
+ if (status === 409 || /already/i.test(String(err))) {
156
+ if (/conflicting operation/i.test(String(err))) continue; // create race
157
+ return { ok: true, repo: "present" };
158
+ }
159
+ if (status === 401 || status === 403) {
160
+ const probe = await repoProbe();
161
+ if (probe === "present") return { ok: true, repo: "present" };
162
+ if (probe === "absent") return { ok: false, kind: "cannot-create" };
163
+ return { ok: false, kind: probe === "denied" ? "stale-token" : "network" };
164
+ }
165
+ return { ok: false, kind: "network", detail: String(err).slice(0, 120) };
166
+ }
167
  }
168
+ return { ok: false, kind: "network", detail: "create kept conflicting" };
169
  }
170
 
171
  const remotePath = () => `${CONFIG.task}/${state.user}.jsonl`;
172
 
173
+ // Read one of the user's own log files with honest outcomes: an absent repo
174
+ // or file is "absent" (legitimately not started); permission problems are
175
+ // "denied" (stale token — NOT the same as zero progress); anything else is
176
+ // "error". Callers must never render "0 graded" for denied/error.
177
+ async function readOwnLog(task) {
178
+ try {
179
+ const blob = await hub().downloadFile({
180
+ repo: state.resultsRepo, accessToken: state.auth.accessToken,
181
+ path: `${task}/${state.user}.jsonl` });
182
+ if (blob === null) return { kind: "absent", rows: [] };
183
+ const rows = (await blob.text()).split("\n").filter(Boolean)
184
+ .map((line) => JSON.parse(line));
185
+ return { kind: "rows", rows };
186
+ } catch (err) {
187
+ const status = statusOf(err);
188
+ if (status === 404) return { kind: "absent", rows: [] }; // repo not created yet
189
+ if (status === 401 || status === 403) return { kind: "denied", rows: [] };
190
+ return { kind: "error", rows: [], detail: String(err).slice(0, 100) };
191
+ }
192
+ }
193
+
194
  async function pullRemote() {
195
  const blob = await hub().downloadFile({ repo: state.resultsRepo, path: remotePath(),
196
  accessToken: state.auth.accessToken });
 
228
  parentCommit: parent,
229
  });
230
  } catch (err) {
231
+ if (attempt < 3 && (statusOf(err) === 412
232
+ || /412|precondition/i.test(String(err)))) continue;
233
  throw err; // someone kept committing, or a real failure
234
  }
235
  // Events recorded while the upload was in flight are still unsaved —
236
  // assigning 0 here would make the pagehide flush skip them.
237
  state.unsaved = state.events.length - snapshot;
238
+ state.publishBlocked = null; // publishing works again
239
  clearErrorBanner(); // never eats an info banner
240
  renderStatus();
241
  return;
 
272
  catch (err) {
273
  if (document.visibilityState === "hidden") return; // browser killed the
274
  // fetch on backgrounding — expected on phones; retried on return/boot
275
+ const status = statusOf(err);
276
  if (!storedAuth()) {
277
+ // Tokens expire (no refresh) and stale-scope tokens are rejected — a
278
+ // retry can never succeed. Re-login; localStorage keeps everything.
279
+ banner("Your login needs refreshing — all your work is saved in this "
280
+ + "browser. Sign in again to publish it; you will continue "
281
+ + "exactly where you left off.", true,
282
+ [{ label: "Sign in again (refreshes permissions)", onClick: signIn },
283
  { label: "Download your log", onClick: downloadLog }]);
284
  return;
285
  }
286
+ if (status === 401 || status === 403 || /forbidden/i.test(String(err))) {
287
+ state.publishBlocked = "permissions"; // stop burning auto-pushes
 
288
  banner("Your work is saved in this browser, but publishing was refused "
289
  + "(permissions). Sign in again to refresh your access — you will "
290
  + "continue exactly where you left off.", true,
291
+ [{ label: "Sign in again (refreshes permissions)", onClick: signIn },
 
292
  { label: "Download your log", onClick: downloadLog }]);
293
  return;
294
  }
 
367
  cls: stampClass(values) };
368
  state.unsaved += 1;
369
  if (!item.warmup) state.gradedSinceBreak += 1;
370
+ if (state.unsaved >= SAVE_EVERY && !state.publishBlocked) pushGuarded();
371
  }
372
 
373
  // ── UI ───────────────────────────────────────────────────────────────────────
 
605
  renderItem();
606
  if (discarded) {
607
  banner("Heads up: the note on the item you just left was not recorded — "
608
+ + "notes only save once both of its questions are answered.",
609
+ false, [], "info");
610
  }
611
  }
612
 
 
748
  return;
749
  }
750
 
751
+ let prep;
752
  try {
753
+ prep = await withTimeout(ensureResultsRepo(), 60, "preparing your results dataset");
754
+ } catch { prep = { ok: false, kind: "network" }; }
755
+ if (!prep.ok) {
756
+ // NEVER block grading on this: every judgment lands in localStorage and
757
+ // the backlog auto-publishes once publishing works. Only the "published"
758
+ // claim is blocked (completion screen + counters stay honest).
759
+ state.publishBlocked = prep.kind;
760
+ const text = prep.kind === "stale-token"
761
+ ? "Your access needs refreshing before results can publish. You can "
762
+ + "grade now — everything is kept in this browser — but do sign in "
763
+ + "again soon so it uploads."
764
+ : prep.kind === "cannot-create"
765
+ ? "Your results dataset does not exist yet and this login cannot "
766
+ + "create it. You can grade now — everything is kept in this "
767
+ + "browser — then sign in again to set it up."
768
+ : "Could not reach your results dataset (network). You can grade — "
769
+ + "everything is kept in this browser and publishing retries.";
770
+ banner(text, true,
771
+ [{ label: "Sign in again (refreshes permissions)", onClick: signIn },
772
+ { label: "Download your log", onClick: downloadLog }]);
773
  }
774
 
775
  // Ordering gate: a task can require another task's completion first (the
776
  // conversion fidelity pass must not open before the blind verdict pass,
777
  // because it reveals which items the filter kept).
778
  if (CONFIG.requires) {
779
+ const log = await readOwnLog(CONFIG.requires.task);
780
+ if (log.kind === "denied" || log.kind === "error") {
781
+ // Saying "you have graded 0 items" here would be FALSE — the read
782
+ // failed; their work may be complete. Fail closed with the truth.
783
+ $("#main").replaceChildren(el("p", null,
784
+ `Could not read your ${CONFIG.requires.label} progress`
785
+ + (log.kind === "denied" ? " (permissions — your login may need refreshing)."
786
+ : " (network)." )
787
+ + " This pass stays locked until it can be verified."));
788
+ const again = el("button", "signin", log.kind === "denied"
789
+ ? "Sign in again (refreshes permissions)" : "Retry");
790
+ again.addEventListener("click",
791
+ log.kind === "denied" ? signIn : () => location.reload());
792
+ $("#main").append(again);
793
+ $("#controls").hidden = true;
794
+ return;
795
+ }
796
+ const done = countComplete(log.rows, CONFIG.requires.fields);
797
  if (done < CONFIG.requires.count) {
798
  $("#main").replaceChildren(el("p", null,
799
  `${CONFIG.requires.label} must be finished first — you have graded ` +
 
814
  // wrongly locking is a reload; wrongly unlocking is an invisible validity
815
  // hole (post-unblinding edits are only detectable, not preventable).
816
  if (CONFIG.lockWhen) {
817
+ const log = await readOwnLog(CONFIG.lockWhen.task);
818
+ const started = log.kind === "rows" && log.rows.some((ev) => !ev.warmup);
819
+ const unreadable = log.kind === "denied" || log.kind === "error";
 
 
 
 
 
 
 
 
820
  if (started || unreadable) {
821
  $("#main").replaceChildren(el("p", null, unreadable
822
  ? "Could not verify whether the next pass has already started, so this "
823
+ + "one stays closed to be safe."
824
+ + (log.kind === "denied" ? " Your login may need refreshing." : "")
825
  : `${CONFIG.lockWhen.label} has begun, so this pass is closed for `
826
  + "revision — its blindness window has ended. Every verdict you "
827
  + "recorded here is safe and counted. If a correction is genuinely "
828
  + "needed, tell Samuel; corrections after unblinding are flagged in "
829
  + "the analysis rather than silently applied."));
830
+ if (unreadable) {
831
+ const again = el("button", "signin", log.kind === "denied"
832
+ ? "Sign in again (refreshes permissions)" : "Retry");
833
+ again.addEventListener("click",
834
+ log.kind === "denied" ? signIn : () => location.reload());
835
+ $("#main").append(again);
836
+ }
837
  $("#controls").hidden = true;
838
  return;
839
  }
 
862
  // Transient on purpose: mobile browsers reload the tab on every app
863
  // switch, so a sticky notice re-arms forever and reads as stuck.
864
  banner(`Welcome back — ${done} already recorded. `
865
+ + "Continuing exactly where you left off.", false, [], "info");
866
  }
867
  }
868
 
config.js CHANGED
@@ -2,7 +2,7 @@ window.CONFIG = {
2
  task: "calibration",
3
  title: "Judge calibration — 100 gradings",
4
  itemsPath: "calibration_items.json",
5
- build: "d4fa750-space",
6
  landing: "You will grade whether a model's answer states the same fact as a "
7
  + "reference answer. 100 gradings plus 4 discussable warm-ups; expect 2–3 "
8
  + "hours total, in as many sittings as you like — progress saves continuously. "
 
2
  task: "calibration",
3
  title: "Judge calibration — 100 gradings",
4
  itemsPath: "calibration_items.json",
5
+ build: "d667e6d-space",
6
  landing: "You will grade whether a model's answer states the same fact as a "
7
  + "reference answer. 100 gradings plus 4 discussable warm-ups; expect 2–3 "
8
  + "hours total, in as many sittings as you like — progress saves continuously. "
index.html CHANGED
@@ -5,8 +5,8 @@
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
  <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self'; style-src 'self'; style-src-attr 'unsafe-inline'; font-src 'self'; connect-src https://huggingface.co https://*.hf.co; img-src 'self' data:; base-uri 'none'; form-action 'none'">
7
  <title>LocalGate audit</title>
8
- <link rel="stylesheet" href="style.css?v=d4fa750-space">
9
- <link rel="stylesheet" href="katex/katex.min.css?v=d4fa750-space">
10
  </head>
11
  <body>
12
  <header>
@@ -22,9 +22,9 @@
22
  <pre id="rubric-body"></pre></details>
23
  </div>
24
  <div id="controls" hidden></div>
25
- <script src="katex/katex.min.js?v=d4fa750-space"></script>
26
- <script src="katex/auto-render.min.js?v=d4fa750-space"></script>
27
- <script src="config.js?v=d4fa750-space"></script>
28
- <script type="module" src="app.js?v=d4fa750-space"></script>
29
  </body>
30
  </html>
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
  <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self'; style-src 'self'; style-src-attr 'unsafe-inline'; font-src 'self'; connect-src https://huggingface.co https://*.hf.co; img-src 'self' data:; base-uri 'none'; form-action 'none'">
7
  <title>LocalGate audit</title>
8
+ <link rel="stylesheet" href="style.css?v=d667e6d-space">
9
+ <link rel="stylesheet" href="katex/katex.min.css?v=d667e6d-space">
10
  </head>
11
  <body>
12
  <header>
 
22
  <pre id="rubric-body"></pre></details>
23
  </div>
24
  <div id="controls" hidden></div>
25
+ <script src="katex/katex.min.js?v=d667e6d-space"></script>
26
+ <script src="katex/auto-render.min.js?v=d667e6d-space"></script>
27
+ <script src="config.js?v=d667e6d-space"></script>
28
+ <script type="module" src="app.js?v=d667e6d-space"></script>
29
  </body>
30
  </html>
logic.mjs CHANGED
@@ -82,3 +82,24 @@ export function isForeignFresh(hb, sessionId, now, freshMs) {
82
  export function standsDownTo(hb, sessionId, myEpoch, now, freshMs) {
83
  return isForeignFresh(hb, sessionId, now, freshMs) && (hb.epoch ?? 0) >= myEpoch;
84
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  export function standsDownTo(hb, sessionId, myEpoch, now, freshMs) {
83
  return isForeignFresh(hb, sessionId, now, freshMs) && (hb.epoch ?? 0) >= myEpoch;
84
  }
85
+
86
+ // ── error/status classification (HF error prose carries no status digits —
87
+ // branch on HubApiError.statusCode, never on message text) ────────────────
88
+
89
+ export const statusOf = (err) =>
90
+ Number.isInteger(err?.statusCode) ? err.statusCode
91
+ : Number((String(err).match(/\b(40\d|409|412|5\d{2})\b/) ?? [])[1]) || null;
92
+
93
+ // Repo-existence probe outcomes, from a raw fetch status.
94
+ export const classifyProbe = (status) =>
95
+ status === 200 ? "present"
96
+ : status === 404 ? "absent"
97
+ : (status === 401 || status === 403) ? "denied"
98
+ : "network";
99
+
100
+ // A cached token is only usable if it still carries every scope the app now
101
+ // requires — scopes changed mid-study, and a stale grant fails at publish.
102
+ export const hasScopes = (grantedString, required) => {
103
+ const granted = new Set(String(grantedString ?? "").split(/\s+/));
104
+ return required.every((scope) => granted.has(scope));
105
+ };