osolmaz HF Staff commited on
Commit
feec173
·
verified ·
1 Parent(s): 851c583

deploy: 755c0b7

Browse files
.xtap-deployment.json CHANGED
@@ -1,3 +1,3 @@
1
  {
2
- "source_revision": "65f6c17c7e476c1888fff0d27ba56b4728a036e9"
3
  }
 
1
  {
2
+ "source_revision": "755c0b7c154599a59ec569189ebfa2fb70e13353"
3
  }
package.json CHANGED
@@ -13,6 +13,7 @@
13
  "setup": "npm run build --workspace shared && npm run build --workspace setup && npm run start --workspace setup",
14
  "update": "npm run build --workspace shared && npm run build --workspace setup && npm run start --workspace setup -- update",
15
  "doctor": "npm run --silent build --workspace shared && npm run --silent build --workspace setup && npm run --silent start --workspace setup -- doctor",
 
16
  "format": "prettier --check .",
17
  "format:write": "prettier --write .",
18
  "lint": "eslint .",
 
13
  "setup": "npm run build --workspace shared && npm run build --workspace setup && npm run start --workspace setup",
14
  "update": "npm run build --workspace shared && npm run build --workspace setup && npm run start --workspace setup -- update",
15
  "doctor": "npm run --silent build --workspace shared && npm run --silent build --workspace setup && npm run --silent start --workspace setup -- doctor",
16
+ "index:bootstrap": "npm run build --workspace shared && npm run build --workspace space && npm run index:bootstrap --workspace space",
17
  "format": "prettier --check .",
18
  "format:write": "prettier --write .",
19
  "lint": "eslint .",
scripts/deploy-space.sh CHANGED
@@ -1,64 +1,103 @@
1
  #!/usr/bin/env bash
2
  # Deploy this repo to a Hugging Face Docker Space.
3
  #
4
- # Requires an HF token with write access to the target namespace (a personal
5
- # session via `hf auth login`, not an agent's propose-only token).
 
 
6
  #
7
  # Usage:
8
- # scripts/deploy-space.sh <namespace> # e.g. dutifuldev or osolmaz
9
- # SPACE_REPO=<ns>/<name> DATASET_REPO=<ns>/<name> scripts/deploy-space.sh
 
10
  set -euo pipefail
11
 
12
  NAMESPACE="${1:-${NAMESPACE:-}}"
13
  SPACE_REPO="${SPACE_REPO:-${NAMESPACE:?usage: deploy-space.sh <namespace>}/xtap-pool}"
14
  DATASET_REPO="${DATASET_REPO:-${NAMESPACE}/xtap-pool-data}"
 
15
  ALLOWED_USERS="${ALLOWED_USERS:-osolmaz}"
 
 
16
 
17
  ROOT="$(cd "$(dirname "$0")/.." && pwd)"
18
  STAGE="$(mktemp -d)"
19
- trap 'rm -rf "$STAGE"' EXIT
 
20
 
21
- echo "==> Creating repos (idempotent)"
22
  hf repos create "$DATASET_REPO" --repo-type dataset --private 2>/dev/null || true
 
23
  # The Space itself is public: a private Space would put HF's repo-access gate
24
- # in front of the app, blocking friends who are on ALLOWED_USERS but not
25
- # Space collaborators. All data access is enforced in-app (OAuth allowlist +
26
- # pool tokens); anonymous visitors only see the sign-in page. To keep the
27
- # Space page private instead, add every friend as a Space collaborator.
28
  hf repos create "$SPACE_REPO" --repo-type space --space-sdk docker 2>/dev/null || true
29
 
30
- echo "==> Staging Space contents"
31
- git -C "$ROOT" archive HEAD | tar -x -C "$STAGE"
32
- cp "$ROOT/space/hf-space-README.md" "$STAGE/README.md"
33
- rm -rf "$STAGE/docs" "$STAGE/extension"
 
 
 
 
 
 
 
 
 
 
34
 
35
- echo "==> Uploading to $SPACE_REPO"
36
- hf upload "$SPACE_REPO" "$STAGE" . --repo-type space --commit-message "deploy: $(git -C "$ROOT" rev-parse --short HEAD)"
 
 
 
 
 
 
37
 
38
  echo "==> Setting Space secrets and variables"
39
- python3 - "$SPACE_REPO" "$DATASET_REPO" "$ALLOWED_USERS" <<'PY'
 
 
 
 
 
 
 
 
40
  import secrets
41
- import sys
42
 
43
  from huggingface_hub import HfApi
44
 
45
- space, dataset, allowed = sys.argv[1:4]
46
  api = HfApi()
47
  variables = dict(api.get_space_variables(space))
48
- api.add_space_variable(space, "DATASET_REPO", dataset)
49
- api.add_space_variable(space, "ALLOWED_USERS", allowed)
 
 
 
 
 
 
 
50
  # Secrets cannot be listed back, so a sentinel variable marks that they were
51
- # set once. Never rotate silently: rotating logs everyone out / disconnects
52
  # every extension.
53
  if "SECRETS_INITIALIZED" not in variables:
54
  for name in ("POOL_SIGNING_SECRET", "SESSION_SECRET"):
55
  api.add_space_secret(space, name, secrets.token_hex(32))
56
  api.add_space_variable(space, "SECRETS_INITIALIZED", "1")
57
- print("Set DATASET_REPO, ALLOWED_USERS, POOL_SIGNING_SECRET, SESSION_SECRET.")
58
- print("Remaining manual steps:")
59
- print(f" 1. Create a fine-grained token with read/write access to {dataset} only,")
60
- print(f" then: python3 -c \"from huggingface_hub import HfApi; HfApi().add_space_secret('{space}', 'HF_TOKEN', '<token>')\"")
61
- print(f" 2. Optionally import history: scripts/seed-dataset.sh {dataset} <hf-username> ~/Downloads/xtap")
62
  PY
63
 
 
 
 
 
 
 
 
 
64
  echo "==> Done. Space: https://huggingface.co/spaces/$SPACE_REPO"
 
1
  #!/usr/bin/env bash
2
  # Deploy this repo to a Hugging Face Docker Space.
3
  #
4
+ # Requires an authenticated Hugging Face CLI session that can create repos and
5
+ # Buckets. XTAP_STORAGE_TOKEN must be a fine-grained token with read/write
6
+ # access to only the target dataset and index Bucket. Supplying it explicitly
7
+ # authorizes this script to install it as the Space HF_TOKEN secret.
8
  #
9
  # Usage:
10
+ # XTAP_STORAGE_TOKEN=... scripts/deploy-space.sh <namespace>
11
+ # SPACE_REPO=<ns>/<name> DATASET_REPO=<ns>/<name> INDEX_BUCKET=<ns>/<name> \
12
+ # XTAP_STORAGE_TOKEN=... scripts/deploy-space.sh
13
  set -euo pipefail
14
 
15
  NAMESPACE="${1:-${NAMESPACE:-}}"
16
  SPACE_REPO="${SPACE_REPO:-${NAMESPACE:?usage: deploy-space.sh <namespace>}/xtap-pool}"
17
  DATASET_REPO="${DATASET_REPO:-${NAMESPACE}/xtap-pool-data}"
18
+ INDEX_BUCKET="${INDEX_BUCKET:-${NAMESPACE}/xtap-pool-bucket}"
19
  ALLOWED_USERS="${ALLOWED_USERS:-osolmaz}"
20
+ LLM_MODEL="${LLM_MODEL:-zai-org/GLM-5.2:fireworks-ai}"
21
+ TAXONOMY_VERSION="${TAXONOMY_VERSION:-1}"
22
 
23
  ROOT="$(cd "$(dirname "$0")/.." && pwd)"
24
  STAGE="$(mktemp -d)"
25
+ INDEX_WORK="$(mktemp -d)"
26
+ trap 'rm -rf "$STAGE" "$INDEX_WORK"' EXIT
27
 
28
+ echo "==> Creating storage and Space resources (idempotent)"
29
  hf repos create "$DATASET_REPO" --repo-type dataset --private 2>/dev/null || true
30
+ hf buckets create "$INDEX_BUCKET" --private --exist-ok
31
  # The Space itself is public: a private Space would put HF's repo-access gate
32
+ # in front of the app. Anonymous visitors still see only the sign-in page.
 
 
 
33
  hf repos create "$SPACE_REPO" --repo-type space --space-sdk docker 2>/dev/null || true
34
 
35
+ if [[ -z "${XTAP_STORAGE_TOKEN:-}" ]]; then
36
+ cat >&2 <<EOF
37
+ XTAP_STORAGE_TOKEN is required before deployment.
38
+ Create a fine-grained token with read/write access to exactly:
39
+ dataset: $DATASET_REPO
40
+ Bucket: $INDEX_BUCKET
41
+ Before rerunning, optionally import history so the first durable generation
42
+ contains it:
43
+ scripts/seed-dataset.sh $DATASET_REPO <hf-username> ~/Downloads/xtap
44
+ Then rerun this command with XTAP_STORAGE_TOKEN set. The script will bootstrap
45
+ the durable index and install the same value as the Space HF_TOKEN.
46
+ EOF
47
+ exit 2
48
+ fi
49
 
50
+ echo "==> Bootstrapping and verifying the durable index"
51
+ DATA_DIR="$INDEX_WORK" \
52
+ DATASET_REPO="$DATASET_REPO" \
53
+ INDEX_BUCKET="$INDEX_BUCKET" \
54
+ HF_TOKEN="$XTAP_STORAGE_TOKEN" \
55
+ LLM_MODEL="$LLM_MODEL" \
56
+ TAXONOMY_VERSION="$TAXONOMY_VERSION" \
57
+ npm --prefix "$ROOT" run index:bootstrap
58
 
59
  echo "==> Setting Space secrets and variables"
60
+ SPACE_REPO="$SPACE_REPO" \
61
+ DATASET_REPO="$DATASET_REPO" \
62
+ INDEX_BUCKET="$INDEX_BUCKET" \
63
+ ALLOWED_USERS="$ALLOWED_USERS" \
64
+ LLM_MODEL="$LLM_MODEL" \
65
+ TAXONOMY_VERSION="$TAXONOMY_VERSION" \
66
+ XTAP_STORAGE_TOKEN="$XTAP_STORAGE_TOKEN" \
67
+ python3 <<'PY'
68
+ import os
69
  import secrets
 
70
 
71
  from huggingface_hub import HfApi
72
 
73
+ space = os.environ["SPACE_REPO"]
74
  api = HfApi()
75
  variables = dict(api.get_space_variables(space))
76
+ for name in (
77
+ "DATASET_REPO",
78
+ "INDEX_BUCKET",
79
+ "ALLOWED_USERS",
80
+ "LLM_MODEL",
81
+ "TAXONOMY_VERSION",
82
+ ):
83
+ api.add_space_variable(space, name, os.environ[name])
84
+ api.add_space_secret(space, "HF_TOKEN", os.environ["XTAP_STORAGE_TOKEN"])
85
  # Secrets cannot be listed back, so a sentinel variable marks that they were
86
+ # set once. Never rotate silently: rotating logs everyone out or disconnects
87
  # every extension.
88
  if "SECRETS_INITIALIZED" not in variables:
89
  for name in ("POOL_SIGNING_SECRET", "SESSION_SECRET"):
90
  api.add_space_secret(space, name, secrets.token_hex(32))
91
  api.add_space_variable(space, "SECRETS_INITIALIZED", "1")
92
+ print("Set storage variables, contract variables, and the scoped HF_TOKEN.")
 
 
 
 
93
  PY
94
 
95
+ echo "==> Staging Space contents"
96
+ git -C "$ROOT" archive HEAD | tar -x -C "$STAGE"
97
+ cp "$ROOT/space/hf-space-README.md" "$STAGE/README.md"
98
+ rm -rf "$STAGE/docs" "$STAGE/extension"
99
+
100
+ echo "==> Uploading to $SPACE_REPO"
101
+ hf upload "$SPACE_REPO" "$STAGE" . --repo-type space --commit-message "deploy: $(git -C "$ROOT" rev-parse --short HEAD)"
102
+
103
  echo "==> Done. Space: https://huggingface.co/spaces/$SPACE_REPO"
shared/src/enrichment.ts CHANGED
@@ -3,6 +3,8 @@ import { z } from "zod";
3
  import { dayKey } from "./tweet.js";
4
  import type { Tweet } from "./tweet.js";
5
 
 
 
6
  /** One preset taxonomy entry; the description steers the classifier. */
7
  export const labelConfigSchema = z.object({
8
  name: z.string().min(1),
 
3
  import { dayKey } from "./tweet.js";
4
  import type { Tweet } from "./tweet.js";
5
 
6
+ export const DEFAULT_ENRICHMENT_MODEL = "zai-org/GLM-5.2:fireworks-ai";
7
+
8
  /** One preset taxonomy entry; the description steers the classifier. */
9
  export const labelConfigSchema = z.object({
10
  name: z.string().min(1),
shared/src/index.ts CHANGED
@@ -9,6 +9,7 @@ export {
9
  type TweetValidationResult,
10
  } from "./tweet.js";
11
  export {
 
12
  labelConfigSchema,
13
  enrichmentRowSchema,
14
  parseEnrichmentRow,
 
9
  type TweetValidationResult,
10
  } from "./tweet.js";
11
  export {
12
+ DEFAULT_ENRICHMENT_MODEL,
13
  labelConfigSchema,
14
  enrichmentRowSchema,
15
  parseEnrichmentRow,
space/package.json CHANGED
@@ -9,6 +9,7 @@
9
  "start": "node dist/src/server.js",
10
  "enrich": "node dist/src/enrich-command-main.js",
11
  "enrich:job": "node dist/src/enrich-job-main.js",
 
12
  "format": "prettier --check .",
13
  "lint": "eslint .",
14
  "typecheck": "tsc -p tsconfig.typecheck.json --noEmit",
 
9
  "start": "node dist/src/server.js",
10
  "enrich": "node dist/src/enrich-command-main.js",
11
  "enrich:job": "node dist/src/enrich-job-main.js",
12
+ "index:bootstrap": "node dist/src/index-command-main.js",
13
  "format": "prettier --check .",
14
  "lint": "eslint .",
15
  "typecheck": "tsc -p tsconfig.typecheck.json --noEmit",
space/src/config.ts CHANGED
@@ -1,9 +1,11 @@
 
1
  import { z } from "zod";
2
 
3
  const configSchema = z.object({
4
  PORT: z.coerce.number().int().positive().default(7860),
5
  DATA_DIR: z.string().default(".data"),
6
  DATASET_REPO: z.string().min(1),
 
7
  HF_TOKEN: z.string().min(1),
8
  INFERENCE_TOKEN: z.string().min(1).optional(),
9
  POOL_SIGNING_SECRET: z.string().min(32),
@@ -26,7 +28,7 @@ const configSchema = z.object({
26
  ENRICH_OUTPUT_TOKEN_USD: z.coerce.number().nonnegative().optional(),
27
  ENRICH_MAX_DISCARDED_ASSIGNMENTS_PER_UNIT: z.coerce.number().nonnegative().optional(),
28
  ENRICH_DISCARDED_ASSIGNMENT_RATE_MIN_UNITS: z.coerce.number().int().positive().optional(),
29
- LLM_MODEL: z.string().min(1).default("zai-org/GLM-5.2"),
30
  TAXONOMY_VERSION: z.coerce.number().int().min(1).default(1),
31
  });
32
 
@@ -34,6 +36,7 @@ export type SpaceConfig = {
34
  port: number;
35
  dataDir: string;
36
  datasetRepo: string;
 
37
  hfToken: string;
38
  inferenceToken?: string;
39
  poolSigningSecret: string;
@@ -93,6 +96,7 @@ export function loadConfig(env: Record<string, string | undefined>): SpaceConfig
93
  port: parsed.PORT,
94
  dataDir: parsed.DATA_DIR,
95
  datasetRepo: parsed.DATASET_REPO,
 
96
  hfToken: parsed.HF_TOKEN,
97
  ...(parsed.INFERENCE_TOKEN === undefined ? {} : { inferenceToken: parsed.INFERENCE_TOKEN }),
98
  poolSigningSecret: parsed.POOL_SIGNING_SECRET,
 
1
+ import { DEFAULT_ENRICHMENT_MODEL } from "@xtap-pool/shared";
2
  import { z } from "zod";
3
 
4
  const configSchema = z.object({
5
  PORT: z.coerce.number().int().positive().default(7860),
6
  DATA_DIR: z.string().default(".data"),
7
  DATASET_REPO: z.string().min(1),
8
+ INDEX_BUCKET: z.string().min(1),
9
  HF_TOKEN: z.string().min(1),
10
  INFERENCE_TOKEN: z.string().min(1).optional(),
11
  POOL_SIGNING_SECRET: z.string().min(32),
 
28
  ENRICH_OUTPUT_TOKEN_USD: z.coerce.number().nonnegative().optional(),
29
  ENRICH_MAX_DISCARDED_ASSIGNMENTS_PER_UNIT: z.coerce.number().nonnegative().optional(),
30
  ENRICH_DISCARDED_ASSIGNMENT_RATE_MIN_UNITS: z.coerce.number().int().positive().optional(),
31
+ LLM_MODEL: z.string().min(1).default(DEFAULT_ENRICHMENT_MODEL),
32
  TAXONOMY_VERSION: z.coerce.number().int().min(1).default(1),
33
  });
34
 
 
36
  port: number;
37
  dataDir: string;
38
  datasetRepo: string;
39
+ indexBucket: string;
40
  hfToken: string;
41
  inferenceToken?: string;
42
  poolSigningSecret: string;
 
96
  port: parsed.PORT,
97
  dataDir: parsed.DATA_DIR,
98
  datasetRepo: parsed.DATASET_REPO,
99
+ indexBucket: parsed.INDEX_BUCKET,
100
  hfToken: parsed.HF_TOKEN,
101
  ...(parsed.INFERENCE_TOKEN === undefined ? {} : { inferenceToken: parsed.INFERENCE_TOKEN }),
102
  poolSigningSecret: parsed.POOL_SIGNING_SECRET,
space/src/dataset-token.ts CHANGED
@@ -10,27 +10,32 @@ const TARGET_PERMISSIONS = new Set([
10
  const READ_PERMISSION = "repo.content.read";
11
  const WRITE_PERMISSIONS = ["repo.content.write", "repo.write"] as const;
12
 
 
 
13
  export type DatasetCredentialReadiness =
14
  | { credential: "ok" }
15
  | { credential: "invalid"; error: string }
16
  | { credential: "unknown"; error: string };
17
 
18
- /** Verify the Space HF_TOKEN can read and write only the configured dataset repo. */
19
  export async function checkDatasetCredential(params: {
20
  token: string;
21
  datasetRepo: string;
 
22
  fetchFn?: typeof fetch;
23
  }): Promise<DatasetCredentialReadiness> {
24
  try {
25
  const response = await (params.fetchFn ?? fetch)("https://huggingface.co/api/whoami-v2", {
26
  headers: { authorization: `Bearer ${params.token}` },
27
  });
28
- if (!response.ok) {
29
- return tokenStatusError("HF_TOKEN", response.status);
30
- }
31
- const errors = datasetTokenErrors(await response.json(), params.datasetRepo);
 
 
32
  if (errors.length > 0) return { credential: "invalid", error: errors.join(" ") };
33
- return await checkDatasetDownload(params);
34
  } catch (error) {
35
  return { credential: "unknown", error: errorMessage(error) };
36
  }
@@ -40,26 +45,37 @@ export function datasetCredentialOk(status: DatasetCredentialReadiness): boolean
40
  return status.credential === "ok";
41
  }
42
 
43
- async function checkDatasetDownload(params: {
44
  token: string;
45
  datasetRepo: string;
 
46
  fetchFn?: typeof fetch;
47
  }): Promise<DatasetCredentialReadiness> {
48
- const response = await (params.fetchFn ?? fetch)(datasetProbeUrl(params.datasetRepo), {
49
- headers: { authorization: `Bearer ${params.token}` },
50
- });
51
- if (response.ok || response.status === 404) return { credential: "ok" };
52
- const error = `Hugging Face rejected a direct private-dataset download using HF_TOKEN (${String(response.status)}).`;
53
- return response.status === 401 || response.status === 403
54
- ? { credential: "invalid", error }
55
- : { credential: "unknown", error };
56
- }
57
-
58
- function datasetProbeUrl(datasetRepo: string): string {
59
- return `https://huggingface.co/datasets/${datasetRepo}/resolve/main/config/pool.json`;
 
 
 
 
 
 
 
 
 
 
60
  }
61
 
62
- function datasetTokenErrors(payload: unknown, datasetRepo: string): string[] {
63
  const root = asRecord(payload);
64
  const accessToken = asRecord(asRecord(root["auth"])["accessToken"]);
65
  const fineGrained = asRecord(accessToken["fineGrained"]);
@@ -69,12 +85,15 @@ function datasetTokenErrors(payload: unknown, datasetRepo: string): string[] {
69
  ? []
70
  : [`HF_TOKEN role is '${role || "unknown"}', expected fine-grained.`];
71
  errors.push(...globalPermissionErrors(fineGrained));
72
- const targetPermissions = scopedPermissionErrors(fineGrained, datasetRepo, errors);
73
- if (!targetPermissions.has(READ_PERMISSION)) {
74
- errors.push(`HF_TOKEN must include ${READ_PERMISSION} on ${datasetRepo}.`);
75
- }
76
- if (!WRITE_PERMISSIONS.some((permission) => targetPermissions.has(permission))) {
77
- errors.push(`HF_TOKEN must include ${WRITE_PERMISSIONS.join(" or ")} on ${datasetRepo}.`);
 
 
 
78
  }
79
  return errors;
80
  }
@@ -87,53 +106,51 @@ function globalPermissionErrors(fineGrained: JsonObject): string[] {
87
 
88
  function scopedPermissionErrors(
89
  fineGrained: JsonObject,
90
- datasetRepo: string,
91
  errors: string[],
92
- ): Set<string> {
93
- const targetPermissions = new Set<string>();
 
 
94
  for (const scope of array(fineGrained["scoped"])) {
95
- collectScopePermissions(asRecord(scope), datasetRepo, targetPermissions, errors);
96
  }
97
- return targetPermissions;
98
  }
99
 
100
  function collectScopePermissions(
101
  scope: JsonObject,
102
- datasetRepo: string,
103
- targetPermissions: Set<string>,
104
  errors: string[],
105
  ): void {
106
  const entity = asRecord(scope["entity"]);
 
107
  for (const permission of strings(scope["permissions"])) {
108
- if (matchesDataset(entity, datasetRepo)) {
109
- recordTargetPermission(permission, targetPermissions, errors);
110
- } else {
111
  errors.push(
112
- `Unexpected permission outside ${datasetRepo} on HF_TOKEN: ${permission} on ${entityLabel(entity)}.`,
113
  );
 
 
 
 
114
  }
115
  }
116
  }
117
 
118
- function recordTargetPermission(
119
- permission: string,
120
- targetPermissions: Set<string>,
121
- errors: string[],
122
- ): void {
123
- if (TARGET_PERMISSIONS.has(permission)) targetPermissions.add(permission);
124
- else errors.push(`Unexpected permission on HF_TOKEN: ${permission}.`);
125
- }
126
-
127
- function matchesDataset(entity: JsonObject, datasetRepo: string): boolean {
128
  return (
129
- isDatasetEntity(entity) &&
130
- entityCandidates(entity).some((candidate) => normalizeRepo(candidate) === datasetRepo)
 
 
131
  );
132
  }
133
 
134
- function isDatasetEntity(entity: JsonObject): boolean {
135
- const type = text(entity["type"]);
136
- return type === "dataset" || type === "datasets";
137
  }
138
 
139
  function entityCandidates(entity: JsonObject): readonly string[] {
@@ -150,8 +167,8 @@ function entityLabel(entity: JsonObject): string {
150
  return `${kind}:${name}`;
151
  }
152
 
153
- function normalizeRepo(value: string): string {
154
- return value.replace(/^datasets\//, "");
155
  }
156
 
157
  function tokenStatusError(tokenName: "HF_TOKEN", status: number): DatasetCredentialReadiness {
 
10
  const READ_PERMISSION = "repo.content.read";
11
  const WRITE_PERMISSIONS = ["repo.content.write", "repo.write"] as const;
12
 
13
+ type StorageTarget = { kind: "bucket" | "dataset"; name: string };
14
+
15
  export type DatasetCredentialReadiness =
16
  | { credential: "ok" }
17
  | { credential: "invalid"; error: string }
18
  | { credential: "unknown"; error: string };
19
 
20
+ /** Verify the Space HF_TOKEN can read and write the configured dataset and index Bucket only. */
21
  export async function checkDatasetCredential(params: {
22
  token: string;
23
  datasetRepo: string;
24
+ indexBucket: string;
25
  fetchFn?: typeof fetch;
26
  }): Promise<DatasetCredentialReadiness> {
27
  try {
28
  const response = await (params.fetchFn ?? fetch)("https://huggingface.co/api/whoami-v2", {
29
  headers: { authorization: `Bearer ${params.token}` },
30
  });
31
+ if (!response.ok) return tokenStatusError("HF_TOKEN", response.status);
32
+ const targets: readonly StorageTarget[] = [
33
+ { kind: "dataset", name: params.datasetRepo },
34
+ { kind: "bucket", name: params.indexBucket },
35
+ ];
36
+ const errors = storageTokenErrors(await response.json(), targets);
37
  if (errors.length > 0) return { credential: "invalid", error: errors.join(" ") };
38
+ return await checkStorageReads(params);
39
  } catch (error) {
40
  return { credential: "unknown", error: errorMessage(error) };
41
  }
 
45
  return status.credential === "ok";
46
  }
47
 
48
+ async function checkStorageReads(params: {
49
  token: string;
50
  datasetRepo: string;
51
+ indexBucket: string;
52
  fetchFn?: typeof fetch;
53
  }): Promise<DatasetCredentialReadiness> {
54
+ const fetchFn = params.fetchFn ?? fetch;
55
+ const probes = [
56
+ {
57
+ url: `https://huggingface.co/datasets/${params.datasetRepo}/resolve/main/config/pool.json`,
58
+ label: "private-dataset download",
59
+ },
60
+ {
61
+ url: `https://huggingface.co/api/buckets/${params.indexBucket}`,
62
+ label: "private-Bucket read",
63
+ },
64
+ ];
65
+ for (const probe of probes) {
66
+ const response = await fetchFn(probe.url, {
67
+ headers: { authorization: `Bearer ${params.token}` },
68
+ });
69
+ if (response.ok || response.status === 404) continue;
70
+ const error = `Hugging Face rejected a direct ${probe.label} using HF_TOKEN (${String(response.status)}).`;
71
+ return response.status === 401 || response.status === 403
72
+ ? { credential: "invalid", error }
73
+ : { credential: "unknown", error };
74
+ }
75
+ return { credential: "ok" };
76
  }
77
 
78
+ function storageTokenErrors(payload: unknown, targets: readonly StorageTarget[]): string[] {
79
  const root = asRecord(payload);
80
  const accessToken = asRecord(asRecord(root["auth"])["accessToken"]);
81
  const fineGrained = asRecord(accessToken["fineGrained"]);
 
85
  ? []
86
  : [`HF_TOKEN role is '${role || "unknown"}', expected fine-grained.`];
87
  errors.push(...globalPermissionErrors(fineGrained));
88
+ const permissionsByTarget = scopedPermissionErrors(fineGrained, targets, errors);
89
+ for (const target of targets) {
90
+ const permissions = permissionsByTarget.get(targetKey(target)) ?? new Set<string>();
91
+ if (!permissions.has(READ_PERMISSION)) {
92
+ errors.push(`HF_TOKEN must include ${READ_PERMISSION} on ${target.name}.`);
93
+ }
94
+ if (!WRITE_PERMISSIONS.some((permission) => permissions.has(permission))) {
95
+ errors.push(`HF_TOKEN must include ${WRITE_PERMISSIONS.join(" or ")} on ${target.name}.`);
96
+ }
97
  }
98
  return errors;
99
  }
 
106
 
107
  function scopedPermissionErrors(
108
  fineGrained: JsonObject,
109
+ targets: readonly StorageTarget[],
110
  errors: string[],
111
+ ): Map<string, Set<string>> {
112
+ const permissionsByTarget = new Map(
113
+ targets.map((target) => [targetKey(target), new Set<string>()]),
114
+ );
115
  for (const scope of array(fineGrained["scoped"])) {
116
+ collectScopePermissions(asRecord(scope), targets, permissionsByTarget, errors);
117
  }
118
+ return permissionsByTarget;
119
  }
120
 
121
  function collectScopePermissions(
122
  scope: JsonObject,
123
+ targets: readonly StorageTarget[],
124
+ permissionsByTarget: Map<string, Set<string>>,
125
  errors: string[],
126
  ): void {
127
  const entity = asRecord(scope["entity"]);
128
+ const target = targets.find((candidate) => matchesTarget(entity, candidate));
129
  for (const permission of strings(scope["permissions"])) {
130
+ if (target === undefined) {
 
 
131
  errors.push(
132
+ `Unexpected permission outside configured storage on HF_TOKEN: ${permission} on ${entityLabel(entity)}.`,
133
  );
134
+ } else if (!TARGET_PERMISSIONS.has(permission)) {
135
+ errors.push(`Unexpected permission on HF_TOKEN: ${permission}.`);
136
+ } else {
137
+ permissionsByTarget.get(targetKey(target))?.add(permission);
138
  }
139
  }
140
  }
141
 
142
+ function matchesTarget(entity: JsonObject, target: StorageTarget): boolean {
143
+ const type = text(entity["type"]).replace(/s$/u, "");
 
 
 
 
 
 
 
 
144
  return (
145
+ type === target.kind &&
146
+ entityCandidates(entity).some(
147
+ (candidate) => normalizeName(candidate, target.kind) === target.name,
148
+ )
149
  );
150
  }
151
 
152
+ function targetKey(target: StorageTarget): string {
153
+ return `${target.kind}:${target.name}`;
 
154
  }
155
 
156
  function entityCandidates(entity: JsonObject): readonly string[] {
 
167
  return `${kind}:${name}`;
168
  }
169
 
170
+ function normalizeName(value: string, kind: StorageTarget["kind"]): string {
171
+ return value.replace(kind === "dataset" ? /^datasets\//u : /^buckets\//u, "");
172
  }
173
 
174
  function tokenStatusError(tokenName: "HF_TOKEN", status: number): DatasetCredentialReadiness {
space/src/dataset.ts CHANGED
@@ -2,6 +2,7 @@ import { mkdirSync, readFileSync, rmSync, writeFileSync, existsSync } from "node
2
  import { dirname, isAbsolute, join, normalize, relative } from "node:path";
3
 
4
  import { commit, downloadFile, listFiles } from "@huggingface/hub";
 
5
 
6
  import {
7
  attemptEventSchema,
@@ -31,6 +32,13 @@ export type EnrichmentRefresh = {
31
  receipt?: EnrichReceipt;
32
  };
33
 
 
 
 
 
 
 
 
34
  type EnrichmentShardUpdate = {
35
  path: string;
36
  content: string;
@@ -41,8 +49,31 @@ type EnrichmentReplayCounts = Pick<EnrichmentRefresh, "rows" | "attempts" | "reg
41
 
42
  const REFRESH_SHARDS_PER_KIND = 4;
43
  const REFRESH_ATTEMPTS = 2;
44
-
45
- function isNotFound(error: unknown): boolean {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  return (
47
  typeof error === "object" &&
48
  error !== null &&
@@ -54,18 +85,23 @@ function isNotFound(error: unknown): boolean {
54
  function isMissingDatasetFile(error: unknown, path: string): boolean {
55
  const message = error instanceof Error ? error.message : String(error);
56
  return (
57
- isNotFound(error) ||
58
  message.includes(`missing: ${path}`) ||
59
  message.includes(`dataset file not found: ${path}`)
60
  );
61
  }
62
 
63
- async function assertDatasetRepoReadable(
64
  repo: { type: "dataset"; name: string },
65
  accessToken: string,
 
66
  ): Promise<void> {
67
  try {
68
- for await (const _entry of listFiles({ repo, accessToken })) {
 
 
 
 
69
  return;
70
  }
71
  } catch (error) {
@@ -86,7 +122,7 @@ export function createHubClient(datasetRepo: string, accessToken: string): HubCl
86
  if (entry.type === "file" && entry.path.endsWith(".jsonl")) paths.push(entry.path);
87
  }
88
  } catch (error) {
89
- if (isNotFound(error)) {
90
  // A fresh pool can lack the requested tree. Verify the repo itself is
91
  // readable so auth failures do not look like an empty dataset.
92
  await assertDatasetRepoReadable(repo, accessToken);
@@ -103,7 +139,7 @@ export function createHubClient(datasetRepo: string, accessToken: string): HubCl
103
  await assertDatasetRepoReadable(repo, accessToken);
104
  throw new Error(`dataset file not found: ${path}`);
105
  } catch (error) {
106
- if (isNotFound(error)) await assertDatasetRepoReadable(repo, accessToken);
107
  throw error;
108
  }
109
  },
@@ -354,6 +390,40 @@ export class DatasetMirror {
354
  return count;
355
  }
356
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357
  /** Read a dataset file through the Hub, returning undefined when it is absent. */
358
  async readText(path: string): Promise<string | undefined> {
359
  try {
@@ -381,10 +451,13 @@ export class DatasetMirror {
381
  writes: readonly { path: string; content: string }[],
382
  title: string,
383
  ): Promise<void> {
384
- const files = [
385
- ...appends.map(({ path, lines }) => ({ path, content: this.appendedContent(path, lines) })),
386
- ...writes,
387
- ];
 
 
 
388
  await this.hub.commitFiles(files, title);
389
  for (const file of files) {
390
  const local = this.localPath(file.path);
@@ -393,9 +466,11 @@ export class DatasetMirror {
393
  }
394
  }
395
 
396
- private appendedContent(path: string, lines: readonly string[]): string {
397
  const local = this.localPath(path);
398
- const existing = existsSync(local) ? readFileSync(local, "utf8") : "";
 
 
399
  const prefix = existing === "" || existing.endsWith("\n") ? existing : `${existing}\n`;
400
  return `${prefix}${lines.map((line) => `${line}\n`).join("")}`;
401
  }
@@ -427,6 +502,62 @@ export class DatasetMirror {
427
  */
428
  type EnrichmentShardKind = "receipt" | "attempt" | "registry" | "row";
429
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
430
  function classifyEnrichmentPath(path: string): EnrichmentShardKind {
431
  if (path.startsWith("enrichment/receipts/")) return "receipt";
432
  if (path.startsWith("enrichment/attempts/")) return "attempt";
@@ -450,6 +581,19 @@ function selectEnrichmentRefreshShards(
450
  return [...selected].sort();
451
  }
452
 
 
 
 
 
 
 
 
 
 
 
 
 
 
453
  function applyEnrichmentLines(enrich: EnrichStore, content: string): number {
454
  let rows = 0;
455
  for (const line of content.split("\n")) {
 
2
  import { dirname, isAbsolute, join, normalize, relative } from "node:path";
3
 
4
  import { commit, downloadFile, listFiles } from "@huggingface/hub";
5
+ import { z } from "zod";
6
 
7
  import {
8
  attemptEventSchema,
 
32
  receipt?: EnrichReceipt;
33
  };
34
 
35
+ export type DatasetSourceKind = "tweet" | "enrichment" | "attempt" | "registry" | "receipt";
36
+
37
+ export type AppliedDatasetSource = {
38
+ kind: DatasetSourceKind;
39
+ rows: number;
40
+ };
41
+
42
  type EnrichmentShardUpdate = {
43
  path: string;
44
  content: string;
 
49
 
50
  const REFRESH_SHARDS_PER_KIND = 4;
51
  const REFRESH_ATTEMPTS = 2;
52
+ const legacyReceiptSchema = z
53
+ .object({
54
+ started_at: z.string().min(1),
55
+ finished_at: z.string().min(1),
56
+ units: z.number().int().nonnegative(),
57
+ calls: z.number().int().nonnegative(),
58
+ prompt_tokens: z.number().int().nonnegative(),
59
+ completion_tokens: z.number().int().nonnegative(),
60
+ failures: z.number().int().nonnegative(),
61
+ })
62
+ .strict();
63
+ const legacyEnrichmentRowSchema = z
64
+ .object({
65
+ unit_id: z.string().min(1),
66
+ tweet_ids: z.array(z.string().min(1)).min(1),
67
+ labels: z.array(z.string()),
68
+ free_labels: z.array(z.string()),
69
+ concepts: z.array(z.unknown()),
70
+ model: z.string().min(1),
71
+ taxonomy_version: z.number().int().min(1),
72
+ enriched_at: z.string().min(1),
73
+ })
74
+ .loose();
75
+
76
+ export function isHubNotFound(error: unknown): boolean {
77
  return (
78
  typeof error === "object" &&
79
  error !== null &&
 
85
  function isMissingDatasetFile(error: unknown, path: string): boolean {
86
  const message = error instanceof Error ? error.message : String(error);
87
  return (
88
+ isHubNotFound(error) ||
89
  message.includes(`missing: ${path}`) ||
90
  message.includes(`dataset file not found: ${path}`)
91
  );
92
  }
93
 
94
+ export async function assertDatasetRepoReadable(
95
  repo: { type: "dataset"; name: string },
96
  accessToken: string,
97
+ revision?: string,
98
  ): Promise<void> {
99
  try {
100
+ for await (const _entry of listFiles({
101
+ repo,
102
+ accessToken,
103
+ ...(revision === undefined ? {} : { revision }),
104
+ })) {
105
  return;
106
  }
107
  } catch (error) {
 
122
  if (entry.type === "file" && entry.path.endsWith(".jsonl")) paths.push(entry.path);
123
  }
124
  } catch (error) {
125
+ if (isHubNotFound(error)) {
126
  // A fresh pool can lack the requested tree. Verify the repo itself is
127
  // readable so auth failures do not look like an empty dataset.
128
  await assertDatasetRepoReadable(repo, accessToken);
 
139
  await assertDatasetRepoReadable(repo, accessToken);
140
  throw new Error(`dataset file not found: ${path}`);
141
  } catch (error) {
142
+ if (isHubNotFound(error)) await assertDatasetRepoReadable(repo, accessToken);
143
  throw error;
144
  }
145
  },
 
390
  return count;
391
  }
392
 
393
+ /** Apply one complete file or verified append suffix to the SQLite projection. */
394
+ applySourceContent(
395
+ path: string,
396
+ content: string,
397
+ store: TweetStore,
398
+ enrich: EnrichStore,
399
+ ): AppliedDatasetSource {
400
+ const kind = datasetSourceKind(path);
401
+ switch (kind) {
402
+ case "tweet": {
403
+ const tweets = parseJsonlTweets(content, path);
404
+ store.insert(tweets);
405
+ enrich.registerTweets(tweets);
406
+ return { kind, rows: tweets.length };
407
+ }
408
+ case "enrichment":
409
+ return { kind, rows: applyEnrichmentLines(enrich, content) };
410
+ case "attempt":
411
+ return { kind, rows: replayAttemptLines(enrich, content) };
412
+ case "registry":
413
+ return { kind, rows: replayRegistryLines(enrich, content) };
414
+ case "receipt":
415
+ this.recordLatestReceipt(content);
416
+ return { kind, rows: countValidReceipts(content) };
417
+ }
418
+ }
419
+
420
+ /** Keep a current source file locally so a later append preserves its prefix. */
421
+ rememberSourceFile(path: string, content: string): void {
422
+ const local = this.localPath(path);
423
+ mkdirSync(dirname(local), { recursive: true });
424
+ writeFileSync(local, content);
425
+ }
426
+
427
  /** Read a dataset file through the Hub, returning undefined when it is absent. */
428
  async readText(path: string): Promise<string | undefined> {
429
  try {
 
451
  writes: readonly { path: string; content: string }[],
452
  title: string,
453
  ): Promise<void> {
454
+ const appended = await Promise.all(
455
+ appends.map(async ({ path, lines }) => ({
456
+ path,
457
+ content: await this.appendedContent(path, lines),
458
+ })),
459
+ );
460
+ const files = [...appended, ...writes];
461
  await this.hub.commitFiles(files, title);
462
  for (const file of files) {
463
  const local = this.localPath(file.path);
 
466
  }
467
  }
468
 
469
+ private async appendedContent(path: string, lines: readonly string[]): Promise<string> {
470
  const local = this.localPath(path);
471
+ const existing = existsSync(local)
472
+ ? readFileSync(local, "utf8")
473
+ : ((await this.readText(path)) ?? "");
474
  const prefix = existing === "" || existing.endsWith("\n") ? existing : `${existing}\n`;
475
  return `${prefix}${lines.map((line) => `${line}\n`).join("")}`;
476
  }
 
502
  */
503
  type EnrichmentShardKind = "receipt" | "attempt" | "registry" | "row";
504
 
505
+ export function assertValidDatasetSourceContent(path: string, content: string): void {
506
+ const kind = datasetSourceKind(path);
507
+ const lines = content.split("\n");
508
+ for (const [index, line] of lines.entries()) {
509
+ if (line.trim() === "") continue;
510
+ let candidate: unknown;
511
+ try {
512
+ candidate = JSON.parse(line);
513
+ } catch {
514
+ throw new Error(`invalid JSON in ${path} at line ${String(index + 1)}`);
515
+ }
516
+ if (!validDatasetSourceRecord(kind, candidate)) {
517
+ throw new Error(`invalid ${kind} record in ${path} at line ${String(index + 1)}`);
518
+ }
519
+ }
520
+ }
521
+
522
+ function validDatasetSourceRecord(kind: DatasetSourceKind, candidate: unknown): boolean {
523
+ switch (kind) {
524
+ case "tweet":
525
+ return validateTweet(candidate).ok;
526
+ case "enrichment":
527
+ return parseEnrichmentRow(candidate) !== undefined || isLegacyEnrichmentRow(candidate);
528
+ case "attempt":
529
+ return attemptEventSchema.safeParse(candidate).success;
530
+ case "registry":
531
+ return freeLabelEventSchema.safeParse(candidate).success;
532
+ case "receipt":
533
+ return (
534
+ parseEnrichReceipt(candidate) !== undefined ||
535
+ legacyReceiptSchema.safeParse(candidate).success
536
+ );
537
+ }
538
+ }
539
+
540
+ function isLegacyEnrichmentRow(candidate: unknown): boolean {
541
+ return legacyEnrichmentRowSchema.safeParse(candidate).success;
542
+ }
543
+
544
+ export function datasetSourceKind(path: string): DatasetSourceKind {
545
+ if (/^data\/[^/]+\/\d{4}\/\d{2}\/tweets-\d{4}-\d{2}-\d{2}\.jsonl$/u.test(path)) {
546
+ return "tweet";
547
+ }
548
+ if (/^enrichment\/\d{4}\/\d{2}\/enrichment-\d{4}-\d{2}-\d{2}\.jsonl$/u.test(path)) {
549
+ return "enrichment";
550
+ }
551
+ if (/^enrichment\/attempts\/\d{4}\/\d{2}\/attempts-\d{4}-\d{2}-\d{2}\.jsonl$/u.test(path)) {
552
+ return "attempt";
553
+ }
554
+ if (/^enrichment\/registry\/\d{4}\/\d{2}\/registry-\d{4}-\d{2}-\d{2}\.jsonl$/u.test(path)) {
555
+ return "registry";
556
+ }
557
+ if (/^enrichment\/receipts\/\d{4}-\d{2}-\d{2}\.jsonl$/u.test(path)) return "receipt";
558
+ throw new Error(`unsupported dataset index source: ${path}`);
559
+ }
560
+
561
  function classifyEnrichmentPath(path: string): EnrichmentShardKind {
562
  if (path.startsWith("enrichment/receipts/")) return "receipt";
563
  if (path.startsWith("enrichment/attempts/")) return "attempt";
 
581
  return [...selected].sort();
582
  }
583
 
584
+ function countValidReceipts(content: string): number {
585
+ let count = 0;
586
+ for (const line of content.split("\n")) {
587
+ if (line.trim() === "") continue;
588
+ try {
589
+ if (parseEnrichReceipt(JSON.parse(line)) !== undefined) count += 1;
590
+ } catch {
591
+ continue;
592
+ }
593
+ }
594
+ return count;
595
+ }
596
+
597
  function applyEnrichmentLines(enrich: EnrichStore, content: string): number {
598
  let rows = 0;
599
  for (const line of content.split("\n")) {
space/src/durable-index.ts ADDED
@@ -0,0 +1,887 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { createReadStream, createWriteStream, existsSync, openAsBlob } from "node:fs";
3
+ import { mkdir, rename, rm } from "node:fs/promises";
4
+ import { dirname } from "node:path";
5
+ import { Readable } from "node:stream";
6
+ import { pipeline } from "node:stream/promises";
7
+
8
+ import Database from "better-sqlite3";
9
+ import {
10
+ commit,
11
+ datasetInfo,
12
+ deleteFiles,
13
+ downloadFile,
14
+ listFiles,
15
+ uploadFile,
16
+ } from "@huggingface/hub";
17
+ import { z } from "zod";
18
+
19
+ import {
20
+ assertDatasetRepoReadable,
21
+ assertValidDatasetSourceContent,
22
+ DatasetMirror,
23
+ datasetSourceKind,
24
+ isHubNotFound,
25
+ } from "./dataset.js";
26
+ import type { DatasetSourceKind } from "./dataset.js";
27
+ import { EnrichStore } from "./enrich-store.js";
28
+ import { TweetStore } from "./store.js";
29
+
30
+ const INDEX_SCHEMA_VERSION = 1;
31
+ const CURRENT_MANIFEST_KEY = "index/current.json";
32
+ const DATABASE_PREFIX = "index/databases";
33
+ const RETAINED_PREDECESSORS = 3;
34
+ const MAX_PUBLICATION_ATTEMPTS = 5;
35
+ const SHA256_PATTERN = /^[a-f0-9]{64}$/;
36
+ const REVISION_PATTERN = /^[a-f0-9]{7,64}$/;
37
+ const DATABASE_KEY_PATTERN = /^index\/databases\/[a-f0-9]{64}\.sqlite$/;
38
+
39
+ const indexCountsSchema = z
40
+ .object({
41
+ tweets: z.number().int().nonnegative(),
42
+ units: z.number().int().nonnegative(),
43
+ enrichments: z.number().int().nonnegative(),
44
+ attempt_events: z.number().int().nonnegative(),
45
+ registry_events: z.number().int().nonnegative(),
46
+ })
47
+ .strict();
48
+
49
+ export const durableIndexManifestSchema = z
50
+ .object({
51
+ schema_version: z.literal(INDEX_SCHEMA_VERSION),
52
+ dataset: z
53
+ .object({ repo: z.string().min(1), revision: z.string().regex(REVISION_PATTERN) })
54
+ .strict(),
55
+ projection: z.object({ contract_hash: z.string().regex(SHA256_PATTERN) }).strict(),
56
+ database: z
57
+ .object({
58
+ key: z.string().regex(DATABASE_KEY_PATTERN),
59
+ sha256: z.string().regex(SHA256_PATTERN),
60
+ predecessors: z
61
+ .array(z.string().regex(DATABASE_KEY_PATTERN))
62
+ .max(RETAINED_PREDECESSORS)
63
+ .refine((keys) => new Set(keys).size === keys.length, "predecessors must be unique"),
64
+ })
65
+ .strict(),
66
+ counts: indexCountsSchema,
67
+ })
68
+ .strict()
69
+ .superRefine((manifest, context) => {
70
+ const expected = `${DATABASE_PREFIX}/${manifest.database.sha256}.sqlite`;
71
+ if (manifest.database.key !== expected) {
72
+ context.addIssue({
73
+ code: "custom",
74
+ path: ["database", "key"],
75
+ message: `database key must be ${expected}`,
76
+ });
77
+ }
78
+ if (manifest.database.predecessors.includes(manifest.database.key)) {
79
+ context.addIssue({
80
+ code: "custom",
81
+ path: ["database", "predecessors"],
82
+ message: "predecessors must not include the active database",
83
+ });
84
+ }
85
+ });
86
+
87
+ export type DurableIndexManifest = z.infer<typeof durableIndexManifestSchema>;
88
+ export type DurableIndexCounts = z.infer<typeof indexCountsSchema>;
89
+
90
+ export type DatasetSourceFile = {
91
+ path: string;
92
+ oid: string;
93
+ size: number;
94
+ };
95
+
96
+ export type DatasetSnapshotClient = {
97
+ currentRevision(): Promise<string>;
98
+ listJsonlFiles(revision: string): Promise<readonly DatasetSourceFile[]>;
99
+ downloadFile(path: string, revision: string): Promise<Uint8Array>;
100
+ readText(path: string, revision: string): Promise<string | undefined>;
101
+ commitText(path: string, content: string, parentRevision: string): Promise<string>;
102
+ };
103
+
104
+ export type BucketFile = { path: string; uploadedAt?: string };
105
+
106
+ export type DurableIndexBucketClient = {
107
+ download(path: string, destination: string): Promise<boolean>;
108
+ uploadFile(path: string, source: string): Promise<void>;
109
+ list(prefix: string): Promise<readonly BucketFile[]>;
110
+ remove(paths: readonly string[]): Promise<void>;
111
+ };
112
+
113
+ type SourceFileRow = {
114
+ path: string;
115
+ kind: DatasetSourceKind;
116
+ oid: string;
117
+ byte_length: number;
118
+ content_sha256: string;
119
+ row_count: number;
120
+ };
121
+
122
+ type IndexMetadataRow = {
123
+ schema_version: number;
124
+ dataset_repo: string;
125
+ dataset_revision: string;
126
+ contract_hash: string;
127
+ };
128
+
129
+ type StagedSourceFile = {
130
+ current: DatasetSourceFile;
131
+ kind: DatasetSourceKind;
132
+ fullContent: Uint8Array;
133
+ fullText: string;
134
+ suffixText: string;
135
+ contentSha256: string;
136
+ previousRows: number;
137
+ };
138
+
139
+ export type DurableIndexOptions = {
140
+ datasetRepo: string;
141
+ indexBucket: string;
142
+ accessToken: string;
143
+ databasePath: string;
144
+ mirror: DatasetMirror;
145
+ taxonomyVersion: number;
146
+ contractHash: string;
147
+ sourceClient?: DatasetSnapshotClient;
148
+ bucketClient?: DurableIndexBucketClient;
149
+ predecessorKeys?: readonly string[];
150
+ };
151
+
152
+ export type IndexAdvance = {
153
+ revision: string;
154
+ filesChanged: number;
155
+ rowsApplied: number;
156
+ counts: DurableIndexCounts;
157
+ };
158
+
159
+ export type DurableIndexStats = {
160
+ tweetFiles: number;
161
+ tweetRows: number;
162
+ enrichmentFiles: number;
163
+ enrichmentRows: number;
164
+ attemptEvents: number;
165
+ registryEvents: number;
166
+ };
167
+
168
+ /**
169
+ * Restored local SQLite projection backed by an immutable Bucket generation.
170
+ * The dataset stays authoritative; this class only advances strict append-only
171
+ * JSONL inputs and publishes verified replacement generations.
172
+ */
173
+ export class DurableIndex {
174
+ readonly store: TweetStore;
175
+ readonly enrichStore: EnrichStore;
176
+
177
+ private constructor(
178
+ private readonly options: DurableIndexOptions,
179
+ private readonly source: DatasetSnapshotClient,
180
+ private readonly bucket: DurableIndexBucketClient,
181
+ store: TweetStore,
182
+ enrichStore: EnrichStore,
183
+ private publishedKeys: string[],
184
+ ) {
185
+ this.store = store;
186
+ this.enrichStore = enrichStore;
187
+ }
188
+
189
+ static async restore(options: DurableIndexOptions): Promise<DurableIndex> {
190
+ const source =
191
+ options.sourceClient ?? createDatasetSnapshotClient(options.datasetRepo, options.accessToken);
192
+ const bucket =
193
+ options.bucketClient ??
194
+ createDurableIndexBucketClient(options.indexBucket, options.accessToken);
195
+ const manifestRevision = await source.currentRevision();
196
+ const rawManifest = await source.readText(CURRENT_MANIFEST_KEY, manifestRevision);
197
+ if (rawManifest === undefined) {
198
+ throw new Error("durable index manifest is missing; run the index bootstrap command");
199
+ }
200
+ const manifest = parseManifest(rawManifest, options);
201
+ await mkdir(dirname(options.databasePath), { recursive: true });
202
+ const staged = `${options.databasePath}.${randomUUID()}.download`;
203
+ try {
204
+ if (!(await bucket.download(manifest.database.key, staged))) {
205
+ throw new Error(`durable index database is missing: ${manifest.database.key}`);
206
+ }
207
+ await assertFileSha256(staged, manifest.database.sha256);
208
+ await removeDatabaseFiles(options.databasePath);
209
+ await rename(staged, options.databasePath);
210
+ } finally {
211
+ await rm(staged, { force: true });
212
+ }
213
+ const store = new TweetStore(options.databasePath);
214
+ const enrichStore = new EnrichStore(
215
+ store.database,
216
+ options.taxonomyVersion,
217
+ (): Date => new Date(),
218
+ options.contractHash,
219
+ );
220
+ ensureIndexTables(store.database);
221
+ validateDatabase(store.database, manifest, options);
222
+ const index = new DurableIndex(options, source, bucket, store, enrichStore, [
223
+ manifest.database.key,
224
+ ...manifest.database.predecessors,
225
+ ]);
226
+ await index.loadLatestReceipt(manifest.dataset.revision);
227
+ return index;
228
+ }
229
+
230
+ static openLocal(options: DurableIndexOptions): DurableIndex {
231
+ if (!existsSync(options.databasePath)) {
232
+ throw new Error(`durable index working database is missing: ${options.databasePath}`);
233
+ }
234
+ const source =
235
+ options.sourceClient ?? createDatasetSnapshotClient(options.datasetRepo, options.accessToken);
236
+ const bucket =
237
+ options.bucketClient ??
238
+ createDurableIndexBucketClient(options.indexBucket, options.accessToken);
239
+ const store = new TweetStore(options.databasePath);
240
+ const enrichStore = new EnrichStore(
241
+ store.database,
242
+ options.taxonomyVersion,
243
+ (): Date => new Date(),
244
+ options.contractHash,
245
+ );
246
+ ensureIndexTables(store.database);
247
+ assertDatabaseIntegrity(store.database);
248
+ const metadata = readMetadata(store.database);
249
+ if (
250
+ metadata?.schema_version !== INDEX_SCHEMA_VERSION ||
251
+ metadata.dataset_repo !== options.datasetRepo ||
252
+ metadata.contract_hash !== options.contractHash
253
+ ) {
254
+ store.close();
255
+ throw new Error("durable index working database provenance mismatch");
256
+ }
257
+ return new DurableIndex(
258
+ options,
259
+ source,
260
+ bucket,
261
+ store,
262
+ enrichStore,
263
+ configuredPredecessors(options),
264
+ );
265
+ }
266
+
267
+ static async bootstrap(options: DurableIndexOptions): Promise<DurableIndex> {
268
+ const source =
269
+ options.sourceClient ?? createDatasetSnapshotClient(options.datasetRepo, options.accessToken);
270
+ const bucket =
271
+ options.bucketClient ??
272
+ createDurableIndexBucketClient(options.indexBucket, options.accessToken);
273
+ await mkdir(dirname(options.databasePath), { recursive: true });
274
+ await removeDatabaseFiles(options.databasePath);
275
+ const store = new TweetStore(options.databasePath);
276
+ const enrichStore = new EnrichStore(
277
+ store.database,
278
+ options.taxonomyVersion,
279
+ (): Date => new Date(),
280
+ options.contractHash,
281
+ );
282
+ ensureIndexTables(store.database);
283
+ const index = new DurableIndex(options, source, bucket, store, enrichStore, []);
284
+ await index.advanceToLatest();
285
+ return index;
286
+ }
287
+
288
+ async advanceToLatest(): Promise<IndexAdvance> {
289
+ return this.advanceToRevision(await this.source.currentRevision());
290
+ }
291
+
292
+ async advanceToRevision(revision: string): Promise<IndexAdvance> {
293
+ if (!REVISION_PATTERN.test(revision)) throw new Error(`invalid dataset revision: ${revision}`);
294
+ const currentFiles = [...(await this.source.listJsonlFiles(revision))].sort((a, b) =>
295
+ sourceOrder(a.path, b.path),
296
+ );
297
+ const previous = sourceFileRows(this.store.database);
298
+ const currentPaths = new Set(currentFiles.map((file) => file.path));
299
+ const deleted = [...previous.keys()].filter((path) => !currentPaths.has(path));
300
+ if (deleted.length > 0) {
301
+ throw new Error(`dataset source files were deleted: ${deleted.slice(0, 3).join(", ")}`);
302
+ }
303
+ const staged: StagedSourceFile[] = [];
304
+ for (const file of currentFiles) {
305
+ const old = previous.get(file.path);
306
+ if (old?.oid === file.oid && old.byte_length === file.size) continue;
307
+ staged.push(await this.stageSourceFile(file, old, revision));
308
+ }
309
+
310
+ let rowsApplied = 0;
311
+ const apply = this.store.database.transaction(() => {
312
+ for (const file of staged) {
313
+ const applied = this.options.mirror.applySourceContent(
314
+ file.current.path,
315
+ file.suffixText,
316
+ this.store,
317
+ this.enrichStore,
318
+ );
319
+ if (applied.kind !== file.kind)
320
+ throw new Error(`source kind changed: ${file.current.path}`);
321
+ rowsApplied += applied.rows;
322
+ upsertSourceFile(this.store.database, {
323
+ path: file.current.path,
324
+ kind: file.kind,
325
+ oid: file.current.oid,
326
+ byte_length: file.fullContent.byteLength,
327
+ content_sha256: file.contentSha256,
328
+ row_count: file.previousRows + applied.rows,
329
+ });
330
+ }
331
+ writeMetadata(this.store.database, {
332
+ schema_version: INDEX_SCHEMA_VERSION,
333
+ dataset_repo: this.options.datasetRepo,
334
+ dataset_revision: revision,
335
+ contract_hash: this.options.contractHash,
336
+ });
337
+ });
338
+ apply();
339
+ for (const file of staged) {
340
+ this.options.mirror.rememberSourceFile(file.current.path, file.fullText);
341
+ }
342
+ await this.loadLatestReceipt(revision, currentFiles);
343
+ const counts = databaseCounts(this.store.database);
344
+ assertDatabaseIntegrity(this.store.database);
345
+ return { revision, filesChanged: staged.length, rowsApplied, counts };
346
+ }
347
+
348
+ stats(): DurableIndexStats {
349
+ const rows = this.store.database
350
+ .prepare(
351
+ `SELECT kind, COUNT(*) AS files, COALESCE(SUM(row_count), 0) AS rows
352
+ FROM source_files GROUP BY kind`,
353
+ )
354
+ .all() as { kind: DatasetSourceKind; files: number; rows: number }[];
355
+ const byKind = new Map(rows.map((row) => [row.kind, row]));
356
+ const tweets = sourceKindStats(byKind, "tweet");
357
+ const enrichments = sourceKindStats(byKind, "enrichment");
358
+ return {
359
+ tweetFiles: tweets.files,
360
+ tweetRows: tweets.rows,
361
+ enrichmentFiles: enrichments.files,
362
+ enrichmentRows: enrichments.rows,
363
+ attemptEvents: sourceKindStats(byKind, "attempt").rows,
364
+ registryEvents: sourceKindStats(byKind, "registry").rows,
365
+ };
366
+ }
367
+
368
+ retainedDatabaseKeys(): readonly string[] {
369
+ return [...this.publishedKeys];
370
+ }
371
+
372
+ async createWorkingCopy(path: string): Promise<void> {
373
+ await rm(path, { force: true });
374
+ await this.store.database.backup(path);
375
+ }
376
+
377
+ async publishLatest(): Promise<{
378
+ advance: IndexAdvance;
379
+ manifest: DurableIndexManifest;
380
+ }> {
381
+ for (let attempt = 1; attempt <= MAX_PUBLICATION_ATTEMPTS; attempt += 1) {
382
+ const advance = await this.advanceToLatest();
383
+ try {
384
+ return { advance, manifest: await this.publish() };
385
+ } catch (error) {
386
+ if (attempt === MAX_PUBLICATION_ATTEMPTS || !isConcurrentDatasetUpdate(error)) throw error;
387
+ }
388
+ }
389
+ throw new Error("durable index publication retry invariant failed");
390
+ }
391
+
392
+ async publish(): Promise<DurableIndexManifest> {
393
+ const metadata = readMetadata(this.store.database);
394
+ if (metadata === undefined) throw new Error("durable index metadata is missing");
395
+ assertDatabaseIntegrity(this.store.database);
396
+ const counts = databaseCounts(this.store.database);
397
+ const publicationPath = `${this.options.databasePath}.${randomUUID()}.publish`;
398
+ const verificationPath = `${this.options.databasePath}.${randomUUID()}.verify`;
399
+ try {
400
+ await this.store.database.backup(publicationPath);
401
+ validateStandaloneDatabase(publicationPath, metadata, counts);
402
+ const sha256 = await fileSha256(publicationPath);
403
+ const key = `${DATABASE_PREFIX}/${sha256}.sqlite`;
404
+ await this.bucket.uploadFile(key, publicationPath);
405
+ if (!(await this.bucket.download(key, verificationPath))) {
406
+ throw new Error(`uploaded durable index database is unavailable: ${key}`);
407
+ }
408
+ await assertFileSha256(verificationPath, sha256);
409
+ validateStandaloneDatabase(verificationPath, metadata, counts);
410
+ const predecessors = this.publishedKeys
411
+ .filter((publishedKey) => publishedKey !== key)
412
+ .slice(0, RETAINED_PREDECESSORS);
413
+ const manifest: DurableIndexManifest = {
414
+ schema_version: INDEX_SCHEMA_VERSION,
415
+ dataset: { repo: metadata.dataset_repo, revision: metadata.dataset_revision },
416
+ projection: { contract_hash: metadata.contract_hash },
417
+ database: { key, sha256, predecessors },
418
+ counts,
419
+ };
420
+ const encoded = `${JSON.stringify(manifest, null, 2)}\n`;
421
+ const manifestRevision = await this.source.commitText(
422
+ CURRENT_MANIFEST_KEY,
423
+ encoded,
424
+ metadata.dataset_revision,
425
+ );
426
+ const active = await this.source.readText(CURRENT_MANIFEST_KEY, manifestRevision);
427
+ if (
428
+ active === undefined ||
429
+ JSON.stringify(parseManifest(active, this.options)) !== JSON.stringify(manifest)
430
+ ) {
431
+ throw new Error("durable index manifest read-back did not match the published generation");
432
+ }
433
+ this.publishedKeys = [key, ...predecessors];
434
+ await this.pruneDatabases(this.publishedKeys);
435
+ return manifest;
436
+ } finally {
437
+ await Promise.all([
438
+ rm(publicationPath, { force: true }),
439
+ rm(verificationPath, { force: true }),
440
+ ]);
441
+ }
442
+ }
443
+
444
+ close(): void {
445
+ this.store.close();
446
+ }
447
+
448
+ private async stageSourceFile(
449
+ current: DatasetSourceFile,
450
+ previous: SourceFileRow | undefined,
451
+ revision: string,
452
+ ): Promise<StagedSourceFile> {
453
+ const kind = datasetSourceKind(current.path);
454
+ assertSourceKind(current.path, kind, previous);
455
+ const content = await this.source.downloadFile(current.path, revision);
456
+ assertCompletePinnedSource(current, content);
457
+ const suffix = sourceSuffix(current.path, content, previous);
458
+ const decoder = new TextDecoder("utf-8", { fatal: true });
459
+ const fullText = decoder.decode(content);
460
+ const suffixText = decoder.decode(suffix);
461
+ assertValidDatasetSourceContent(current.path, suffixText);
462
+ return {
463
+ current,
464
+ kind,
465
+ fullContent: content,
466
+ fullText,
467
+ suffixText,
468
+ contentSha256: sha256Bytes(content),
469
+ previousRows: previous?.row_count ?? 0,
470
+ };
471
+ }
472
+
473
+ private async loadLatestReceipt(
474
+ revision: string,
475
+ knownFiles?: readonly DatasetSourceFile[],
476
+ ): Promise<void> {
477
+ const files = knownFiles ?? (await this.source.listJsonlFiles(revision));
478
+ const latest = files
479
+ .filter((file) => datasetSourceKind(file.path) === "receipt")
480
+ .sort((a, b) => a.path.localeCompare(b.path))
481
+ .at(-1);
482
+ if (latest === undefined) return;
483
+ const content = await this.source.downloadFile(latest.path, revision);
484
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(content);
485
+ this.options.mirror.applySourceContent(latest.path, text, this.store, this.enrichStore);
486
+ this.options.mirror.rememberSourceFile(latest.path, text);
487
+ }
488
+
489
+ private async pruneDatabases(retainedKeys: readonly string[]): Promise<void> {
490
+ const files = [...(await this.bucket.list(DATABASE_PREFIX))].filter((file) =>
491
+ file.path.endsWith(".sqlite"),
492
+ );
493
+ const keep = new Set(retainedKeys);
494
+ const stale = files.map((file) => file.path).filter((path) => !keep.has(path));
495
+ if (stale.length > 0) await this.bucket.remove(stale);
496
+ }
497
+ }
498
+
499
+ function configuredPredecessors(options: DurableIndexOptions): string[] {
500
+ return Array.from(options.predecessorKeys ?? []);
501
+ }
502
+
503
+ async function removeDatabaseFiles(path: string): Promise<void> {
504
+ await Promise.all([
505
+ rm(path, { force: true }),
506
+ rm(`${path}-wal`, { force: true }),
507
+ rm(`${path}-shm`, { force: true }),
508
+ ]);
509
+ }
510
+
511
+ function isConcurrentDatasetUpdate(error: unknown): boolean {
512
+ const statusCode =
513
+ typeof error === "object" && error !== null && "statusCode" in error
514
+ ? (error as { statusCode?: unknown }).statusCode
515
+ : undefined;
516
+ const message = error instanceof Error ? error.message.toLowerCase() : "";
517
+ return (
518
+ statusCode === 409 ||
519
+ message.includes("branch was updated") ||
520
+ message.includes("parent commit does not match")
521
+ );
522
+ }
523
+
524
+ function assertSourceKind(
525
+ path: string,
526
+ kind: DatasetSourceKind,
527
+ previous: SourceFileRow | undefined,
528
+ ): void {
529
+ if (previous !== undefined && previous.kind !== kind) {
530
+ throw new Error(`dataset source kind changed: ${path}`);
531
+ }
532
+ }
533
+
534
+ function assertCompletePinnedSource(current: DatasetSourceFile, content: Uint8Array): void {
535
+ if (content.byteLength !== current.size) {
536
+ throw new Error(`dataset source size changed while pinned: ${current.path}`);
537
+ }
538
+ if (content.byteLength > 0 && content.at(-1) !== 0x0a) {
539
+ throw new Error(`dataset source does not end with a complete JSONL line: ${current.path}`);
540
+ }
541
+ }
542
+
543
+ function sourceSuffix(
544
+ path: string,
545
+ content: Uint8Array,
546
+ previous: SourceFileRow | undefined,
547
+ ): Uint8Array {
548
+ if (previous === undefined) return content;
549
+ if (content.byteLength < previous.byte_length) {
550
+ throw new Error(`dataset source was truncated: ${path}`);
551
+ }
552
+ const prefix = content.subarray(0, previous.byte_length);
553
+ if (sha256Bytes(prefix) !== previous.content_sha256) {
554
+ throw new Error(`dataset source prefix changed: ${path}`);
555
+ }
556
+ return content.subarray(previous.byte_length);
557
+ }
558
+
559
+ export function createDatasetSnapshotClient(
560
+ datasetRepo: string,
561
+ accessToken: string,
562
+ ): DatasetSnapshotClient {
563
+ const repo = { type: "dataset", name: datasetRepo } as const;
564
+ return {
565
+ async currentRevision(): Promise<string> {
566
+ const info = await datasetInfo({
567
+ name: datasetRepo,
568
+ accessToken,
569
+ additionalFields: ["sha"],
570
+ });
571
+ if (typeof info.sha !== "string" || !REVISION_PATTERN.test(info.sha)) {
572
+ throw new Error(`dataset ${datasetRepo} did not return a valid revision`);
573
+ }
574
+ return info.sha;
575
+ },
576
+ async listJsonlFiles(revision: string): Promise<readonly DatasetSourceFile[]> {
577
+ const groups = await Promise.all(
578
+ ["data", "enrichment"].map((prefix) =>
579
+ listDatasetPrefix(repo, accessToken, revision, prefix),
580
+ ),
581
+ );
582
+ return groups.flat();
583
+ },
584
+ async downloadFile(path: string, revision: string): Promise<Uint8Array> {
585
+ const blob = await downloadFile({ repo, accessToken, path, revision });
586
+ if (blob === null) throw new Error(`dataset source is missing: ${path}`);
587
+ return new Uint8Array(await blob.arrayBuffer());
588
+ },
589
+ async readText(path: string, revision: string): Promise<string | undefined> {
590
+ const blob = await downloadFile({ repo, accessToken, path, revision });
591
+ return blob === null ? undefined : blob.text();
592
+ },
593
+ async commitText(path: string, content: string, parentRevision: string): Promise<string> {
594
+ const result = await commit({
595
+ repo,
596
+ accessToken,
597
+ parentCommit: parentRevision,
598
+ title: "Publish durable enrichment index manifest",
599
+ operations: [{ operation: "addOrUpdate", path, content: new Blob([content]) }],
600
+ });
601
+ if (result === undefined || !REVISION_PATTERN.test(result.commit.oid)) {
602
+ throw new Error("dataset did not confirm the durable index manifest commit");
603
+ }
604
+ return result.commit.oid;
605
+ },
606
+ };
607
+ }
608
+
609
+ // eslint-disable-next-line complexity -- Hub entries require type, path, and immutable-object validation.
610
+ async function listDatasetPrefix(
611
+ repo: { type: "dataset"; name: string },
612
+ accessToken: string,
613
+ revision: string,
614
+ prefix: string,
615
+ ): Promise<DatasetSourceFile[]> {
616
+ const files: DatasetSourceFile[] = [];
617
+ try {
618
+ for await (const entry of listFiles({
619
+ repo,
620
+ accessToken,
621
+ recursive: true,
622
+ path: prefix,
623
+ revision,
624
+ })) {
625
+ if (entry.type !== "file" || !entry.path.endsWith(".jsonl")) continue;
626
+ const oid = entry.xetHash ?? entry.lfs?.oid ?? entry.oid;
627
+ if (oid === undefined || oid.length === 0) {
628
+ throw new Error(`dataset source has no immutable object id: ${entry.path}`);
629
+ }
630
+ files.push({ path: entry.path, oid, size: entry.size });
631
+ }
632
+ } catch (error) {
633
+ if (!isHubNotFound(error)) throw error;
634
+ await assertDatasetRepoReadable(repo, accessToken, revision);
635
+ }
636
+ return files;
637
+ }
638
+
639
+ export function createDurableIndexBucketClient(
640
+ indexBucket: string,
641
+ accessToken: string,
642
+ ): DurableIndexBucketClient {
643
+ const repo = { type: "bucket", name: indexBucket } as const;
644
+ return {
645
+ async download(path: string, destination: string): Promise<boolean> {
646
+ const blob = await downloadFile({ repo, accessToken, path });
647
+ if (blob === null) return false;
648
+ await mkdir(dirname(destination), { recursive: true });
649
+ await pipeline(Readable.fromWeb(blob.stream()), createWriteStream(destination));
650
+ return true;
651
+ },
652
+ async uploadFile(path: string, source: string): Promise<void> {
653
+ await uploadFile({
654
+ repo,
655
+ accessToken,
656
+ file: { path, content: await openAsBlob(source) },
657
+ commitTitle: `Publish ${path}`,
658
+ });
659
+ },
660
+ async list(prefix: string): Promise<readonly BucketFile[]> {
661
+ const files: BucketFile[] = [];
662
+ for await (const entry of listFiles({
663
+ repo,
664
+ accessToken,
665
+ recursive: true,
666
+ path: prefix,
667
+ expand: true,
668
+ })) {
669
+ if (entry.type === "file") {
670
+ files.push({
671
+ path: entry.path,
672
+ ...(entry.uploadedAt === undefined ? {} : { uploadedAt: entry.uploadedAt }),
673
+ });
674
+ }
675
+ }
676
+ return files;
677
+ },
678
+ async remove(paths: readonly string[]): Promise<void> {
679
+ if (paths.length === 0) return;
680
+ await deleteFiles({
681
+ repo,
682
+ accessToken,
683
+ paths: [...paths],
684
+ commitTitle: "Prune durable index generations",
685
+ });
686
+ },
687
+ };
688
+ }
689
+
690
+ function parseManifest(
691
+ raw: string,
692
+ options: Pick<DurableIndexOptions, "datasetRepo" | "contractHash">,
693
+ ): DurableIndexManifest {
694
+ let candidate: unknown;
695
+ try {
696
+ candidate = JSON.parse(raw);
697
+ } catch {
698
+ throw new Error("durable index manifest is not valid JSON");
699
+ }
700
+ const manifest = durableIndexManifestSchema.parse(candidate);
701
+ if (manifest.dataset.repo !== options.datasetRepo) {
702
+ throw new Error(`durable index dataset mismatch: ${manifest.dataset.repo}`);
703
+ }
704
+ if (manifest.projection.contract_hash !== options.contractHash) {
705
+ throw new Error("durable index enrichment contract does not match the running code");
706
+ }
707
+ return manifest;
708
+ }
709
+
710
+ function ensureIndexTables(db: Database.Database): void {
711
+ db.exec(`
712
+ CREATE TABLE IF NOT EXISTS index_metadata (
713
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
714
+ schema_version INTEGER NOT NULL,
715
+ dataset_repo TEXT NOT NULL,
716
+ dataset_revision TEXT NOT NULL,
717
+ contract_hash TEXT NOT NULL
718
+ );
719
+ CREATE TABLE IF NOT EXISTS source_files (
720
+ path TEXT PRIMARY KEY,
721
+ kind TEXT NOT NULL,
722
+ oid TEXT NOT NULL,
723
+ byte_length INTEGER NOT NULL CHECK (byte_length >= 0),
724
+ content_sha256 TEXT NOT NULL,
725
+ row_count INTEGER NOT NULL CHECK (row_count >= 0)
726
+ );
727
+ `);
728
+ }
729
+
730
+ function sourceFileRows(db: Database.Database): Map<string, SourceFileRow> {
731
+ const rows = db
732
+ .prepare(
733
+ `SELECT path, kind, oid, byte_length, content_sha256, row_count
734
+ FROM source_files ORDER BY path`,
735
+ )
736
+ .all() as SourceFileRow[];
737
+ return new Map(rows.map((row) => [row.path, row]));
738
+ }
739
+
740
+ function upsertSourceFile(db: Database.Database, row: SourceFileRow): void {
741
+ db.prepare(
742
+ `INSERT INTO source_files
743
+ (path, kind, oid, byte_length, content_sha256, row_count)
744
+ VALUES (?, ?, ?, ?, ?, ?)
745
+ ON CONFLICT(path) DO UPDATE SET
746
+ kind = excluded.kind,
747
+ oid = excluded.oid,
748
+ byte_length = excluded.byte_length,
749
+ content_sha256 = excluded.content_sha256,
750
+ row_count = excluded.row_count`,
751
+ ).run(row.path, row.kind, row.oid, row.byte_length, row.content_sha256, row.row_count);
752
+ }
753
+
754
+ function writeMetadata(db: Database.Database, metadata: IndexMetadataRow): void {
755
+ db.prepare(
756
+ `INSERT INTO index_metadata
757
+ (singleton, schema_version, dataset_repo, dataset_revision, contract_hash)
758
+ VALUES (1, ?, ?, ?, ?)
759
+ ON CONFLICT(singleton) DO UPDATE SET
760
+ schema_version = excluded.schema_version,
761
+ dataset_repo = excluded.dataset_repo,
762
+ dataset_revision = excluded.dataset_revision,
763
+ contract_hash = excluded.contract_hash`,
764
+ ).run(
765
+ metadata.schema_version,
766
+ metadata.dataset_repo,
767
+ metadata.dataset_revision,
768
+ metadata.contract_hash,
769
+ );
770
+ }
771
+
772
+ function readMetadata(db: Database.Database): IndexMetadataRow | undefined {
773
+ return db
774
+ .prepare(
775
+ `SELECT schema_version, dataset_repo, dataset_revision, contract_hash
776
+ FROM index_metadata WHERE singleton = 1`,
777
+ )
778
+ .get() as IndexMetadataRow | undefined;
779
+ }
780
+
781
+ function sourceKindStats(
782
+ byKind: ReadonlyMap<DatasetSourceKind, { files: number; rows: number }>,
783
+ kind: DatasetSourceKind,
784
+ ): { files: number; rows: number } {
785
+ return byKind.get(kind) ?? { files: 0, rows: 0 };
786
+ }
787
+
788
+ function databaseCounts(db: Database.Database): DurableIndexCounts {
789
+ const count = (table: string): number =>
790
+ (db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get() as { n: number }).n;
791
+ const eventCount = (kind: DatasetSourceKind): number =>
792
+ (
793
+ db
794
+ .prepare("SELECT COALESCE(SUM(row_count), 0) AS n FROM source_files WHERE kind = ?")
795
+ .get(kind) as {
796
+ n: number;
797
+ }
798
+ ).n;
799
+ return {
800
+ tweets: count("tweets"),
801
+ units: count("enrich_queue"),
802
+ enrichments: count("enrichment"),
803
+ attempt_events: eventCount("attempt"),
804
+ registry_events: eventCount("registry"),
805
+ };
806
+ }
807
+
808
+ function validateDatabase(
809
+ db: Database.Database,
810
+ manifest: DurableIndexManifest,
811
+ options: Pick<DurableIndexOptions, "datasetRepo" | "contractHash">,
812
+ ): void {
813
+ assertDatabaseIntegrity(db);
814
+ const metadata = readMetadata(db);
815
+ if (metadata === undefined) throw new Error("durable index metadata is missing");
816
+ if (metadata.schema_version !== INDEX_SCHEMA_VERSION)
817
+ throw new Error("durable index schema version mismatch");
818
+ if (
819
+ metadata.dataset_repo !== options.datasetRepo ||
820
+ metadata.dataset_revision !== manifest.dataset.revision
821
+ ) {
822
+ throw new Error("durable index dataset provenance mismatch");
823
+ }
824
+ if (metadata.contract_hash !== options.contractHash)
825
+ throw new Error("durable index contract mismatch");
826
+ if (JSON.stringify(databaseCounts(db)) !== JSON.stringify(manifest.counts)) {
827
+ throw new Error("durable index physical counts do not match the manifest");
828
+ }
829
+ }
830
+
831
+ function validateStandaloneDatabase(
832
+ path: string,
833
+ metadata: IndexMetadataRow,
834
+ counts: DurableIndexCounts,
835
+ ): void {
836
+ const db = new Database(path, { readonly: true, fileMustExist: true });
837
+ try {
838
+ db.pragma("query_only = ON");
839
+ assertDatabaseIntegrity(db);
840
+ if (JSON.stringify(readMetadata(db)) !== JSON.stringify(metadata)) {
841
+ throw new Error("published durable index metadata does not match");
842
+ }
843
+ if (JSON.stringify(databaseCounts(db)) !== JSON.stringify(counts)) {
844
+ throw new Error("published durable index counts do not match");
845
+ }
846
+ } finally {
847
+ db.close();
848
+ }
849
+ }
850
+
851
+ function assertDatabaseIntegrity(db: Database.Database): void {
852
+ const rows = db.pragma("quick_check") as { quick_check: string }[];
853
+ if (rows.length !== 1 || rows[0]?.quick_check !== "ok") {
854
+ throw new Error("durable index SQLite quick_check failed");
855
+ }
856
+ }
857
+
858
+ function sourceOrder(left: string, right: string): number {
859
+ const priorities: Record<DatasetSourceKind, number> = {
860
+ tweet: 0,
861
+ enrichment: 1,
862
+ registry: 2,
863
+ attempt: 3,
864
+ receipt: 4,
865
+ };
866
+ const difference = priorities[datasetSourceKind(left)] - priorities[datasetSourceKind(right)];
867
+ return difference === 0 ? left.localeCompare(right) : difference;
868
+ }
869
+
870
+ async function fileSha256(path: string): Promise<string> {
871
+ const hash = createHash("sha256");
872
+ for await (const chunk of createReadStream(path)) {
873
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as Uint8Array);
874
+ hash.update(bytes);
875
+ }
876
+ return hash.digest("hex");
877
+ }
878
+
879
+ async function assertFileSha256(path: string, expected: string): Promise<void> {
880
+ if ((await fileSha256(path)) !== expected) {
881
+ throw new Error("durable index database checksum mismatch");
882
+ }
883
+ }
884
+
885
+ function sha256Bytes(content: Uint8Array): string {
886
+ return createHash("sha256").update(content).digest("hex");
887
+ }
space/src/enrich-command.ts CHANGED
@@ -1,9 +1,10 @@
 
1
  import { join } from "node:path";
2
 
3
  import { loadConfig } from "./config.js";
4
  import { createHubClient, DatasetMirror } from "./dataset.js";
5
  import { loadEnrichTaxonomy } from "./enrich-config.js";
6
- import { EnrichStore } from "./enrich-store.js";
7
  import {
8
  contractHashFor,
9
  createExactHubVerifier,
@@ -14,7 +15,6 @@ import {
14
  runEnrichTick,
15
  } from "./enrich-worker.js";
16
  import type { WorkerCeilings } from "./enrich-worker.js";
17
- import { TweetStore } from "./store.js";
18
 
19
  /**
20
  * Standalone worker: rebuilds the local mirror from the dataset (system of
@@ -35,10 +35,8 @@ export async function runEnrichCommand(env: Record<string, string | undefined>):
35
  process.exitCode = 2;
36
  return;
37
  }
38
- const store = new TweetStore();
39
  const hub = createHubClient(config.datasetRepo, config.hfToken);
40
  const mirror = new DatasetMirror(hub, join(config.dataDir, "mirror"));
41
- const enrichStore = new EnrichStore(store.database, config.taxonomyVersion, () => new Date());
42
  const taxonomy = await loadEnrichTaxonomy(mirror, config.taxonomyVersion);
43
  if (taxonomy.error !== undefined) {
44
  console.error(`[xtap-pool worker] taxonomy unavailable: ${taxonomy.error}`);
@@ -46,19 +44,30 @@ export async function runEnrichCommand(env: Record<string, string | undefined>):
46
  return;
47
  }
48
  const contractHash = contractHashFor({ taxonomy, model: config.llmModel });
49
- enrichStore.setContractHash(contractHash);
50
 
51
  console.log(
52
- `[xtap-pool worker] rebuilding index from ${config.datasetRepo}, contract ${contractHash.slice(0, 12)} ...`,
53
  );
54
- mirror.clearForRebuild();
55
- const tweetStats = await mirror.rebuild(store, enrichStore);
56
- const enrichStats = await mirror.rebuildEnrichment(enrichStore);
57
- enrichStore.releaseClaims();
 
 
 
 
 
 
 
 
58
  console.log(
59
- `[xtap-pool worker] indexed ${String(tweetStats.tweets)} tweets, ` +
60
- `${String(enrichStats.rows)} enrichment rows, ${String(enrichStats.attempts)} attempt events`,
 
61
  );
 
 
 
62
 
63
  const llm = createRouterLlmClient({
64
  hfToken: config.inferenceToken,
@@ -82,7 +91,7 @@ export async function runEnrichCommand(env: Record<string, string | undefined>):
82
  discardedAssignmentRateMinUnits: config.enrichDiscardedAssignmentRateMinUnits,
83
  };
84
  const receipt = await runEnrichTick({
85
- enrichStore,
86
  mirror,
87
  taxonomy,
88
  llm,
@@ -95,6 +104,25 @@ export async function runEnrichCommand(env: Record<string, string | undefined>):
95
  now: (): Date => new Date(),
96
  ceilings,
97
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  console.log(
99
  `[xtap-pool worker] finished: units=${String(receipt.units)} retries=${String(receipt.retries)} ` +
100
  `blocked=${String(receipt.blocked)} calls=${String(receipt.calls)} ` +
@@ -103,7 +131,6 @@ export async function runEnrichCommand(env: Record<string, string | undefined>):
103
  `tokens=${String(receipt.prompt_tokens + receipt.completion_tokens)} ` +
104
  `stopped_by=${receipt.stopped_by ?? "batch-complete"}`,
105
  );
106
- store.close();
107
  }
108
 
109
  /**
 
1
+ import { rm } from "node:fs/promises";
2
  import { join } from "node:path";
3
 
4
  import { loadConfig } from "./config.js";
5
  import { createHubClient, DatasetMirror } from "./dataset.js";
6
  import { loadEnrichTaxonomy } from "./enrich-config.js";
7
+ import { DurableIndex } from "./durable-index.js";
8
  import {
9
  contractHashFor,
10
  createExactHubVerifier,
 
15
  runEnrichTick,
16
  } from "./enrich-worker.js";
17
  import type { WorkerCeilings } from "./enrich-worker.js";
 
18
 
19
  /**
20
  * Standalone worker: rebuilds the local mirror from the dataset (system of
 
35
  process.exitCode = 2;
36
  return;
37
  }
 
38
  const hub = createHubClient(config.datasetRepo, config.hfToken);
39
  const mirror = new DatasetMirror(hub, join(config.dataDir, "mirror"));
 
40
  const taxonomy = await loadEnrichTaxonomy(mirror, config.taxonomyVersion);
41
  if (taxonomy.error !== undefined) {
42
  console.error(`[xtap-pool worker] taxonomy unavailable: ${taxonomy.error}`);
 
44
  return;
45
  }
46
  const contractHash = contractHashFor({ taxonomy, model: config.llmModel });
 
47
 
48
  console.log(
49
+ `[xtap-pool worker] restoring durable index from ${config.indexBucket}, contract ${contractHash.slice(0, 12)} ...`,
50
  );
51
+ const indexOptions = {
52
+ datasetRepo: config.datasetRepo,
53
+ indexBucket: config.indexBucket,
54
+ accessToken: config.hfToken,
55
+ databasePath: join(config.dataDir, "index", "worker.sqlite"),
56
+ mirror,
57
+ taxonomyVersion: config.taxonomyVersion,
58
+ contractHash,
59
+ };
60
+ const index = await DurableIndex.restore(indexOptions);
61
+ const advanced = await index.advanceToLatest();
62
+ index.enrichStore.releaseClaims();
63
  console.log(
64
+ `[xtap-pool worker] index revision ${advanced.revision.slice(0, 12)}; ` +
65
+ `changed_files=${String(advanced.filesChanged)} rows=${String(advanced.rowsApplied)} ` +
66
+ `tweets=${String(advanced.counts.tweets)} units=${String(advanced.counts.units)}`,
67
  );
68
+ const publicationBase = join(config.dataDir, "index", "publication-base.sqlite");
69
+ await index.createWorkingCopy(publicationBase);
70
+ const predecessorKeys = index.retainedDatabaseKeys();
71
 
72
  const llm = createRouterLlmClient({
73
  hfToken: config.inferenceToken,
 
91
  discardedAssignmentRateMinUnits: config.enrichDiscardedAssignmentRateMinUnits,
92
  };
93
  const receipt = await runEnrichTick({
94
+ enrichStore: index.enrichStore,
95
  mirror,
96
  taxonomy,
97
  llm,
 
104
  now: (): Date => new Date(),
105
  ceilings,
106
  });
107
+ index.close();
108
+ const publicationMirror = new DatasetMirror(hub, join(config.dataDir, "publication-mirror"));
109
+ const publication = DurableIndex.openLocal({
110
+ ...indexOptions,
111
+ databasePath: publicationBase,
112
+ mirror: publicationMirror,
113
+ predecessorKeys,
114
+ });
115
+ try {
116
+ const { advance: finalAdvance, manifest } = await publication.publishLatest();
117
+ console.log(
118
+ `[xtap-pool worker] published index ${manifest.database.sha256.slice(0, 12)} at ` +
119
+ `${finalAdvance.revision.slice(0, 12)}; changed_files=${String(finalAdvance.filesChanged)} ` +
120
+ `rows=${String(finalAdvance.rowsApplied)}`,
121
+ );
122
+ } finally {
123
+ publication.close();
124
+ await rm(publicationBase, { force: true });
125
+ }
126
  console.log(
127
  `[xtap-pool worker] finished: units=${String(receipt.units)} retries=${String(receipt.retries)} ` +
128
  `blocked=${String(receipt.blocked)} calls=${String(receipt.calls)} ` +
 
131
  `tokens=${String(receipt.prompt_tokens + receipt.completion_tokens)} ` +
132
  `stopped_by=${receipt.stopped_by ?? "batch-complete"}`,
133
  );
 
134
  }
135
 
136
  /**
space/src/index-command-main.ts ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import { runIndexCommand } from "./index-command.js";
2
+
3
+ try {
4
+ await runIndexCommand(process.env);
5
+ } catch (error) {
6
+ const message = error instanceof Error ? error.message : "unknown error";
7
+ console.error(`[xtap-pool index] fatal: ${message}`);
8
+ process.exitCode = 1;
9
+ }
space/src/index-command.ts ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { join } from "node:path";
2
+
3
+ import { DEFAULT_ENRICHMENT_MODEL } from "@xtap-pool/shared";
4
+ import { z } from "zod";
5
+
6
+ import { createHubClient, DatasetMirror } from "./dataset.js";
7
+ import { DurableIndex } from "./durable-index.js";
8
+ import { loadEnrichTaxonomy } from "./enrich-config.js";
9
+ import { contractHashFor } from "./enrich-worker.js";
10
+
11
+ const indexCommandConfigSchema = z.object({
12
+ DATA_DIR: z.string().default(".data"),
13
+ DATASET_REPO: z.string().min(1),
14
+ INDEX_BUCKET: z.string().min(1),
15
+ HF_TOKEN: z.string().min(1),
16
+ LLM_MODEL: z.string().min(1).default(DEFAULT_ENRICHMENT_MODEL),
17
+ TAXONOMY_VERSION: z.coerce.number().int().min(1).default(1),
18
+ });
19
+
20
+ /** Explicit full replay used to seed or repair the durable enrichment index. */
21
+ export async function runIndexCommand(env: Record<string, string | undefined>): Promise<void> {
22
+ const config = indexCommandConfigSchema.parse(env);
23
+ const hub = createHubClient(config.DATASET_REPO, config.HF_TOKEN);
24
+ const mirror = new DatasetMirror(hub, join(config.DATA_DIR, "index-bootstrap-mirror"));
25
+ const taxonomy = await loadEnrichTaxonomy(mirror, config.TAXONOMY_VERSION);
26
+ if (taxonomy.error !== undefined) {
27
+ throw new Error(`enrichment taxonomy unavailable: ${taxonomy.error}`);
28
+ }
29
+ const contractHash = contractHashFor({ taxonomy, model: config.LLM_MODEL });
30
+ const index = await DurableIndex.bootstrap({
31
+ datasetRepo: config.DATASET_REPO,
32
+ indexBucket: config.INDEX_BUCKET,
33
+ accessToken: config.HF_TOKEN,
34
+ databasePath: join(config.DATA_DIR, "index", "bootstrap.sqlite"),
35
+ mirror,
36
+ taxonomyVersion: config.TAXONOMY_VERSION,
37
+ contractHash,
38
+ });
39
+ try {
40
+ const { manifest } = await index.publishLatest();
41
+ const stats = index.stats();
42
+ console.log(
43
+ `[xtap-pool index] published ${manifest.database.sha256} at ` +
44
+ `${manifest.dataset.revision}; tweets=${String(stats.tweetRows)} ` +
45
+ `enrichments=${String(stats.enrichmentRows)} attempts=${String(stats.attemptEvents)}`,
46
+ );
47
+ } finally {
48
+ index.close();
49
+ }
50
+ }
space/src/server.ts CHANGED
@@ -7,25 +7,20 @@ import { createApp } from "./app.js";
7
  import type { AppReadiness } from "./app.js";
8
  import { loadConfig } from "./config.js";
9
  import { createHubClient, DatasetMirror } from "./dataset.js";
10
- import { datasetStateFromRebuildError, errorStatus } from "./dataset-state.js";
11
  import type { DatasetState } from "./dataset-state.js";
12
  import { checkDatasetCredential, datasetCredentialOk } from "./dataset-token.js";
13
  import type { DatasetCredentialReadiness } from "./dataset-token.js";
14
  import { loadEnrichTaxonomy } from "./enrich-config.js";
15
- import { EnrichStore } from "./enrich-store.js";
16
  import { contractHashFor } from "./enrich-worker.js";
17
  import { ingestBatch, Mutex } from "./ingest.js";
18
  import { checkInferenceCredential, inferenceCredentialOk } from "./inference-token.js";
19
  import type { InferenceCredentialReadiness } from "./inference-token.js";
20
  import { PoolMembership } from "./membership.js";
21
  import { ServiceAccountRegistry } from "./service-accounts.js";
22
- import { TweetStore } from "./store.js";
23
  import { UnitStore } from "./unit-store.js";
24
 
25
  const config = loadConfig(process.env);
26
- const store = new TweetStore();
27
- const enrichStore = new EnrichStore(store.database, config.taxonomyVersion);
28
- const unitStore = new UnitStore(store.database, config.taxonomyVersion);
29
  const hub = createHubClient(config.datasetRepo, config.hfToken);
30
  const mirror = new DatasetMirror(hub, join(config.dataDir, "mirror"));
31
  const mutex = new Mutex();
@@ -37,7 +32,7 @@ let rebuilt: RebuildStats = { files: 0, tweets: 0 };
37
  let enrichment: EnrichmentStats = { files: 0, rows: 0 };
38
  let datasetState: DatasetState = {
39
  state: "unknown",
40
- error: "The dataset index has not been built yet.",
41
  };
42
  let datasetCredential: DatasetCredentialReadiness = {
43
  credential: "unknown",
@@ -49,14 +44,40 @@ let inferenceCredential: InferenceCredentialReadiness = config.enrichEnabled
49
  let readiness: AppReadiness;
50
  let credentialRetryTimer: ReturnType<typeof setTimeout> | undefined;
51
  let enrichmentRefreshTimer: ReturnType<typeof setTimeout> | undefined;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
  [datasetCredential, inferenceCredential] = await Promise.all([
54
- checkDatasetCredential({ token: config.hfToken, datasetRepo: config.datasetRepo }),
 
 
 
 
55
  checkInferenceCredential({
56
  enabled: config.enrichEnabled,
57
  token: config.inferenceToken,
58
  }),
59
  ]);
 
60
 
61
  const [membership, serviceAccounts] = await Promise.all([
62
  PoolMembership.load({
@@ -68,8 +89,23 @@ const [membership, serviceAccounts] = await Promise.all([
68
  ServiceAccountRegistry.load({ mirror, now: () => new Date() }),
69
  ]);
70
  let taxonomy = await loadEnrichTaxonomy(mirror, config.taxonomyVersion);
71
- enrichStore.setContractHash(contractHashFor({ taxonomy, model: config.llmModel }));
72
- readiness = buildReadiness();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  let lastReceipt: import("@xtap-pool/shared").EnrichReceipt | undefined;
74
  function recordLastReceipt(receipt: import("@xtap-pool/shared").EnrichReceipt | undefined): void {
75
  if (receipt?.contract_hash !== enrichStore.currentContractHash()) return;
@@ -77,6 +113,10 @@ function recordLastReceipt(receipt: import("@xtap-pool/shared").EnrichReceipt |
77
  lastReceipt = receipt;
78
  }
79
  }
 
 
 
 
80
  const app = createApp({
81
  config,
82
  store,
@@ -91,9 +131,25 @@ const app = createApp({
91
  lastReceipt: () => lastReceipt,
92
  },
93
  ingest: (username, payload) =>
94
- mutex.run(() =>
95
- ingestBatch({ store, mirror, enrich: enrichStore, now: () => new Date() }, username, payload),
96
- ),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  repairMembership: (actor) =>
98
  mutex.run(async () => {
99
  const pool = await membership.repairConfig(actor);
@@ -113,8 +169,11 @@ const app = createApp({
113
  app.use("*", serveStatic({ root: config.staticRoot }));
114
  app.use("*", serveStatic({ root: config.staticRoot, path: "index.html" }));
115
 
116
- console.log(`[xtap-pool] rebuilding index from ${config.datasetRepo} ...`);
117
- await rebuildDatasetIndexIfReady();
 
 
 
118
  readiness = buildReadiness();
119
  const pool = membership.snapshot();
120
  console.log(
@@ -135,37 +194,15 @@ if (config.enrichEnabled) {
135
  "to drain the queue.",
136
  );
137
  }
 
138
  startCredentialRetryIfNeeded();
139
  startEnrichmentRefresh();
140
 
141
- serve({ fetch: app.fetch, port: config.port }, (info) => {
142
- console.log(`[xtap-pool] listening on :${String(info.port)}`);
143
- });
144
-
145
- async function rebuildDatasetIndexIfReady(): Promise<void> {
146
- if (!datasetCredentialOk(datasetCredential)) return;
147
- datasetState = { state: "unknown", error: "Rebuilding the dataset index." };
148
- readiness = buildReadiness();
149
- mirror.clearForRebuild();
150
- enrichStore.clearForRebuild();
151
- store.clearForRebuild();
152
- rebuilt = { files: 0, tweets: 0 };
153
- enrichment = { files: 0, rows: 0 };
154
- try {
155
- rebuilt = await mirror.rebuild(store, enrichStore);
156
- enrichment = await mirror.rebuildEnrichment(enrichStore);
157
- recordLastReceipt(mirror.latestReceipt());
158
- enrichStore.releaseClaims();
159
- datasetState = { state: "ready" };
160
- } catch (error) {
161
- const status = errorStatus(error);
162
- if (status === 401 || status === 403) {
163
- datasetCredential = { credential: "invalid", error: errorMessage(error) };
164
- datasetState = { state: "unknown", error: "Dataset indexing requires a valid HF_TOKEN." };
165
- } else {
166
- datasetState = datasetStateFromRebuildError(error);
167
- }
168
- }
169
  }
170
 
171
  /**
@@ -179,30 +216,31 @@ function startEnrichmentRefresh(): void {
179
  enrichmentRefreshTimer = undefined;
180
  void refreshExternalEnrichment()
181
  .catch((error: unknown) => {
182
- console.error(`[xtap-pool] enrichment refresh failed: ${errorMessage(error)}`);
 
 
 
183
  })
184
  .finally(startEnrichmentRefresh);
185
  }, enrichmentRefreshMs());
186
  }
187
 
188
  async function refreshExternalEnrichment(): Promise<void> {
189
- if (!datasetCredentialOk(datasetCredential) || datasetState.state !== "ready") return;
190
  await mutex.run(async () => {
191
- // A rebuild may have changed readiness while this refresh was waiting.
192
- if (!datasetCredentialOk(datasetCredential) || datasetState.state !== "ready") return;
193
  const nextTaxonomy = await loadEnrichTaxonomy(mirror, config.taxonomyVersion);
194
  if (nextTaxonomy.error !== undefined) {
195
  throw new Error(`enrichment taxonomy refresh failed: ${nextTaxonomy.error}`);
196
  }
197
- const refreshed = await mirror.refreshEnrichment(enrichStore, () => {
198
- taxonomy = nextTaxonomy;
199
- enrichStore.setContractHash(contractHashFor({ taxonomy, model: config.llmModel }));
200
- if (lastReceipt?.contract_hash !== enrichStore.currentContractHash()) {
201
- lastReceipt = undefined;
202
- }
203
- });
204
- recordLastReceipt(refreshed.receipt);
205
- enrichment = { ...enrichment, rows: enrichStore.enrichmentRowCount() };
206
  readiness = buildReadiness();
207
  });
208
  }
@@ -296,12 +334,17 @@ function taxonomyReady(): boolean {
296
  return taxonomy.error === undefined;
297
  }
298
 
 
299
  async function reloadDatasetBackedConfig(force: boolean): Promise<void> {
300
  if (force || membership.hasRetryableConfigError()) await membership.reload();
301
  if (force || serviceAccounts.hasRetryableConfigError()) await serviceAccounts.reload();
302
  if (config.enrichEnabled && (force || !taxonomyReady())) {
303
- taxonomy = await loadEnrichTaxonomy(mirror, config.taxonomyVersion);
304
- enrichStore.setContractHash(contractHashFor({ taxonomy, model: config.llmModel }));
 
 
 
 
305
  }
306
  }
307
 
@@ -341,6 +384,7 @@ async function retryUncertainCredentials(): Promise<void> {
341
  datasetCredential = await checkDatasetCredential({
342
  token: config.hfToken,
343
  datasetRepo: config.datasetRepo,
 
344
  });
345
  }
346
  const datasetRecovered = !datasetWasReady && datasetCredentialOk(datasetCredential);
@@ -348,7 +392,9 @@ async function retryUncertainCredentials(): Promise<void> {
348
  await mutex.run(async () => {
349
  await reloadDatasetBackedConfig(datasetRecovered);
350
  if (datasetRecovered || datasetState.state === "unknown") {
351
- await rebuildDatasetIndexIfReady();
 
 
352
  }
353
  });
354
  }
@@ -361,6 +407,22 @@ async function retryUncertainCredentials(): Promise<void> {
361
  readiness = buildReadiness();
362
  }
363
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
  function credentialRetryMs(): number {
365
  return Math.min(Math.max(config.enrichIntervalMs, 5000), 60000);
366
  }
 
7
  import type { AppReadiness } from "./app.js";
8
  import { loadConfig } from "./config.js";
9
  import { createHubClient, DatasetMirror } from "./dataset.js";
 
10
  import type { DatasetState } from "./dataset-state.js";
11
  import { checkDatasetCredential, datasetCredentialOk } from "./dataset-token.js";
12
  import type { DatasetCredentialReadiness } from "./dataset-token.js";
13
  import { loadEnrichTaxonomy } from "./enrich-config.js";
14
+ import { DurableIndex } from "./durable-index.js";
15
  import { contractHashFor } from "./enrich-worker.js";
16
  import { ingestBatch, Mutex } from "./ingest.js";
17
  import { checkInferenceCredential, inferenceCredentialOk } from "./inference-token.js";
18
  import type { InferenceCredentialReadiness } from "./inference-token.js";
19
  import { PoolMembership } from "./membership.js";
20
  import { ServiceAccountRegistry } from "./service-accounts.js";
 
21
  import { UnitStore } from "./unit-store.js";
22
 
23
  const config = loadConfig(process.env);
 
 
 
24
  const hub = createHubClient(config.datasetRepo, config.hfToken);
25
  const mirror = new DatasetMirror(hub, join(config.dataDir, "mirror"));
26
  const mutex = new Mutex();
 
32
  let enrichment: EnrichmentStats = { files: 0, rows: 0 };
33
  let datasetState: DatasetState = {
34
  state: "unknown",
35
+ error: "The durable dataset index has not been restored yet.",
36
  };
37
  let datasetCredential: DatasetCredentialReadiness = {
38
  credential: "unknown",
 
44
  let readiness: AppReadiness;
45
  let credentialRetryTimer: ReturnType<typeof setTimeout> | undefined;
46
  let enrichmentRefreshTimer: ReturnType<typeof setTimeout> | undefined;
47
+ let activeFetch: (request: Request) => Response | Promise<Response> = () =>
48
+ Response.json(
49
+ {
50
+ ok: false,
51
+ dataset: {
52
+ credential: datasetCredential.credential,
53
+ state: datasetCredential.credential === "invalid" ? "invalid" : "unknown",
54
+ indexed_files: 0,
55
+ indexed_tweets: 0,
56
+ enrichment_rows: 0,
57
+ ...(datasetCredential.credential === "ok"
58
+ ? {}
59
+ : { credential_error: datasetCredential.error }),
60
+ error: datasetState.state === "ready" ? undefined : datasetState.error,
61
+ },
62
+ },
63
+ { status: 503 },
64
+ );
65
+ serve({ fetch: (request: Request) => activeFetch(request), port: config.port }, (info) => {
66
+ console.log(`[xtap-pool] listening on :${String(info.port)}`);
67
+ });
68
 
69
  [datasetCredential, inferenceCredential] = await Promise.all([
70
+ checkDatasetCredential({
71
+ token: config.hfToken,
72
+ datasetRepo: config.datasetRepo,
73
+ indexBucket: config.indexBucket,
74
+ }),
75
  checkInferenceCredential({
76
  enabled: config.enrichEnabled,
77
  token: config.inferenceToken,
78
  }),
79
  ]);
80
+ await waitForStorageCredential();
81
 
82
  const [membership, serviceAccounts] = await Promise.all([
83
  PoolMembership.load({
 
89
  ServiceAccountRegistry.load({ mirror, now: () => new Date() }),
90
  ]);
91
  let taxonomy = await loadEnrichTaxonomy(mirror, config.taxonomyVersion);
92
+ if (taxonomy.error !== undefined) {
93
+ throw new Error(`enrichment taxonomy unavailable: ${taxonomy.error}`);
94
+ }
95
+ const contractHash = contractHashFor({ taxonomy, model: config.llmModel });
96
+ const index = await DurableIndex.restore({
97
+ datasetRepo: config.datasetRepo,
98
+ indexBucket: config.indexBucket,
99
+ accessToken: config.hfToken,
100
+ databasePath: join(config.dataDir, "index", "space.sqlite"),
101
+ mirror,
102
+ taxonomyVersion: config.taxonomyVersion,
103
+ contractHash,
104
+ });
105
+ const initialAdvance = await index.advanceToLatest();
106
+ const store = index.store;
107
+ const enrichStore = index.enrichStore;
108
+ const unitStore = new UnitStore(store.database, config.taxonomyVersion);
109
  let lastReceipt: import("@xtap-pool/shared").EnrichReceipt | undefined;
110
  function recordLastReceipt(receipt: import("@xtap-pool/shared").EnrichReceipt | undefined): void {
111
  if (receipt?.contract_hash !== enrichStore.currentContractHash()) return;
 
113
  lastReceipt = receipt;
114
  }
115
  }
116
+ applyIndexStats();
117
+ datasetState = { state: "ready" };
118
+ enrichStore.releaseClaims();
119
+ readiness = buildReadiness();
120
  const app = createApp({
121
  config,
122
  store,
 
131
  lastReceipt: () => lastReceipt,
132
  },
133
  ingest: (username, payload) =>
134
+ mutex.run(async () => {
135
+ const result = await ingestBatch(
136
+ { store, mirror, enrich: enrichStore, now: () => new Date() },
137
+ username,
138
+ payload,
139
+ );
140
+ try {
141
+ await index.advanceToLatest();
142
+ applyIndexStats();
143
+ datasetState = { state: "ready" };
144
+ readiness = buildReadiness();
145
+ } catch (error) {
146
+ const message = errorMessage(error);
147
+ datasetState = { state: "invalid", error: `durable index ingest sync failed: ${message}` };
148
+ readiness = buildReadiness();
149
+ throw error;
150
+ }
151
+ return result;
152
+ }),
153
  repairMembership: (actor) =>
154
  mutex.run(async () => {
155
  const pool = await membership.repairConfig(actor);
 
169
  app.use("*", serveStatic({ root: config.staticRoot }));
170
  app.use("*", serveStatic({ root: config.staticRoot, path: "index.html" }));
171
 
172
+ console.log(
173
+ `[xtap-pool] restored durable index ${initialAdvance.revision.slice(0, 12)} from ` +
174
+ `${config.indexBucket}; changed_files=${String(initialAdvance.filesChanged)} ` +
175
+ `rows=${String(initialAdvance.rowsApplied)}`,
176
+ );
177
  readiness = buildReadiness();
178
  const pool = membership.snapshot();
179
  console.log(
 
194
  "to drain the queue.",
195
  );
196
  }
197
+ activeFetch = (request) => app.fetch(request);
198
  startCredentialRetryIfNeeded();
199
  startEnrichmentRefresh();
200
 
201
+ function applyIndexStats(): void {
202
+ const stats = index.stats();
203
+ rebuilt = { files: stats.tweetFiles, tweets: stats.tweetRows };
204
+ enrichment = { files: stats.enrichmentFiles, rows: stats.enrichmentRows };
205
+ recordLastReceipt(mirror.latestReceipt());
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  }
207
 
208
  /**
 
216
  enrichmentRefreshTimer = undefined;
217
  void refreshExternalEnrichment()
218
  .catch((error: unknown) => {
219
+ const message = errorMessage(error);
220
+ datasetState = { state: "invalid", error: `durable index refresh failed: ${message}` };
221
+ readiness = buildReadiness();
222
+ console.error(`[xtap-pool] enrichment refresh failed: ${message}`);
223
  })
224
  .finally(startEnrichmentRefresh);
225
  }, enrichmentRefreshMs());
226
  }
227
 
228
  async function refreshExternalEnrichment(): Promise<void> {
229
+ if (!datasetCredentialOk(datasetCredential)) return;
230
  await mutex.run(async () => {
231
+ if (!datasetCredentialOk(datasetCredential)) return;
 
232
  const nextTaxonomy = await loadEnrichTaxonomy(mirror, config.taxonomyVersion);
233
  if (nextTaxonomy.error !== undefined) {
234
  throw new Error(`enrichment taxonomy refresh failed: ${nextTaxonomy.error}`);
235
  }
236
+ const nextContractHash = contractHashFor({ taxonomy: nextTaxonomy, model: config.llmModel });
237
+ if (nextContractHash !== enrichStore.currentContractHash()) {
238
+ throw new Error("enrichment contract changed; publish a replacement durable index");
239
+ }
240
+ taxonomy = nextTaxonomy;
241
+ await index.advanceToLatest();
242
+ applyIndexStats();
243
+ datasetState = { state: "ready" };
 
244
  readiness = buildReadiness();
245
  });
246
  }
 
334
  return taxonomy.error === undefined;
335
  }
336
 
337
+ // eslint-disable-next-line complexity -- Recovery checks each independently persisted dataset-backed configuration.
338
  async function reloadDatasetBackedConfig(force: boolean): Promise<void> {
339
  if (force || membership.hasRetryableConfigError()) await membership.reload();
340
  if (force || serviceAccounts.hasRetryableConfigError()) await serviceAccounts.reload();
341
  if (config.enrichEnabled && (force || !taxonomyReady())) {
342
+ const nextTaxonomy = await loadEnrichTaxonomy(mirror, config.taxonomyVersion);
343
+ const nextContractHash = contractHashFor({ taxonomy: nextTaxonomy, model: config.llmModel });
344
+ if (nextContractHash !== enrichStore.currentContractHash()) {
345
+ throw new Error("enrichment contract changed; publish a replacement durable index");
346
+ }
347
+ taxonomy = nextTaxonomy;
348
  }
349
  }
350
 
 
384
  datasetCredential = await checkDatasetCredential({
385
  token: config.hfToken,
386
  datasetRepo: config.datasetRepo,
387
+ indexBucket: config.indexBucket,
388
  });
389
  }
390
  const datasetRecovered = !datasetWasReady && datasetCredentialOk(datasetCredential);
 
392
  await mutex.run(async () => {
393
  await reloadDatasetBackedConfig(datasetRecovered);
394
  if (datasetRecovered || datasetState.state === "unknown") {
395
+ await index.advanceToLatest();
396
+ applyIndexStats();
397
+ datasetState = { state: "ready" };
398
  }
399
  });
400
  }
 
407
  readiness = buildReadiness();
408
  }
409
 
410
+ async function waitForStorageCredential(): Promise<void> {
411
+ while (!datasetCredentialOk(datasetCredential)) {
412
+ datasetState = {
413
+ state: datasetCredential.credential === "invalid" ? "invalid" : "unknown",
414
+ error: "error" in datasetCredential ? datasetCredential.error : "HF_TOKEN is unavailable.",
415
+ };
416
+ console.error(`[xtap-pool] storage credential unavailable: ${datasetState.error}`);
417
+ await new Promise((resolve) => setTimeout(resolve, credentialRetryMs()));
418
+ datasetCredential = await checkDatasetCredential({
419
+ token: config.hfToken,
420
+ datasetRepo: config.datasetRepo,
421
+ indexBucket: config.indexBucket,
422
+ });
423
+ }
424
+ }
425
+
426
  function credentialRetryMs(): number {
427
  return Math.min(Math.max(config.enrichIntervalMs, 5000), 60000);
428
  }
space/tests/config.test.ts CHANGED
@@ -4,6 +4,7 @@ import { loadConfig } from "../src/config.js";
4
 
5
  const baseEnv = {
6
  DATASET_REPO: "osolmaz/xtap-pool-data",
 
7
  HF_TOKEN: "hf_x",
8
  POOL_SIGNING_SECRET: "pool-secret-0123456789abcdef0123456789abcdef",
9
  SESSION_SECRET: "session-secret-0123456789abcdef0123456789ab",
@@ -18,6 +19,7 @@ describe("loadConfig", () => {
18
  it("parses a full environment with defaults", () => {
19
  const config = loadConfig(baseEnv);
20
  expect(config.port).toBe(7860);
 
21
  expect(config.allowedUsers).toEqual(["osolmaz", "alice", "bob"]);
22
  expect(config.poolAdmins).toEqual(["osolmaz"]);
23
  expect(config.publicUrl).toBe("https://dutifuldev-xtap-pool.hf.space");
@@ -52,7 +54,7 @@ describe("loadConfig", () => {
52
  expect(config.enrichEnabled).toBe(false);
53
  expect(config.enrichIntervalMs).toBe(60000);
54
  expect(config.enrichMaxConcurrentCalls).toBe(1);
55
- expect(config.llmModel).toBe("zai-org/GLM-5.2");
56
  expect(config.taxonomyVersion).toBe(1);
57
  });
58
 
 
4
 
5
  const baseEnv = {
6
  DATASET_REPO: "osolmaz/xtap-pool-data",
7
+ INDEX_BUCKET: "osolmaz/xtap-pool-bucket",
8
  HF_TOKEN: "hf_x",
9
  POOL_SIGNING_SECRET: "pool-secret-0123456789abcdef0123456789abcdef",
10
  SESSION_SECRET: "session-secret-0123456789abcdef0123456789ab",
 
19
  it("parses a full environment with defaults", () => {
20
  const config = loadConfig(baseEnv);
21
  expect(config.port).toBe(7860);
22
+ expect(config.indexBucket).toBe("osolmaz/xtap-pool-bucket");
23
  expect(config.allowedUsers).toEqual(["osolmaz", "alice", "bob"]);
24
  expect(config.poolAdmins).toEqual(["osolmaz"]);
25
  expect(config.publicUrl).toBe("https://dutifuldev-xtap-pool.hf.space");
 
54
  expect(config.enrichEnabled).toBe(false);
55
  expect(config.enrichIntervalMs).toBe(60000);
56
  expect(config.enrichMaxConcurrentCalls).toBe(1);
57
+ expect(config.llmModel).toBe("zai-org/GLM-5.2:fireworks-ai");
58
  expect(config.taxonomyVersion).toBe(1);
59
  });
60
 
space/tests/dataset-token.test.ts CHANGED
@@ -2,103 +2,92 @@ import { describe, expect, it, vi } from "vitest";
2
 
3
  import { checkDatasetCredential } from "../src/dataset-token.js";
4
 
5
- describe("dataset credential readiness", () => {
6
- it("accepts fine-grained tokens with dataset read/write permissions only", async () => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  const fetchFn: typeof fetch = () =>
8
  Promise.resolve(
9
- Response.json({
10
- auth: {
11
- accessToken: {
12
- role: "fineGrained",
13
- fineGrained: {
14
- global: [],
15
- scoped: [
16
- {
17
- entity: { type: "dataset", name: "alice/xtap-pool-data" },
18
- permissions: ["repo.content.read", "repo.content.write"],
19
- },
20
- ],
21
- },
22
- },
23
- },
24
- }),
25
  );
26
 
27
- await expect(
28
- checkDatasetCredential({ token: "hf_dataset", datasetRepo: "alice/xtap-pool-data", fetchFn }),
29
- ).resolves.toEqual({ credential: "ok" });
 
30
  });
31
 
32
- it("rejects tokens whose metadata passes but private-dataset downloads fail", async () => {
33
  const fetchFn = vi
34
  .fn<typeof fetch>()
35
- .mockResolvedValueOnce(
36
- Response.json({
37
- auth: {
38
- accessToken: {
39
- role: "fineGrained",
40
- fineGrained: {
41
- global: [],
42
- scoped: [
43
- {
44
- entity: { type: "dataset", name: "alice/xtap-pool-data" },
45
- permissions: ["repo.content.read", "repo.content.write"],
46
- },
47
- ],
48
- },
49
- },
50
- },
51
- }),
52
- )
53
  .mockResolvedValueOnce(new Response("unauthorized", { status: 401 }));
54
 
55
- await expect(
56
- checkDatasetCredential({ token: "hf_dataset", datasetRepo: "alice/xtap-pool-data", fetchFn }),
57
- ).resolves.toEqual({
58
  credential: "invalid",
59
- error: "Hugging Face rejected a direct private-dataset download using HF_TOKEN (401).",
60
  });
61
  });
62
 
63
- it("rejects dataset tokens without write permission", async () => {
64
  const fetchFn: typeof fetch = () =>
65
  Promise.resolve(
66
- Response.json({
67
- auth: {
68
- accessToken: {
69
- role: "fineGrained",
70
- fineGrained: {
71
- global: [],
72
- scoped: [
73
- {
74
- entity: { type: "dataset", name: "alice/xtap-pool-data" },
75
- permissions: ["repo.content.read"],
76
- },
77
- ],
78
- },
79
- },
80
- },
81
- }),
82
  );
83
 
84
- await expect(
85
- checkDatasetCredential({
86
- token: "hf_readonly",
87
- datasetRepo: "alice/xtap-pool-data",
88
- fetchFn,
89
- }),
90
- ).resolves.toEqual({
91
  credential: "invalid",
92
- error: "HF_TOKEN must include repo.content.write or repo.write on alice/xtap-pool-data.",
93
  });
94
  });
95
 
96
  it("treats transient Hub failures as unknown so startup can retry", async () => {
97
  const fetchFn: typeof fetch = () => Promise.resolve(new Response("oops", { status: 503 }));
98
 
99
- await expect(
100
- checkDatasetCredential({ token: "hf_dataset", datasetRepo: "alice/xtap-pool-data", fetchFn }),
101
- ).resolves.toEqual({
102
  credential: "unknown",
103
  error: "Hugging Face rejected HF_TOKEN (503).",
104
  });
 
2
 
3
  import { checkDatasetCredential } from "../src/dataset-token.js";
4
 
5
+ const datasetRepo = "alice/xtap-pool-data";
6
+ const indexBucket = "alice/xtap-pool-bucket";
7
+
8
+ function scope(name: string, permissions: readonly string[], type: string): unknown {
9
+ return { entity: { type, name }, permissions };
10
+ }
11
+
12
+ function whoami(scopes: readonly unknown[]): unknown {
13
+ return {
14
+ auth: {
15
+ accessToken: {
16
+ role: "fineGrained",
17
+ fineGrained: { global: [], scoped: scopes },
18
+ },
19
+ },
20
+ };
21
+ }
22
+
23
+ function validScopes(): readonly unknown[] {
24
+ const permissions = ["repo.content.read", "repo.content.write"];
25
+ return [scope(datasetRepo, permissions, "dataset"), scope(indexBucket, permissions, "bucket")];
26
+ }
27
+
28
+ function check(fetchFn: typeof fetch, token = "hf_storage") {
29
+ return checkDatasetCredential({ token, datasetRepo, indexBucket, fetchFn });
30
+ }
31
+
32
+ describe("storage credential readiness", () => {
33
+ it("accepts exact dataset and Bucket read/write scopes plus direct reads", async () => {
34
+ const fetchFn = vi
35
+ .fn<typeof fetch>()
36
+ .mockResolvedValueOnce(Response.json(whoami(validScopes())))
37
+ .mockResolvedValueOnce(new Response("{}", { status: 200 }))
38
+ .mockResolvedValueOnce(new Response("{}", { status: 200 }));
39
+
40
+ await expect(check(fetchFn)).resolves.toEqual({ credential: "ok" });
41
+ });
42
+
43
+ it("rejects a token whose Bucket metadata scope is missing", async () => {
44
  const fetchFn: typeof fetch = () =>
45
  Promise.resolve(
46
+ Response.json(
47
+ whoami([scope(datasetRepo, ["repo.content.read", "repo.content.write"], "dataset")]),
48
+ ),
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  );
50
 
51
+ await expect(check(fetchFn)).resolves.toEqual({
52
+ credential: "invalid",
53
+ error: `HF_TOKEN must include repo.content.read on ${indexBucket}. HF_TOKEN must include repo.content.write or repo.write on ${indexBucket}.`,
54
+ });
55
  });
56
 
57
+ it("rejects tokens whose metadata passes but a direct storage read fails", async () => {
58
  const fetchFn = vi
59
  .fn<typeof fetch>()
60
+ .mockResolvedValueOnce(Response.json(whoami(validScopes())))
61
+ .mockResolvedValueOnce(new Response("{}", { status: 200 }))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  .mockResolvedValueOnce(new Response("unauthorized", { status: 401 }));
63
 
64
+ await expect(check(fetchFn)).resolves.toEqual({
 
 
65
  credential: "invalid",
66
+ error: "Hugging Face rejected a direct private-Bucket read using HF_TOKEN (401).",
67
  });
68
  });
69
 
70
+ it("rejects storage tokens without write permission", async () => {
71
  const fetchFn: typeof fetch = () =>
72
  Promise.resolve(
73
+ Response.json(
74
+ whoami([
75
+ scope(datasetRepo, ["repo.content.read"], "dataset"),
76
+ scope(indexBucket, ["repo.content.read", "repo.content.write"], "bucket"),
77
+ ]),
78
+ ),
 
 
 
 
 
 
 
 
 
 
79
  );
80
 
81
+ await expect(check(fetchFn, "hf_readonly")).resolves.toEqual({
 
 
 
 
 
 
82
  credential: "invalid",
83
+ error: `HF_TOKEN must include repo.content.write or repo.write on ${datasetRepo}.`,
84
  });
85
  });
86
 
87
  it("treats transient Hub failures as unknown so startup can retry", async () => {
88
  const fetchFn: typeof fetch = () => Promise.resolve(new Response("oops", { status: 503 }));
89
 
90
+ await expect(check(fetchFn)).resolves.toEqual({
 
 
91
  credential: "unknown",
92
  error: "Hugging Face rejected HF_TOKEN (503).",
93
  });
space/tests/dataset.test.ts CHANGED
@@ -4,7 +4,11 @@ import { join } from "node:path";
4
 
5
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
6
 
7
- import { DatasetMirror, parseJsonlTweets } from "../src/dataset.js";
 
 
 
 
8
  import type { HubClient } from "../src/dataset.js";
9
  import { EnrichStore } from "../src/enrich-store.js";
10
  import { TweetStore } from "../src/store.js";
@@ -115,6 +119,57 @@ describe("DatasetMirror.rebuild", () => {
115
  });
116
  });
117
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  describe("DatasetMirror enrichment rebuild", () => {
119
  it("replays enrichment shards without seeding label output from legacy rows", async () => {
120
  hub.files.set(
 
4
 
5
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
6
 
7
+ import {
8
+ assertValidDatasetSourceContent,
9
+ DatasetMirror,
10
+ parseJsonlTweets,
11
+ } from "../src/dataset.js";
12
  import type { HubClient } from "../src/dataset.js";
13
  import { EnrichStore } from "../src/enrich-store.js";
14
  import { TweetStore } from "../src/store.js";
 
119
  });
120
  });
121
 
122
+ describe("durable source validation", () => {
123
+ it("rejects malformed JSON and schema-invalid records for every source kind", () => {
124
+ expect(() => {
125
+ assertValidDatasetSourceContent("data/osolmaz/2026/08/tweets-2026-08-04.jsonl", "not json\n");
126
+ }).toThrow("invalid JSON");
127
+ for (const path of [
128
+ "data/osolmaz/2026/08/tweets-2026-08-04.jsonl",
129
+ "enrichment/2026/08/enrichment-2026-08-04.jsonl",
130
+ "enrichment/attempts/2026/08/attempts-2026-08-04.jsonl",
131
+ "enrichment/registry/2026/08/registry-2026-08-04.jsonl",
132
+ "enrichment/receipts/2026-08-04.jsonl",
133
+ ]) {
134
+ expect(() => {
135
+ assertValidDatasetSourceContent(path, "{}\n");
136
+ }).toThrow("invalid");
137
+ }
138
+ });
139
+
140
+ it("accepts recognized previous enrichment and receipt contracts", () => {
141
+ expect(() => {
142
+ assertValidDatasetSourceContent(
143
+ "enrichment/2026/07/enrichment-2026-07-06.jsonl",
144
+ `${JSON.stringify({
145
+ unit_id: "1:someone",
146
+ tweet_ids: ["1"],
147
+ labels: ["ai"],
148
+ free_labels: ["gguf"],
149
+ concepts: [{ name: "vLLM", aliases: [] }],
150
+ model: "model",
151
+ taxonomy_version: 1,
152
+ enriched_at: "2026-07-06T00:00:00.000Z",
153
+ })}\n`,
154
+ );
155
+ }).not.toThrow();
156
+ expect(() => {
157
+ assertValidDatasetSourceContent(
158
+ "enrichment/receipts/2026-07-26.jsonl",
159
+ `${JSON.stringify({
160
+ started_at: "2026-07-26T00:00:00.000Z",
161
+ finished_at: "2026-07-26T00:01:00.000Z",
162
+ units: 1,
163
+ calls: 1,
164
+ prompt_tokens: 10,
165
+ completion_tokens: 5,
166
+ failures: 0,
167
+ })}\n`,
168
+ );
169
+ }).not.toThrow();
170
+ });
171
+ });
172
+
173
  describe("DatasetMirror enrichment rebuild", () => {
174
  it("replays enrichment shards without seeding label output from legacy rows", async () => {
175
  hub.files.set(
space/tests/durable-index.test.ts ADDED
@@ -0,0 +1,416 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+
6
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
7
+
8
+ import { DatasetMirror } from "../src/dataset.js";
9
+ import { DurableIndex, durableIndexManifestSchema } from "../src/durable-index.js";
10
+ import type {
11
+ BucketFile,
12
+ DatasetSnapshotClient,
13
+ DatasetSourceFile,
14
+ DurableIndexBucketClient,
15
+ DurableIndexOptions,
16
+ } from "../src/durable-index.js";
17
+ import { FakeHub, makePooled } from "./helpers.js";
18
+
19
+ const DATASET = "osolmaz/xtap-pool-data";
20
+ const BUCKET = "osolmaz/xtap-pool-bucket";
21
+ const CONTRACT = "a".repeat(64);
22
+
23
+ class FakeSource implements DatasetSnapshotClient {
24
+ revision = "1".repeat(40);
25
+ files = new Map<string, string>();
26
+ textFiles = new Map<string, string>();
27
+ commitErrors: unknown[] = [];
28
+
29
+ currentRevision(): Promise<string> {
30
+ return Promise.resolve(this.revision);
31
+ }
32
+
33
+ listJsonlFiles(): Promise<readonly DatasetSourceFile[]> {
34
+ return Promise.resolve(
35
+ [...this.files.entries()].map(([path, content]) => ({
36
+ path,
37
+ oid: sha256(content),
38
+ size: Buffer.byteLength(content),
39
+ })),
40
+ );
41
+ }
42
+
43
+ downloadFile(path: string): Promise<Uint8Array> {
44
+ const content = this.files.get(path);
45
+ if (content === undefined) return Promise.reject(new Error(`missing ${path}`));
46
+ return Promise.resolve(Buffer.from(content));
47
+ }
48
+
49
+ readText(path: string): Promise<string | undefined> {
50
+ return Promise.resolve(this.textFiles.get(path));
51
+ }
52
+
53
+ commitText(path: string, content: string, parentRevision: string): Promise<string> {
54
+ const commitError = this.commitErrors.shift();
55
+ if (commitError !== undefined) {
56
+ this.advanceRevision();
57
+ // Deliberately permit a non-Error rejection to cover defensive classifier behavior.
58
+ // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
59
+ return Promise.reject(commitError);
60
+ }
61
+ if (parentRevision !== this.revision) {
62
+ return Promise.reject(new Error("parent commit does not match dataset HEAD"));
63
+ }
64
+ this.textFiles.set(path, content);
65
+ this.advanceRevision();
66
+ return Promise.resolve(this.revision);
67
+ }
68
+
69
+ advanceRevision(): void {
70
+ const value = Number.parseInt(this.revision.slice(0, 8), 16) + 1;
71
+ this.revision = value.toString(16).padStart(40, "0");
72
+ }
73
+ }
74
+
75
+ class FakeBucket implements DurableIndexBucketClient {
76
+ files = new Map<string, Buffer>();
77
+ uploaded = new Map<string, string>();
78
+ removed: string[] = [];
79
+ private clock = 0;
80
+
81
+ async download(path: string, destination: string): Promise<boolean> {
82
+ const content = this.files.get(path);
83
+ if (content === undefined) return false;
84
+ await mkdir(dirname(destination), { recursive: true });
85
+ await writeFile(destination, content);
86
+ return true;
87
+ }
88
+
89
+ async uploadFile(path: string, source: string): Promise<void> {
90
+ this.files.set(path, await readFile(source));
91
+ this.clock += 1;
92
+ this.uploaded.set(path, String(this.clock).padStart(8, "0"));
93
+ }
94
+
95
+ list(prefix: string): Promise<readonly BucketFile[]> {
96
+ return Promise.resolve(
97
+ [...this.files.keys()]
98
+ .filter((path) => path.startsWith(`${prefix}/`))
99
+ .map((path) => {
100
+ const uploadedAt = this.uploaded.get(path);
101
+ return { path, ...(uploadedAt === undefined ? {} : { uploadedAt }) };
102
+ }),
103
+ );
104
+ }
105
+
106
+ remove(paths: readonly string[]): Promise<void> {
107
+ for (const path of paths) {
108
+ this.files.delete(path);
109
+ this.uploaded.delete(path);
110
+ this.removed.push(path);
111
+ }
112
+ return Promise.resolve();
113
+ }
114
+ }
115
+
116
+ let dir: string;
117
+ let source: FakeSource;
118
+ let bucket: FakeBucket;
119
+ let hub: FakeHub;
120
+
121
+ beforeEach(async () => {
122
+ dir = await mkdtemp(join(tmpdir(), "xtap-index-test-"));
123
+ source = new FakeSource();
124
+ bucket = new FakeBucket();
125
+ hub = new FakeHub();
126
+ });
127
+
128
+ afterEach(async () => {
129
+ await rm(dir, { recursive: true, force: true });
130
+ });
131
+
132
+ function options(name: string): DurableIndexOptions {
133
+ return {
134
+ datasetRepo: DATASET,
135
+ indexBucket: BUCKET,
136
+ accessToken: "hf_test",
137
+ databasePath: join(dir, `${name}.sqlite`),
138
+ mirror: new DatasetMirror(hub, join(dir, `${name}-mirror`)),
139
+ taxonomyVersion: 1,
140
+ contractHash: CONTRACT,
141
+ sourceClient: source,
142
+ bucketClient: bucket,
143
+ };
144
+ }
145
+
146
+ function tweetLine(id: string, author = "someone"): string {
147
+ return `${JSON.stringify(
148
+ makePooled({
149
+ id,
150
+ url: `https://x.com/${author}/status/${id}`,
151
+ author: { username: author, display_name: author },
152
+ conversation_id: id,
153
+ }),
154
+ )}\n`;
155
+ }
156
+
157
+ describe("durable enrichment index", () => {
158
+ it("bootstraps, publishes, restores, and applies only a strict append suffix", async () => {
159
+ const path = "data/osolmaz/2026/08/tweets-2026-08-04.jsonl";
160
+ source.files.set(path, tweetLine("1"));
161
+ const initial = await DurableIndex.bootstrap(options("bootstrap"));
162
+ expect(initial.store.count()).toBe(1);
163
+ const firstManifest = await initial.publish();
164
+ initial.close();
165
+
166
+ expect(durableIndexManifestSchema.parse(firstManifest)).toEqual(firstManifest);
167
+ expect(() =>
168
+ durableIndexManifestSchema.parse({ ...firstManifest, unexpected: true }),
169
+ ).toThrow();
170
+ const restoreOptions = options("restored");
171
+ await Promise.all([
172
+ writeFile(restoreOptions.databasePath, "stale database"),
173
+ writeFile(`${restoreOptions.databasePath}-wal`, "stale wal"),
174
+ writeFile(`${restoreOptions.databasePath}-shm`, "stale shm"),
175
+ ]);
176
+ const restored = await DurableIndex.restore(restoreOptions);
177
+ expect(restored.store.count()).toBe(1);
178
+ source.files.set(path, `${source.files.get(path) ?? ""}${tweetLine("2", "other")}`);
179
+ source.advanceRevision();
180
+
181
+ const advanced = await restored.advanceToLatest();
182
+ expect(advanced).toMatchObject({ filesChanged: 1, rowsApplied: 1 });
183
+ expect(restored.store.count()).toBe(2);
184
+ await expect(restored.advanceToLatest()).resolves.toMatchObject({
185
+ filesChanged: 0,
186
+ rowsApplied: 0,
187
+ });
188
+ restored.close();
189
+ });
190
+
191
+ it("replays enrichment, attempt, registry, and receipt suffixes", async () => {
192
+ const tweetPath = "data/osolmaz/2026/08/tweets-2026-08-04.jsonl";
193
+ source.files.set(tweetPath, `${tweetLine("1")}${tweetLine("2", "other")}`);
194
+ const eventOptions = options("events");
195
+ const index = await DurableIndex.bootstrap(eventOptions);
196
+ const items = index.enrichStore.claimQueued(10);
197
+ index.enrichStore.releaseClaims();
198
+ const first = items.find((item) => item.tweetIds.includes("1"));
199
+ const second = items.find((item) => item.tweetIds.includes("2"));
200
+ if (first === undefined || second === undefined) throw new Error("expected queue items");
201
+
202
+ source.files.set(
203
+ "enrichment/2026/08/enrichment-2026-08-04.jsonl",
204
+ `${JSON.stringify({
205
+ unit_id: first.unitId,
206
+ tweet_ids: first.tweetIds,
207
+ input_hash: first.inputHash,
208
+ contract_hash: first.contractHash,
209
+ preset_labels: [],
210
+ free_labels: [],
211
+ model: "model",
212
+ taxonomy_version: 1,
213
+ enriched_at: "2026-08-04T00:00:00.000Z",
214
+ })}\n`,
215
+ );
216
+ source.files.set(
217
+ "enrichment/attempts/2026/08/attempts-2026-08-04.jsonl",
218
+ `${JSON.stringify({
219
+ unit_id: second.unitId,
220
+ input_hash: second.inputHash,
221
+ contract_hash: second.contractHash,
222
+ attempt: 1,
223
+ outcome: "transient_failure",
224
+ error_class: "timeout",
225
+ at: "2026-08-04T00:00:00.000Z",
226
+ })}\n`,
227
+ );
228
+ source.files.set(
229
+ "enrichment/registry/2026/08/registry-2026-08-04.jsonl",
230
+ `${JSON.stringify({
231
+ name: "new-label",
232
+ status: "candidate",
233
+ at: "2026-08-04T00:00:00.000Z",
234
+ actor: "worker",
235
+ contract_hash: CONTRACT,
236
+ registry_revision: 2,
237
+ })}\n`,
238
+ );
239
+ source.files.set(
240
+ "enrichment/receipts/2026-08-04.jsonl",
241
+ `${JSON.stringify(receiptFixture("job-1"))}\n`,
242
+ );
243
+ source.advanceRevision();
244
+
245
+ const advanced = await index.advanceToLatest();
246
+ expect(advanced.filesChanged).toBe(4);
247
+ expect(index.enrichStore.queueEntry(first.unitId)?.status).toBe("done");
248
+ expect(index.enrichStore.queueEntry(second.unitId)?.status).toBe("retrying");
249
+ expect(index.enrichStore.registryStatus("new-label")).toBe("candidate");
250
+ expect(eventOptions.mirror.latestReceipt()?.worker_id).toBe("job-1");
251
+ expect(index.stats()).toMatchObject({
252
+ enrichmentRows: 1,
253
+ attemptEvents: 1,
254
+ registryEvents: 1,
255
+ });
256
+ index.close();
257
+ });
258
+
259
+ it("fails closed on prefix edits, truncation, deletion, and checksum mismatch", async () => {
260
+ const path = "data/osolmaz/2026/08/tweets-2026-08-04.jsonl";
261
+ source.files.set(path, `${tweetLine("1")}${tweetLine("2")}`);
262
+ const index = await DurableIndex.bootstrap(options("failures"));
263
+ await index.publish();
264
+
265
+ source.files.set(path, `${tweetLine("1")}${tweetLine("2")}not json\n`);
266
+ source.advanceRevision();
267
+ await expect(index.advanceToLatest()).rejects.toThrow("invalid JSON");
268
+ source.files.set(path, `${tweetLine("1")}${tweetLine("2")}{}\n`);
269
+ source.advanceRevision();
270
+ await expect(index.advanceToLatest()).rejects.toThrow("invalid tweet record");
271
+ source.files.set(path, `${tweetLine("changed")}${tweetLine("2")}`);
272
+ source.advanceRevision();
273
+ await expect(index.advanceToLatest()).rejects.toThrow("prefix changed");
274
+ source.files.set(path, tweetLine("1"));
275
+ source.advanceRevision();
276
+ await expect(index.advanceToLatest()).rejects.toThrow("truncated");
277
+ source.files.delete(path);
278
+ source.advanceRevision();
279
+ await expect(index.advanceToLatest()).rejects.toThrow("were deleted");
280
+ source.files.set(path, tweetLine("1").trimEnd());
281
+ source.advanceRevision();
282
+ await expect(index.advanceToLatest()).rejects.toThrow("complete JSONL line");
283
+ source.files.set(path, `${tweetLine("1")}${tweetLine("2")}`);
284
+ source.files.set("data/unknown.jsonl", tweetLine("1"));
285
+ source.advanceRevision();
286
+ await expect(index.advanceToLatest()).rejects.toThrow("unsupported dataset index source");
287
+ index.close();
288
+
289
+ const manifest = JSON.parse(source.textFiles.get("index/current.json") ?? "{}") as {
290
+ database: { key: string };
291
+ };
292
+ bucket.files.set(manifest.database.key, Buffer.from("corrupt"));
293
+ await expect(DurableIndex.restore(options("corrupt"))).rejects.toThrow("checksum mismatch");
294
+ });
295
+
296
+ it("refuses an atomic manifest commit after the dataset head changes", async () => {
297
+ source.files.set("data/osolmaz/2026/08/tweets-2026-08-04.jsonl", tweetLine("1"));
298
+ const index = await DurableIndex.bootstrap(options("race"));
299
+ source.advanceRevision();
300
+
301
+ await expect(index.publish()).rejects.toThrow("parent commit does not match");
302
+ expect(source.textFiles.has("index/current.json")).toBe(false);
303
+ index.close();
304
+ });
305
+
306
+ it("re-advances and retries when the dataset changes during publication", async () => {
307
+ source.files.set("data/osolmaz/2026/08/tweets-2026-08-04.jsonl", tweetLine("1"));
308
+ const index = await DurableIndex.bootstrap(options("publication-retry"));
309
+ source.commitErrors.push(new Error("The branch was updated since publication started"));
310
+
311
+ const published = await index.publishLatest();
312
+
313
+ expect(published.manifest.dataset.revision).toBe(published.advance.revision);
314
+ expect(source.textFiles.has("index/current.json")).toBe(true);
315
+ expect(bucket.files.has(published.manifest.database.key)).toBe(true);
316
+ index.close();
317
+ });
318
+
319
+ it("retries status-code conflicts but rejects unrelated and exhausted failures", async () => {
320
+ source.files.set("data/osolmaz/2026/08/tweets-2026-08-04.jsonl", tweetLine("1"));
321
+ const index = await DurableIndex.bootstrap(options("publication-errors"));
322
+ source.commitErrors.push(Object.assign(new Error("conflict"), { statusCode: 409 }));
323
+ await expect(index.publishLatest()).resolves.toHaveProperty("manifest.dataset.repo", DATASET);
324
+
325
+ source.commitErrors.push(new Error("permission denied"));
326
+ await expect(index.publishLatest()).rejects.toThrow("permission denied");
327
+ source.commitErrors.push("non-error rejection");
328
+ await expect(index.publishLatest()).rejects.toBe("non-error rejection");
329
+
330
+ source.commitErrors.push(
331
+ ...Array.from(
332
+ { length: 5 },
333
+ () => new Error("The branch was updated since publication started"),
334
+ ),
335
+ );
336
+ await expect(index.publishLatest()).rejects.toThrow("branch was updated");
337
+ index.close();
338
+ });
339
+
340
+ it("does not let failed publication uploads displace successful predecessors", async () => {
341
+ const path = "data/osolmaz/2026/08/tweets-2026-08-04.jsonl";
342
+ source.files.set(path, tweetLine("1"));
343
+ const index = await DurableIndex.bootstrap(options("orphan-retention"));
344
+ const first = await index.publish();
345
+ for (let generation = 2; generation <= 5; generation += 1) {
346
+ source.files.set(path, `${source.files.get(path) ?? ""}${tweetLine(String(generation))}`);
347
+ source.advanceRevision();
348
+ await index.advanceToLatest();
349
+ source.commitErrors.push(new Error("permission denied"));
350
+ await expect(index.publish()).rejects.toThrow("permission denied");
351
+ }
352
+ source.files.set(path, `${source.files.get(path) ?? ""}${tweetLine("6")}`);
353
+ source.advanceRevision();
354
+ await index.advanceToLatest();
355
+ const final = await index.publish();
356
+
357
+ expect(final.database.predecessors).toEqual([first.database.key]);
358
+ expect([...bucket.files.keys()].filter((key) => key.endsWith(".sqlite")).sort()).toEqual(
359
+ [first.database.key, final.database.key].sort(),
360
+ );
361
+ index.close();
362
+ });
363
+
364
+ it("retains the active database and three recent predecessors", async () => {
365
+ const path = "data/osolmaz/2026/08/tweets-2026-08-04.jsonl";
366
+ source.files.set(path, tweetLine("1"));
367
+ const index = await DurableIndex.bootstrap(options("retention"));
368
+ for (let generation = 0; generation < 6; generation += 1) {
369
+ if (generation > 0) {
370
+ source.files.set(
371
+ path,
372
+ `${source.files.get(path) ?? ""}${tweetLine(String(generation + 1))}`,
373
+ );
374
+ source.advanceRevision();
375
+ await index.advanceToLatest();
376
+ }
377
+ await index.publish();
378
+ }
379
+ const databases = [...bucket.files.keys()].filter((key) => key.startsWith("index/databases/"));
380
+ expect(databases).toHaveLength(4);
381
+ const current = JSON.parse(source.textFiles.get("index/current.json") ?? "{}") as {
382
+ database: { key: string; predecessors: string[] };
383
+ };
384
+ expect(current.database.predecessors).toHaveLength(3);
385
+ expect(databases.sort()).toEqual(
386
+ [current.database.key, ...current.database.predecessors].sort(),
387
+ );
388
+ expect(bucket.removed).toHaveLength(2);
389
+ index.close();
390
+ });
391
+ });
392
+
393
+ function receiptFixture(workerId: string): Record<string, unknown> {
394
+ return {
395
+ started_at: "2026-08-04T00:00:00.000Z",
396
+ finished_at: "2026-08-04T00:01:00.000Z",
397
+ units: 1,
398
+ calls: 1,
399
+ prompt_tokens: 1,
400
+ completion_tokens: 1,
401
+ cost_usd: 0.00001,
402
+ failures: 0,
403
+ retries: 0,
404
+ blocked: 0,
405
+ contract_hash: CONTRACT,
406
+ worker_id: workerId,
407
+ discarded_assignments: 0,
408
+ new_candidates: 0,
409
+ new_approvals: 0,
410
+ new_rejections: 0,
411
+ };
412
+ }
413
+
414
+ function sha256(value: string): string {
415
+ return createHash("sha256").update(value).digest("hex");
416
+ }
space/tests/helpers.ts CHANGED
@@ -7,6 +7,7 @@ export const testConfig: SpaceConfig = {
7
  port: 7860,
8
  dataDir: ".data-test",
9
  datasetRepo: "osolmaz/xtap-pool-data",
 
10
  hfToken: "hf_test_token",
11
  poolSigningSecret: "pool-secret-0123456789abcdef0123456789abcdef",
12
  sessionSecret: "session-secret-0123456789abcdef0123456789ab",
 
7
  port: 7860,
8
  dataDir: ".data-test",
9
  datasetRepo: "osolmaz/xtap-pool-data",
10
+ indexBucket: "osolmaz/xtap-pool-bucket",
11
  hfToken: "hf_test_token",
12
  poolSigningSecret: "pool-secret-0123456789abcdef0123456789abcdef",
13
  sessionSecret: "session-secret-0123456789abcdef0123456789ab",