@fangorn-network/westmarch 0.0.2-dev → 0.0.3-dev

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/publish/ship.js CHANGED
@@ -2,6 +2,7 @@
2
2
  // westmarch-ship: one config, one command, a live Fangorn app.
3
3
  //
4
4
  // westmarch-ship app.json [--no-crawl] [--no-deploy] [--dry-run]
5
+ // westmarch-ship app.json --local <out> [--crawl] a view from staged records, no chain (see buildLocal)
5
6
  //
6
7
  // Every step checks the chain (or Cloudflare) first and only acts on a difference, so
7
8
  // re-running is how you update: new data, a changed schema, a new source, a moved site.
@@ -25,7 +26,7 @@ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync,
25
26
  import { createRequire } from "node:module";
26
27
  import { dirname, join, resolve } from "node:path";
27
28
  import { homedir } from "node:os";
28
- import { fileURLToPath } from "node:url";
29
+ import { fileURLToPath, pathToFileURL } from "node:url";
29
30
 
30
31
  const HERE = dirname(fileURLToPath(import.meta.url));
31
32
  const USDC = { "arbitrum-sepolia": "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d", arbitrum: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
@@ -37,7 +38,11 @@ const log = (s) => console.error(`[ship] ${s}`);
37
38
  //
38
39
  // {
39
40
  // "app": "quorum", "name": "Quorum", "description": "…", "tags": ["civic"],
40
- // "site": { "project": "quorum", "account": "<cloudflare account id>" },
41
+ // "site": { "project": "quorum", "account": "<cloudflare account id>",
42
+ // "pages": "pages", // optional: the app's own files, copied over the stock page
43
+ // "nav": [{ "href": "coverage.html", "label": "Coverage" }], // optional: header links to them
44
+ // "agent": "app/agent.js" }, // optional: the module whose registerAgent the card lists, when
45
+ // // `pages` replaces the stock index.html with a page of its own
41
46
  // "types": { "<tag>": { "description": "…", "role_map": {…}, "presentation": {…} } },
42
47
  // "relations": [{ "rel": "…", "from": "<tag>", "to": "<tag>" }],
43
48
  // "sources": [{ "namespace": "…", "cwd": "…", "command": ["python", "-m", "…", …] }],
@@ -45,7 +50,7 @@ const log = (s) => console.error(`[ship] ${s}`);
45
50
  // "paid": { "price": "0.01", "network": "arbitrum-sepolia", "description": "…" }
46
51
  // }
47
52
  //
48
- // `paid` sells per-record detail over x402 (site/paid-worker.js): each source that names a
53
+ // `paid` sells per-record detail over x402 (site/worker.js): each source that names a
49
54
  // `paid_dir` gets `--paid-dir` and writes one JSON per record there; they are deployed
50
55
  // behind the worker, and the price goes in the card. The payee is the app owner unless
51
56
  // `paid.payTo` says otherwise; the asset is USDC on `paid.network` unless `paid.asset` does.
@@ -150,7 +155,7 @@ function assertSameDeployment(cli) {
150
155
 
151
156
  // ── the steps ─────────────────────────────────────────────────────────────────
152
157
 
153
- export async function ship(configPath, { crawl = true, deploy = true, dry = false, replace = false } = {}) {
158
+ export async function ship(configPath, { crawl = true, deploy = true, dry = false, replace = false, only = null } = {}) {
154
159
  const cfg = loadConfig(configPath);
155
160
  CLI = cliArgv(cfg);
156
161
  assertSameDeployment(CLI);
@@ -207,13 +212,21 @@ export async function ship(configPath, { crawl = true, deploy = true, dry = fals
207
212
  fangornCli(cfg.app, ["push"], { cwd: sdir, dry });
208
213
  } else log("schema unchanged");
209
214
 
210
- // 4. sources: crawl + publish straight into the app
211
- if (crawl) for (const s of cfg.sources) {
215
+ // 4. sources: crawl + publish straight into the app. One source failing (a site that
216
+ // blocks us, a feed that is down) keeps its last published data and does not stop the
217
+ // others; the ship still exits non-zero at the end, naming it.
218
+ const failed = [];
219
+ if (crawl) for (const s of cfg.sources.filter((s) => !only || only.includes(s.namespace))) {
212
220
  const cwd = s.cwd ? resolve(base, s.cwd.replace(/^~/, homedir())) : base;
213
221
  const paidArgs = cfg.paid && s.paid_dir ? ["--paid-dir", resolve(cwd, s.paid_dir)] : [];
214
- run(s.command[0], [...s.command.slice(1), ...paidArgs, ...(replace ? ["--replace"] : []),
215
- "--publish", "--namespace", s.namespace, "--fangorn-bin", CLI.join(" ")],
216
- { cwd, env: { FANGORN_APP_ID: cfg.app }, dry });
222
+ try {
223
+ run(s.command[0], [...s.command.slice(1), ...paidArgs, ...(replace ? ["--replace"] : []),
224
+ "--publish", "--namespace", s.namespace, "--fangorn-bin", CLI.join(" ")],
225
+ { cwd, env: { FANGORN_APP_ID: cfg.app }, dry });
226
+ } catch (e) {
227
+ log(`✗ ${s.namespace}: ${e.message}; its last published records stay live`);
228
+ failed.push(s.namespace);
229
+ }
217
230
  }
218
231
 
219
232
  // 5. the view. Its files are its state: on a fresh machine (CI), start from the deployed
@@ -229,9 +242,12 @@ export async function ship(configPath, { crawl = true, deploy = true, dry = fals
229
242
  mkdirSync(site, { recursive: true });
230
243
  for (const f of readdirSync(site, { withFileTypes: true })) if (f.name !== "view" && f.name !== "_paid") rmSync(join(site, f.name), { recursive: true, force: true });
231
244
  cpSync(STOCK, site, { recursive: true });
245
+ if (cfg.site.pages) cpSync(resolve(base, cfg.site.pages), site, { recursive: true });
246
+ writeFileSync(join(site, "nav.json"), JSON.stringify(cfg.site.nav ?? []));
232
247
  const url = deploy && !dry ? pagesUrl(cfg) : state.url ?? `https://${cfg.site.project}.pages.dev`;
233
248
  // Paid records: every source's paid_dir → site/_paid (served only through the worker).
234
- let paid = null;
249
+ let paid = null, sale = null;
250
+ const live = view.liveFields(join(site, "view"));
235
251
  if (cfg.paid) {
236
252
  const network = cfg.paid.network ?? "arbitrum-sepolia";
237
253
  const asset = cfg.paid.asset ?? USDC[network];
@@ -244,7 +260,7 @@ export async function ship(configPath, { crawl = true, deploy = true, dry = fals
244
260
  // an item since removed or re-extracted is not for sale, whatever is still on disk.
245
261
  rmSync(join(site, "_paid"), { recursive: true, force: true });
246
262
  mkdirSync(join(site, "_paid"), { recursive: true });
247
- const vouched = new Set(view.liveFields(join(site, "view")).map((f) => f.paid_sha256).filter(Boolean));
263
+ const vouched = new Set(live.map((f) => f.paid_sha256).filter(Boolean));
248
264
  let n = 0;
249
265
  for (const s of cfg.sources) {
250
266
  const d = s.paid_dir && resolve(s.cwd ? resolve(base, s.cwd.replace(/^~/, homedir())) : base, s.paid_dir);
@@ -254,13 +270,20 @@ export async function ship(configPath, { crawl = true, deploy = true, dry = fals
254
270
  writeFileSync(join(site, "_paid", f), bytes); n++;
255
271
  }
256
272
  }
257
- const worker = readFileSync(join(HERE, "../site/paid-worker.js"), "utf8").replace(/\/\*CONFIG\*\/[\s\S]*?\/\*END\*\//, JSON.stringify({
258
- price, network, asset, payTo: cfg.paid.payTo ?? owner, facilitator: cfg.paid.facilitator ?? "https://facilitator.payai.network",
259
- name: cfg.paid.tokenName ?? "USD Coin", version: cfg.paid.tokenVersion ?? "2", description: paid.description }));
260
- writeFileSync(join(site, "_worker.js"), worker);
261
- log(`paid: ${n} record(s) at ${cfg.paid.price} ${paid.symbol} on ${network}, to ${cfg.paid.payTo ?? owner}`);
273
+ sale = { price, network, asset, payTo: cfg.paid.payTo ?? owner, facilitator: cfg.paid.facilitator ?? "https://facilitator.payai.network",
274
+ name: cfg.paid.tokenName ?? "USD Coin", version: cfg.paid.tokenVersion ?? "2", description: paid.description };
275
+ log(`paid: ${n} record(s) at ${cfg.paid.price} ${paid.symbol} on ${network}, to ${sale.payTo}`);
262
276
  }
263
- const { registerAgent } = await import("../site/agent.js");
277
+ // The worker: paid records if any, and each record's source document shown in the page.
278
+ // Only the hosts the app's own records link to (their media role) are proxied.
279
+ const media = Object.values(cfg.types).flatMap((t) => [t.role_map?.media ?? []].flat());
280
+ const docHosts = [...new Set(live.flatMap((f) => media.map((m) => f[m])).filter((u) => typeof u === "string" && u.startsWith("https://"))
281
+ .map((u) => { try { return new URL(u).hostname; } catch { return null; } }).filter(Boolean))].sort();
282
+ writeFileSync(join(site, "_worker.js"), readFileSync(join(HERE, "../site/worker.js"), "utf8")
283
+ .replace(/\/\*CONFIG\*\/[\s\S]*?\/\*END\*\//, JSON.stringify({ docHosts, paid: sale })));
284
+ log(`documents: shown inline from ${docHosts.length} source host(s)`);
285
+ // The card lists the tools the page at `url` registers: the stock page's, or the app's own.
286
+ const { registerAgent } = await import(cfg.site.agent ? pathToFileURL(resolve(base, cfg.site.agent)).href : "../site/agent.js");
264
287
  const agentCard = card.agentCard({
265
288
  name: cfg.name, description: cfg.description, url: `${url}/`, version: new Date().toISOString().slice(0, 10),
266
289
  tools: await card.captureTools(() => registerAgent({})), tags: cfg.tags,
@@ -297,9 +320,62 @@ export async function ship(configPath, { crawl = true, deploy = true, dry = fals
297
320
 
298
321
  const added = Object.values(report).reduce((n, r) => n + r.added, 0);
299
322
  log(`done: ${url} · card ${cardUrl} · agent ${state.agentId ?? "(unchanged)"} · +${added} records embedded`);
323
+ if (failed.length) throw new Error(`deployed, but these sources failed and kept their old records: ${failed.join(", ")}`);
300
324
  return { url, cardUrl, agentId: state.agentId, report };
301
325
  }
302
326
 
327
+ /**
328
+ * A view from what the sources staged, with app.json's schema, touching no chain and no
329
+ * site: what a recipe change would ship, built so it can be graded first (westmarch-eval).
330
+ * The sources run without --publish, or not at all with crawl=false. Every build is whole
331
+ * (a changed `text` role must reach every row, and publishView re-embeds only new CIDs);
332
+ * `.ship/vectors.ndjson` keeps it cheap, so two recipes over the same data embed only the
333
+ * text that differs.
334
+ *
335
+ * ponytail: reads every `volume_<n>_*.json` but edges in a source's --output-dir; the
336
+ * harness publishes only the stems its source declares, so a stale file left by an older
337
+ * shape of a source is built here and not on chain. Clear the stage dir if that bites.
338
+ */
339
+ export async function buildLocal(configPath, out, { crawl = false, only = null, log: say = log } = {}) {
340
+ const cfg = loadConfig(configPath);
341
+ const base = dirname(resolve(configPath));
342
+ const [{ env }, view] = await Promise.all([import("@huggingface/transformers"), import("./view.js")]);
343
+ env.cacheDir = `${homedir()}/.cache/fangorn-mcp/models`;
344
+ // Not the app's real id or owner: nothing here is compared with the chain.
345
+ const appId = `0x${createHash("sha256").update(cfg.app).digest("hex")}`, owner = `0x${"10ca1".padStart(40, "0")}`;
346
+ const g = schemaGraph(cfg.types, cfg.relations);
347
+ const chain = { [`${owner}/${view.SCHEMA_NAMESPACE}`]: {
348
+ vertices: g.vertices.map((v) => ({ cid: v.id, schemaId: v.tag, payload: v.payload })),
349
+ edges: g.edges.map((e) => ({ relation: e.rel, sourceCid: e.from, targetCid: e.to })) } };
350
+ for (const s of cfg.sources.filter((s) => !only || only.includes(s.namespace))) {
351
+ const cwd = s.cwd ? resolve(base, s.cwd.replace(/^~/, homedir())) : base;
352
+ if (crawl) run(s.command[0], s.command.slice(1), { cwd, env: { FANGORN_APP_ID: cfg.app } });
353
+ const at = s.command.indexOf("--output-dir");
354
+ if (at < 0) { say(`${s.namespace}: no --output-dir in its command, so nothing staged to read`); continue; }
355
+ const dir = resolve(cwd, expandEnv(s.command[at + 1]));
356
+ const volume = s.command.includes("--volume") ? s.command[s.command.indexOf("--volume") + 1] : "1";
357
+ const ns = (chain[`${owner}/${s.namespace}`] ??= { vertices: [], edges: [] });
358
+ for (const f of existsSync(dir) ? readdirSync(dir).sort() : []) {
359
+ const m = new RegExp(`^volume_${volume}_(.+)\\.json$`).exec(f);
360
+ if (!m || m[1] === "edges") continue;
361
+ // The harness publishes {id: name, tag: entity, payload: fields}; the chain
362
+ // addresses it by the payload's hash, and so does this.
363
+ for (const n of JSON.parse(readFileSync(join(dir, f), "utf8"))) ns.vertices.push({
364
+ cid: createHash("sha256").update(JSON.stringify(stable(n.fields))).digest("hex"),
365
+ schemaId: n.fields?.entityType ?? m[1][0].toUpperCase() + m[1].slice(1), payload: n.fields });
366
+ }
367
+ }
368
+ rmSync(out, { recursive: true, force: true });
369
+ mkdirSync(join(base, ".ship"), { recursive: true });
370
+ const embed = view.cachedEmbed(join(base, ".ship/vectors.ndjson"));
371
+ const namespaces = [...new Set(cfg.sources.map((s) => s.namespace))].filter((n) => !only || only.includes(n));
372
+ const report = await view.publishView({ fangorn: view.localFangorn(chain, { appId, owner }), app: cfg.app,
373
+ namespaces, out, embed, log: say });
374
+ const rows = Object.values(report).reduce((n, r) => n + r.added, 0);
375
+ say(`local view: ${out} · ${rows} records, ${embed.misses} embedded, ${rows - embed.misses} from the vector cache`);
376
+ return report;
377
+ }
378
+
303
379
  /** Copy a deployed view (catalog, manifests, shards) to `dir`, so the next publish appends. */
304
380
  async function mirrorView(base, dir) {
305
381
  const get = async (p) => { const r = await fetch(`${base}/${p}`); if (!r.ok) throw new Error(`${base}/${p}: HTTP ${r.status}`); return r; };
@@ -369,10 +445,18 @@ if (process.argv[1]?.endsWith("ship.js") || process.argv[1]?.endsWith("westmarch
369
445
  }
370
446
  const [path, ...flags] = process.argv.slice(2);
371
447
  if (!path || flags.includes("--help")) {
372
- console.error("usage: westmarch-ship app.json [--no-crawl] [--no-deploy] [--dry-run] [--replace]");
448
+ console.error("usage: westmarch-ship app.json [--no-crawl] [--no-deploy] [--dry-run] [--replace] [--only ns1,ns2]\n" +
449
+ " westmarch-ship app.json --local <out> [--crawl] [--only ns1,ns2] (no chain, no deploy)");
373
450
  process.exit(path ? 0 : 2);
374
451
  }
452
+ const only = flags.includes("--only") ? flags[flags.indexOf("--only") + 1].split(",") : null;
453
+ if (flags.includes("--local")) {
454
+ await buildLocal(path, resolve(flags[flags.indexOf("--local") + 1]), { crawl: flags.includes("--crawl"), only })
455
+ .catch((e) => { console.error(`[ship] ✗ ${e.message}`); process.exit(1); });
456
+ process.exit(0);
457
+ }
375
458
  await ship(path, { crawl: !flags.includes("--no-crawl"), deploy: !flags.includes("--no-deploy"), dry: flags.includes("--dry-run"),
376
- replace: flags.includes("--replace") })
459
+ replace: flags.includes("--replace"),
460
+ only })
377
461
  .catch((e) => { console.error(`[ship] ✗ ${e.message}`); process.exit(1); });
378
462
  }
package/publish/view.js CHANGED
@@ -31,7 +31,7 @@
31
31
 
32
32
  import { gunzipSync, gzipSync } from "node:zlib";
33
33
  import { createHash } from "node:crypto";
34
- import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
34
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
35
35
  import { EMBED_DIM, EMBED_MODEL, embedDocumentDirect, packVec, unpackVec } from "../consume/embed.js";
36
36
  import { coverage as fitCoverage } from "./reactions.js";
37
37
 
@@ -130,13 +130,45 @@ export function domainSpec(schema, tags) {
130
130
  export async function readOnlyFangorn() {
131
131
  const [{ Fangorn, FangornConfig }, { generatePrivateKey }, { homedir }] = await Promise.all([
132
132
  import("@fangorn-network/sdk"), import("viem/accounts"), import("node:os")]);
133
- let gateway = process.env.FANGORN_IPFS_GATEWAY;
133
+ let gateway = process.env.FANGORN_IPFS_GATEWAY ?? process.env.IPFS_GATEWAY;
134
134
  try { gateway ??= JSON.parse(readFileSync(`${homedir()}/.fangorn/config.json`, "utf8")).pinataGateway || undefined; } catch { /* no CLI config */ }
135
135
  if (gateway && !/^https?:\/\//.test(gateway)) gateway = `https://${gateway}`;
136
136
  return Fangorn.create({ privateKey: generatePrivateKey(), config: FangornConfig,
137
137
  storage: { signedUrl: gateway ? { gateway } : {} } });
138
138
  }
139
139
 
140
+ /** What publishView reads, served from memory instead of the chain: `chain` maps
141
+ * `<owner>/<namespace>` to namespace contents ({ vertices: [{cid, schemaId, payload}],
142
+ * edges }). A build that must not touch the chain — a candidate recipe graded before it
143
+ * ships — runs the exact code a real one does. */
144
+ export function localFangorn(chain, { appId, owner }) {
145
+ return {
146
+ setAppId() {}, getAppId: () => appId,
147
+ getAppRegistry: () => ({ getAppOwner: async () => owner }),
148
+ appNamespaces: async ({ namespace }) => Object.keys(chain).filter((k) => k.endsWith(`/${namespace}`)).map((k) => ({ owner: k.split("/")[0] })),
149
+ readNamespace: async (o, ns) => { const c = chain[`${o}/${ns}`]; if (!c) throw new Error("no such namespace"); return { contents: c }; },
150
+ };
151
+ }
152
+
153
+ /** `embed`, remembered on disk by the exact text embedded (and the encoder). Two builds
154
+ * of the same data under different recipes then only pay for the text that differs.
155
+ * One ndjson file of packed vectors; packVec's fixed 1/127 scale makes the round trip exact.
156
+ * ponytail: the whole file is held in memory, ~400 bytes a record; shard it past a few million. */
157
+ export function cachedEmbed(path, embed = embedDocumentDirect) {
158
+ const seen = new Map();
159
+ // A build killed mid-write leaves half a line; that one vector is embedded again, nothing else is lost.
160
+ if (existsSync(path)) for (const l of readFileSync(path, "utf8").split("\n")) { try { const [k, v] = JSON.parse(l); seen.set(k, v); } catch { /* blank or cut short */ } }
161
+ const salt = JSON.stringify(EMBEDDER);
162
+ const fn = async (text) => {
163
+ const k = createHash("sha256").update(`${salt}\n${text}`).digest("hex");
164
+ let v = seen.get(k);
165
+ if (!v) { v = packVec(await embed(text)); seen.set(k, v); appendFileSync(path, `${JSON.stringify([k, v])}\n`); fn.misses++; }
166
+ return unpackVec(v);
167
+ };
168
+ fn.misses = 0;
169
+ return fn;
170
+ }
171
+
140
172
  // ── the view on disk ──────────────────────────────────────────────────────────
141
173
 
142
174
  const readJson = (p, dflt) => (existsSync(p) ? JSON.parse(readFileSync(p, "utf8")) : dflt);
@@ -342,15 +374,18 @@ if (typeof process !== "undefined" && import.meta.url === `file://${process.argv
342
374
  { cid: "c3", schemaId: "shop.note.v1", payload: { title: "Note", body: "an undeclared type, with a body long enough to count as its text field" } },
343
375
  ], edges: [] },
344
376
  };
345
- const fangorn = {
346
- setAppId() {}, getAppId: () => APP,
347
- getAppRegistry: () => ({ getAppOwner: async () => OWNER }),
348
- appNamespaces: async ({ namespace }) => Object.keys(chain).filter((k) => k.endsWith(`/${namespace}`)).map((k) => ({ owner: k.split("/")[0] })),
349
- readNamespace: async (o, ns) => { const c = chain[`${o}/${ns}`]; if (!c) throw new Error("no such namespace"); return { contents: c }; },
350
- };
377
+ const fangorn = localFangorn(chain, { appId: APP, owner: OWNER });
351
378
  const embedded = [];
352
379
  const embed = async (t) => { embedded.push(t); const v = new Array(EMBED_DIM).fill(0); for (const w of t.split(/\W+/)) if (w) v[w.length % EMBED_DIM] += 1; const n = Math.hypot(...v) || 1; return v.map((x) => x / n); };
353
380
  const out = mkdtempSync(`${tmpdir()}/view-`);
381
+
382
+ // The vector cache: a second ask for the same text never reaches the encoder, and reads back exactly.
383
+ const cache = cachedEmbed(`${out}/vectors.ndjson`, embed);
384
+ const v1 = await cache("Title: Oak"), again = cachedEmbed(`${out}/vectors.ndjson`, embed);
385
+ eq([packVec(await again("Title: Oak")), again.misses, embedded.length], [packVec(v1), 0, 1], "a cached vector comes back from disk, unchanged");
386
+ appendFileSync(`${out}/vectors.ndjson`, '["cut sho');
387
+ eq(packVec(await cachedEmbed(`${out}/vectors.ndjson`, embed)("Title: Oak")), packVec(v1), "a line cut short by a kill is skipped, not fatal");
388
+ embedded.length = 0; rmSync(`${out}/vectors.ndjson`);
354
389
  const run = () => publishView({ fangorn, app: APP, namespaces: ["shop"], out, embed, log: () => {} });
355
390
 
356
391
  const d = domainFor(APP, PUB, "shop");
@@ -0,0 +1,35 @@
1
+ // Types for the westmarch JS modules the TypeScript files import, by their package
2
+ // names. Only what those files use; widen as more of westmarch moves to TypeScript.
3
+
4
+ type Row = { id: string; text?: string; vector?: unknown; [field: string]: unknown };
5
+ type Roles = { title: string[]; subtitle: string[]; tags: string[]; text: string[]; declared?: boolean; [k: string]: unknown };
6
+ type Manifest = { role_map?: Record<string, unknown>; presentation?: Record<string, unknown>; [k: string]: unknown };
7
+
8
+ declare module "@fangorn-network/westmarch/shard" {
9
+ export function configure(hooks: {
10
+ rowText?: (fields: Record<string, unknown>, view: string) => string;
11
+ onManifests?: (manifests: Manifest[], view: string) => void;
12
+ }): void;
13
+ export function loadShard(base: string): Promise<Row[]>;
14
+ export function trimView(url: string): string;
15
+ }
16
+
17
+ declare module "@fangorn-network/westmarch/roles" {
18
+ export function rolesFrom(manifests?: Manifest[], sample?: Row[]): Roles;
19
+ export function textOf(row: Record<string, unknown>, roles: Roles): string;
20
+ export function titleOf(row: Row, roles: Roles): string;
21
+ export function subtitleOf(row: Row, roles: Roles): string | null;
22
+ export function values(v: unknown): string[];
23
+ }
24
+
25
+ declare module "@fangorn-network/westmarch/tools" {
26
+ export type Brief = { id: string; title: string; score: number; mode: string; [k: string]: unknown };
27
+ export function search(rows: Row[], query: string, roles: Roles,
28
+ opts?: { qv?: Float32Array | number[] | null; limit?: number; where?: Record<string, string>; lexBoost?: number }): Brief[];
29
+ export function matches(row: Row, where?: Record<string, string>): boolean;
30
+ }
31
+
32
+ declare module "@fangorn-network/westmarch/embed" {
33
+ export const EMBED_MODEL: string;
34
+ export function embedQueryDirect(text: string): Promise<Float32Array>;
35
+ }
package/site/agent.js CHANGED
@@ -33,4 +33,12 @@ export function registerAgent(ctx) {
33
33
  inputSchema: obj({ field: { type: "string" }, where: { type: "object" }, limit: { type: "number" } }, ["field"]),
34
34
  execute: async ({ field, where, limit = 20 }) => ok(facet(ctx.rows, field, { where, limit })),
35
35
  });
36
+ // What the person using this page has done: saved, passed on, searched, and the taste
37
+ // that makes. Theirs, in their browser; an agent in the same browser can act on it.
38
+ mc.registerTool({
39
+ name: "session",
40
+ description: "What the person on this page saved and searched this session, and the taste (a vector over this app's records) their choices make.",
41
+ inputSchema: obj({}),
42
+ execute: async () => ok(ctx.session?.() ?? { saved: [], searches: [], taste: null }),
43
+ });
36
44
  }
@@ -0,0 +1,8 @@
1
+ const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./transformers.web-u9uSoh5E.js","./rolldown-runtime-Dd_uD5pT.js","./preload-helper-uBIymjUX.js","./__vite-browser-external-CKnVkMRn.js","./roles-YDRjv_PY.js","./roles-C9V_uDr_.js"])))=>i.map(i=>d[i]);
2
+ import{i as e}from"./rolldown-runtime-Dd_uD5pT.js";import{t}from"./preload-helper-uBIymjUX.js";import{c as n,d as r,f as i,g as a,h as o,m as s,n as c,p as l,r as u,s as d}from"./roles-C9V_uDr_.js";(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),t.credentials=e.crossOrigin===`use-credentials`?`include`:e.crossOrigin===`anonymous`?`omit`:`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var f=`nomic-ai/nomic-embed-text-v1.5`,p=256,m=null,h=()=>m??=(async()=>{let{pipeline:e}=await t(async()=>{let{pipeline:e}=await import(`./transformers.web-u9uSoh5E.js`);return{pipeline:e}},__vite__mapDeps([0,1,2]),import.meta.url);return e(`feature-extraction`,f,{dtype:`q8`})})();function g(e,t=p){let n=e.length,r=0;for(let t=0;t<n;t++)r+=e[t];r/=n;let i=0;for(let t=0;t<n;t++)i+=(e[t]-r)**2;let a=Math.sqrt(i/n+1e-5),o=Array(Math.min(t,n));for(let t=0;t<o.length;t++)o[t]=(e[t]-r)/a;let s=0;for(let e of o)s+=e*e;if(s=Math.sqrt(s),s)for(let e=0;e<o.length;e++)o[e]/=s;return o}async function _(e){return g((await(await h())(`search_query: ${e}`,{pooling:`mean`,normalize:!1})).data)}var v=()=>typeof window<`u`&&typeof Worker<`u`,y=null,ee=0,b=new Map;function x(){return y||(y=new Worker(new URL(new URL(`embed.worker-CQqmvS52.js`,import.meta.url).href,``+import.meta.url),{type:`module`}),y.onmessage=({data:e})=>{let t=b.get(e.id);t&&(b.delete(e.id),e.error?t.reject(Error(e.error)):t.resolve(e.vec))},y.onerror=e=>{for(let t of b.values())t.reject(Error(`embed worker: ${e?.message??`failed to start`}`));b.clear(),y=null},y)}var te=(e,t)=>new Promise((n,r)=>{let i=++ee;b.set(i,{resolve:n,reject:r}),x().postMessage({id:i,kind:e,text:t})}),ne=e=>v()?te(`query`,e):_(e);function S(e){let t=new Uint8Array(e.length);for(let n=0;n<e.length;n++){let r=Math.round(Math.max(-1,Math.min(1,e[n]))*127);t[n]=r<0?r+256:r}let n=``;for(let e of t)n+=String.fromCharCode(e);return btoa(n)}function C(e){try{let t=atob(e),n=new Float32Array(t.length);for(let e=0;e<t.length;e++){let r=t.charCodeAt(e);n[e]=(r>127?r-256:r)/127}return n.length?n:null}catch{return null}}function w(e){let t=new Uint8Array(Math.ceil(e.length/8));for(let n=0;n<e.length;n++)e[n]>=0&&(t[n>>3]|=128>>(n&7));let n=``;for(let e of t)n+=String.fromCharCode(e);return btoa(n)}function re(e){try{let t=atob(e.padEnd(Math.ceil(e.length/4)*4,`=`)),n=new Float32Array(t.length*8);for(let e=0;e<n.length;e++)n[e]=t.charCodeAt(e>>3)>>7-(e&7)&1?1:-1;return n.length?n:null}catch{return null}}if(typeof process<`u`&&import.meta.url===`file://${process.argv[1]}`&&process.argv[1].endsWith(`/embed.js`)){let e=g(Array.from({length:768},(e,t)=>Math.sin(t))),t=0;for(let n of e)t+=n*n;if(Math.abs(Math.sqrt(t)-1)>1e-6)throw Error(`matryoshka must L2-normalize`);if(e.length!==p)throw Error(`matryoshka must truncate to ${p}`);let n=C(S(e));if(n.length!==e.length)throw Error(`pack/unpack changed the dimension`);let r=0,i=0;for(let t=0;t<e.length;t++)r+=e[t]*n[t],i+=n[t]*n[t];let a=r/Math.sqrt(i);if(a<.999)throw Error(`int8 round-trip cosine ${a} — too lossy to rank with`);let o=C(S([-1,-.5,0,.5,1]));if(o[0]>-.99||o[1]>-.49||o[1]<-.51)throw Error(`negative components corrupted: ${[...o]}`);if(C(`!!!not base64!!!`)!==null)throw Error(`malformed vector must return null, not throw`);let s=w(e);if(s.length>48)throw Error(`sign bits must stay link-sized, got ${s.length} chars`);let c=re(s);if(c.length!==e.length)throw Error(`sign round-trip changed the dimension`);for(let t=0;t<e.length;t++)if(Math.sign(c[t])!==(e[t]<0?-1:1))throw Error(`sign flipped at ${t}`);if(w(c)!==s)throw Error(`sign packing must be idempotent`);if(re(`!!!not base64!!!`)!==null)throw Error(`malformed bits must return null, not throw`);console.log(`embed.js self-check ok — matryoshka norm, int8 round-trip, negatives, malformed input, sign bits`)}var T=(e,t,n)=>{let r=0;for(let n=0;n<t.length&&n<e.vector.length;n++)r+=e.vector[n]*t[n];return r/(e.norm*n)},E=e=>{let t=0;for(let n of e)t+=n*n;return Math.sqrt(t)||1};function ie(e,t=1){if(e.length<3)return-1/0;let n=e.reduce((e,t)=>e+t,0)/e.length,r=Math.sqrt(e.reduce((e,t)=>e+(t-n)**2,0)/e.length);return r>1e-6?n+t*r:-1/0}function ae(e,{limit:t=10,lambda:n=.7,key:r=`s`}={}){if(n>=1||e.length<=1)return e.slice(0,t);let i=e.slice(0,Math.max(t*6,30)),a=[];for(;a.length<t&&i.length;){let e=0,t=-1/0;for(let o=0;o<i.length;o++){let s=0;for(let e of a){if(!i[o].row?.vector||!e.row?.vector)continue;let t=T(i[o].row,e.row.vector,e.row.norm);t>s&&(s=t)}let c=n*i[o][r]-(1-n)*s;c>t&&(t=c,e=o)}a.push(i.splice(e,1)[0])}return a}var oe=e=>e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`),se=e=>RegExp(`\\b${oe(e)}\\b`);function D(e,t){let n=String(e.text??``).toLowerCase();if(!n)return 0;let r=t.split(/\s+/).filter(Boolean);if(!r.length)return 0;let i=0;for(let e of r)se(e).test(n)&&i++;if(!i)return 0;let a=n.indexOf(t);return Math.min(1,.5*(i/r.length)+(se(t).test(n)?.35:0)+(a>=0?.15*(1-a/n.length):0))}function ce(e,t){let n=e[0],r=0;for(let i of e){let e=D({text:i.text},t);e>r&&(r=e,n=i)}return n}function le(e=[],t){let n=E(t);return e.map(e=>{let r=e.coverage;if(!r?.vectors?.length)return{...e,affinity:null};let i=-1;for(let e of r.vectors){let r=0,a=0;for(let n=0;n<e.length&&n<t.length;n++)r+=e[n]*t[n],a+=e[n]*e[n];let o=r/((Math.sqrt(a)||1)*n);o>i&&(i=o)}return{...e,affinity:i}}).sort((e,t)=>(t.affinity??-1/0)-(e.affinity??-1/0))}if(typeof process<`u`&&import.meta.url===`file://${process.argv[1]}`&&process.argv[1].endsWith(`/rank.js`)){if(D({text:`intoxicated education`},`cat`)!==0)throw Error(`substring matched — word boundaries lost`);if(D({text:`a cat sat`},`cat`)<=0)throw Error(`whole word must match`);let e=D({text:`cold war submarines`},`cold war`),t=D({text:`cold nights, the war ended`},`cold war`);if(!(e>t))throw Error(`phrase bonus lost: ${e} vs ${t}`);if(D({text:`what is c++`},`c++`)===void 0)throw Error(`must not throw on regex metachars`);if(Math.abs(T({vector:[1,0,0,0],norm:1},[1,0],1)-1)>1e-9)throw Error(`cosine must truncate to the query`);if(ie([.5,.5,.5,.5])!==-1/0)throw Error(`no spread must admit everything, not nothing`);if(ie([.4,.4])!==-1/0)throw Error(`too few rows to have a distribution`);let n=ie([.4,.41,.42,.9]);if(!(.9>=n&&.42<n))throw Error(`floor did not separate the outlier: ${n}`);let r=[{text:`opening titles`},{text:`the submarine dives`}];if(ce(r,`submarine`).text!==`the submarine dives`)throw Error(`wrong passage quoted`);if(ce(r,`nothing here`).text!==`opening titles`)throw Error(`must fall back to the first passage`);{let e=(e,t)=>({vector:[e,t],norm:Math.sqrt(e*e+t*t)||1}),t=[{row:{id:`ep1`,...e(1,0)},s:.9},{row:{id:`ep2`,...e(1,.01)},s:.89},{row:{id:`ep3`,...e(1,.02)},s:.88},{row:{id:`other`,...e(0,1)},s:.7}],n=ae(t,{limit:2,lambda:1}).map(e=>e.row.id);if(n.join()!==`ep1,ep2`)throw Error(`lambda 1 must be the old behaviour: ${n}`);let r=ae(t,{limit:2,lambda:.7}).map(e=>e.row.id);if(r.join()!==`ep1,other`)throw Error(`MMR must not return the same episode twice: ${r}`);if(ae(t,{limit:4,lambda:.5})[0].row.id!==`ep1`)throw Error(`the top hit must survive`);if(ae([{row:{id:`a`},s:1}],{limit:3}).length!==1)throw Error(`vectorless rows must pass through`)}let i=le([{name:`old`,coverage:null},{name:`far`,coverage:{vectors:[[0,1]]}},{name:`near`,coverage:{vectors:[[1,0]]}}],[1,0]);if(i.map(e=>e.name).join(`,`)!==`near,far,old`)throw Error(`domains misranked: ${i.map(e=>e.name)}`);if(i[2].affinity!==null)throw Error(`an unmeasured domain must be null, not 0`);console.log(`rank.js self-check ok — word boundaries, matryoshka cosine, z-floor, MMR drops near-duplicates without moving the top hit, passage pick, domain affinity`)}var ue=null,O=null,k=null,de=new Map,fe=new Map,pe=new Map,me=e=>[e.name,e.path,e.desc].filter(Boolean).join(` `),he=null,ge=null;function A({resolveView:e,rowText:t,onManifests:n}={}){e&&(he=e),t&&(me=t),n&&(ge=n)}var j=()=>{de.clear(),fe.clear(),pe.clear(),ue=null,O=null,k=null};function _e(e){O=e?ye(e):null}var ve=()=>O,ye=e=>String(e).replace(/\/+$/,``).replace(/\/(stream|cdn)$/,``);function be(){return he?ue??=Promise.resolve().then(()=>he()).then(e=>e?ye(e):null).catch(e=>{throw ue=null,e}):Promise.resolve(null)}function xe(e){let t=(Array.isArray(e)?e:String(e??``).split(`,`)).map(e=>String(e).trim()).filter(Boolean);k=t.length?t:null}var Se=e=>!k||k.some(t=>e===t||e.split(`/`).pop()===t||e.startsWith(t)),Ce=`no quickbeam view configured — pass a view base, or wire one with configure({ resolveView })`;async function M(e,t){let n=e??O??await be();if(!n)throw Error(Ce);let r=fe.get(n);r||fe.set(n,r=new Set),t&&r.add(t);let i=de.get(n);if(i)return i;let a=je(n,e=>{for(let t of r)try{t(e)}catch{}}).finally(()=>fe.delete(n)).catch(e=>{throw de.delete(n),e});return de.set(n,a),a}async function we(e){let t=await fetch(e);if(!t.ok)throw Error(`shard: HTTP ${t.status} for ${e}`);return Te(new Uint8Array(await t.arrayBuffer()))}var Te=async e=>e[0]===31&&e[1]===139?await new Response(new Blob([e]).stream().pipeThrough(new DecompressionStream(`gzip`))).text():new TextDecoder().decode(e);function Ee(e,t){let n=e.fields??{},r=typeof e.v==`string`?C(e.v):Array.isArray(e.embedding)?e.embedding:typeof n.embed?.vec==`string`?C(n.embed.vec):null,i=0;if(r)for(let e of r)i+=e*e;return{id:e.track_id,owner:e.owner,...n,text:me(n,t),vector:r,norm:Math.sqrt(i)||1}}async function De(e){let t=0;for(let n of e)t+=n.length;let n=new Uint8Array(t),r=0;for(let t of e)n.set(t,r),r+=t.length;return[...new Uint8Array(await crypto.subtle.digest(`SHA-256`,n))].map(e=>e.toString(16).padStart(2,`0`)).join(``)}async function Oe(e,t,n,r){if(!t)return;if(!r){console.warn(`shard: cannot verify ${e} — the transport decoded it (Content-Encoding), and the manifest digest is over the gzipped bytes`);return}let i=await De(n);if(i!==t)throw Error(`shard: ${e} does not match the digest in its manifest (expected ${t.slice(0,12)}…, got ${i.slice(0,12)}…)`)}async function ke(e,t,n){let r=await fetch(e);if(!r.ok)throw Error(`shard: HTTP ${r.status} for ${e}`);if(!r.body){let i=new Uint8Array(await r.arrayBuffer());await Oe(e,n,[i],i[0]===31&&i[1]===139);for(let e of(await Te(i)).split(`
3
+ `))t(e);return}let i=r.body.getReader(),a=await i.read(),o=a.value??new Uint8Array,s=n?[]:null,c=new ReadableStream({start(e){o.length&&(s?.push(o),e.enqueue(o)),a.done&&e.close()},async pull(e){let{done:t,value:n}=await i.read();t?e.close():(s?.push(n),e.enqueue(n))},cancel(e){return i.cancel(e)}}),l=o[0]===31&&o[1]===139,u=l?c.pipeThrough(new DecompressionStream(`gzip`)):c,d=new TextDecoder,f=``;for(let r=u.getReader();;){let{done:i,value:a}=await r.read();f+=i?d.decode():d.decode(a,{stream:!0});let o=f.split(`
4
+ `);f=i?``:o.pop();for(let e of o)t(e);if(i){f&&t(f),await Oe(e,n,s,l);return}}}var Ae=250;async function je(e,t){let n=`${e}/cdn`,{domains:r=[]}=JSON.parse(await we(`${n}/catalog`)),i=r.filter(e=>Se(e.name));k&&i.length!==r.length&&console.info(`shard: pulling ${i.length} of ${r.length} domains (allowlist: ${k.join(`,`)})`),k&&!i.length&&console.warn(`shard: allowlist ${k.join(`,`)} matched none of ${r.map(e=>e.name).join(`, `)} — nothing will load`),i.length||console.warn(`shard: quickbeam view ${n} has no baked domains yet — nothing to read until the watcher bakes one.`);let a=new Map,o=new Set,s=await Promise.all(i.map(async e=>{let t=`${n}/domains/${encodeURIComponent(e.name)}`,r=JSON.parse(await we(`${t}/manifest`));for(let e of r.tombstones??[])o.add(e);return{at:t,manifest:r,shards:r.shards??[]}})),c=s.map(e=>e.manifest);pe.set(e,c);try{ge?.(c,e)}catch(e){console.warn(`shard: onManifests threw, continuing with the default rowText —`,e)}let l=Date.now(),u=e=>{!t||!e&&Date.now()-l<Ae||(t([...a.values()]),l=Date.now())};return await Promise.all(s.map(async({at:t,shards:n})=>{for(let r of n)await ke(`${t}/shards/${r.file}`,t=>{if(!t.trim())return;let n=Ee(JSON.parse(t),e);o.has(n.id)||a.set(n.id,n),u()},r.sha256),u(!0)})),[...a.values()]}function Me(e,t){let n=null,r=!1;return(async()=>{let i=t??O??await be();if(!i||r)return;n=new EventSource(`${i}/stream`);let a=()=>{j(),e()};n.addEventListener(`added`,a),n.addEventListener(`change`,a)})().catch(()=>{}),()=>{r=!0,n?.close()}}async function Ne(e,{url:t}={}){let n=t??O??await be();if(!n)throw Error(Ce);let{domains:r=[]}=JSON.parse(await we(`${n}/cdn/catalog`));return le(r,e)}if(typeof process<`u`&&import.meta.url===`file://${process.argv[1]}`&&process.argv[1].endsWith(`/shard.js`)){let{gzipSync:n}=await t(async()=>{let{gzipSync:t}=await import(`./__vite-browser-external-CKnVkMRn.js`).then(t=>e(t.default,1));return{gzipSync:t}},__vite__mapDeps([3,1]),import.meta.url),r=console.warn,i=console.info;console.warn=console.info=()=>{};let a=(e,t,n={},r)=>({track_id:`${e}:${t}`,owner:e,...r?{embedding:r}:{},fields:{entityType:`file`,name:t.split(`/`).pop(),path:t,...n}}),o=e=>e.map(e=>JSON.stringify(e)).join(`
5
+ `),s=`https://registry.test/q/qb_test`,c=e=>{let t=Buffer.from(e),n=Math.max(1,Math.floor(t.length/2));return{ok:!0,headers:new Headers,arrayBuffer:async()=>t,body:new ReadableStream({start(e){e.enqueue(new Uint8Array(t.subarray(0,n))),e.enqueue(new Uint8Array(t.subarray(n))),e.close()}})}},l=async e=>[...new Uint8Array(await crypto.subtle.digest(`SHA-256`,e))].map(e=>e.toString(16).padStart(2,`0`)).join(``),u=async(e,{tombstones:t={},gzipped:r=!0,keep:i=!1,coverage:a={},digest:u=!1,corrupt:d=!1}={})=>{let f=new Map;for(let[r,i]of Object.entries(e)){let e=i.map((e,t)=>`shard-000${t}-${r}.ndjson.gz`),a=new Map(e.map((e,t)=>[e,o(i[t])])),s=u?await Promise.all(e.map(async e=>l(n(Buffer.from(d?a.get(e)+`
6
+ `:a.get(e)))))):e.map(()=>void 0);f.set(r,{manifest:JSON.stringify({name:r,shards:e.map((e,t)=>({file:e,...s[t]?{sha256:s[t]}:{}})),tombstones:t[r]??[]}),bodies:a})}globalThis.fetch=async e=>{let{pathname:t}=new URL(String(e),`https://app.test`);if(t.includes(`/stream/`))return{ok:!1,status:404};if(t.endsWith(`/cdn/catalog`))return c(JSON.stringify({domains:[...f.keys()].map(e=>({name:e,...a[e]?{coverage:a[e]}:{}}))}));let i=t.match(/\/cdn\/domains\/([^/]+)\/manifest$/);if(i)return f.has(i[1])?c(f.get(i[1]).manifest):{ok:!1,status:404};if(i=t.match(/\/cdn\/domains\/([^/]+)\/shards\/([^/]+)$/),i){let e=f.get(i[1])?.bodies.get(i[2]);return e===void 0?{ok:!1,status:404}:c(r?n(Buffer.from(e)):Buffer.from(e))}return{ok:!1,status:404}},i||j(),A({resolveView:()=>`${s}/stream`})},d=async(e,t={})=>u({d0:e},{...t,tombstones:{d0:t.tombstones??[]}}),f=e=>e.map(e=>e.path).sort().join(`,`);if(await d([[a(`0xa`,`a.md`)]]),f(await M())!==`a.md`)throw Error(`resolveView never reached the loader`);if(await d([[a(`0xa`,`plain.md`)]],{gzipped:!1}),f(await M())!==`plain.md`)throw Error(`an already-decoded body must still parse`);if(await d([[a(`0xa`,`signed.md`)]],{digest:!0}),f(await M())!==`signed.md`)throw Error(`a shard matching its digest must load`);await d([[a(`0xa`,`tampered.md`)]],{digest:!0,corrupt:!0});let p=null;try{await M()}catch(e){p=e}if(!p)throw Error(`a shard that does not match its digest must fail the load`);if(!/does not match the digest/.test(p.message))throw Error(`wrong error: ${p.message}`);p=null;try{await M()}catch(e){p=e}if(!p)throw Error(`a failed verification must not leave the rows in the cache`);let m=``;if(console.warn=e=>{m+=e},await d([[a(`0xa`,`unverifiable.md`)]],{digest:!0,gzipped:!1}),f(await M())!==`unverifiable.md`)throw Error(`an unverifiable body must still load`);if(!/cannot verify/.test(m))throw Error(`an unverifiable shard must say so, not pass silently`);if(console.warn=()=>{},await u({aaa:[[a(`0xa`,`a.md`,{desc:`alpha`}),a(`0xa`,`gone.md`,{desc:`retracted`})]],bbb:[[a(`0xb`,`b.md`,{desc:`beta`})]]},{tombstones:{aaa:[`0xa:gone.md`]}}),f(await M())!==`a.md,b.md`)throw Error(`domains must merge and tombstones retract`);for(let e of[`bbb`,[`bbb`],`bb`])if(await u({aaa:[[a(`0xa`,`a.md`)]],bbb:[[a(`0xb`,`b.md`)]]}),xe(e),f(await M())!==`b.md`)throw Error(`allowlist ${JSON.stringify(e)} did not narrow the load`);if(await u({aaa:[[a(`0xa`,`a.md`)]]}),xe(null),f(await M())!==`a.md`)throw Error(`a null allowlist must pull everything`);await d([[a(`0xa`,`a.md`),a(`0xa`,`gone.md`)],[a(`0xa`,`b.md`)]],{tombstones:[`0xa:gone.md`]});{let e=[],t=await M(void 0,t=>e.push(t.length));if(e.length!==2||e[0]!==1||e[1]!==2)throw Error(`must emit per shard, got ${e}`);if(t.length!==2)throw Error(`the final rows must still be whole`);if(e.some(e=>e>2))throw Error(`a tombstoned row must never surface in a partial emit`)}await d([[a(`0xa`,`a.md`)],[a(`0xa`,`b.md`)]]);{let e=[],t=[],n=M(void 0,t=>e.push(t.length)),r=M(void 0,e=>t.push(e.length));if(await Promise.all([n,r]),!e.length)throw Error(`the starting caller must still receive partials`);if(!t.length)throw Error(`a caller that joins an in-flight load must receive partials too`)}if(j(),A({resolveView:()=>`${s}/stream`}),globalThis.fetch=async()=>({ok:!1,status:500}),await M().then(()=>{throw Error(`a 500 must reject`)},()=>{}),await d([[a(`0xa`,`a.md`)]],{keep:!0}),f(await M())!==`a.md`)throw Error(`a failed load poisoned the cache — the next read must retry`);{let e=[];await u({d0:[[a(`0xa`,`a.md`,{blurb:`declared prose`})]]}),A({onManifests:t=>e.push(`manifests:${t[0]?.name}`),rowText:t=>(e.push(`rowText`),t.blurb??``)});let t=await M();if(e[0]!==`manifests:d0`)throw Error(`onManifests must fire first, got ${e}`);if(!e.includes(`rowText`))throw Error(`rowText never ran`);if(t[0].text!==`declared prose`)throw Error(`the manifest-derived rowText did not reach toRow`);if(j(),A({resolveView:()=>`${s}/stream`,onManifests:()=>{throw Error(`boom`)}}),(await M()).length!==1)throw Error(`a throwing onManifests must not fail the load`);A({onManifests:()=>{}})}if(await d([[a(`0xa`,`ep1.md`,{cues:[{text:`the submarine dives`}]})]]),(await M())[0].text.includes(`submarine`))throw Error(`the default rowText must not know about cues`);if(A({rowText:e=>e.cues?.length?e.cues.map(e=>e.text).join(` `):[e.name,e.path,e.desc].filter(Boolean).join(` `)}),j(),A({resolveView:()=>`${s}/stream`}),!(await M())[0].text.includes(`submarine`))throw Error(`a configured rowText must reach toRow`);{let e=new Set;if(A({rowText:(t,n)=>(e.add(n),t.name??``),onManifests:(t,n)=>{e.add(`m:${n}`)}}),j(),A({resolveView:()=>`${s}/stream`}),await M(),!e.has(s))throw Error(`rowText must receive its trimmed view base, saw ${[...e]}`);if(!e.has(`m:${s}`))throw Error(`onManifests must receive its view`);A({rowText:e=>[e.name,e.path,e.desc].filter(Boolean).join(` `),onManifests:()=>{}})}await d([[a(`0xa`,`v.md`,{},[3,4]),a(`0xa`,`packed.md`,{embed:{vec:`fwA=`}}),a(`0xa`,`none.md`)]]);{let e=new Map((await M()).map(e=>[e.path,e]));if(Math.abs(e.get(`v.md`).norm-5)>1e-9)throw Error(`norm must be precomputed from the plain float array`);if(!e.get(`packed.md`).vector)throw Error(`the base64 int8 form a publisher commits must unpack`);if(e.get(`none.md`).vector!==null)throw Error(`a row with no vector must be null, and rank lexically`);if(e.get(`none.md`).norm!==1)throw Error(`a zero norm would divide search by zero`)}{await u({d0:[[{track_id:`p1`,owner:`0xa`,v:`AH+B`,fields:{entityType:`file`,name:`packed.md`,path:`packed.md`}},{track_id:`p2`,owner:`0xa`,embedding:[0,1,-1],fields:{entityType:`file`,name:`old.md`,path:`old.md`}}]]});let e=new Map((await M()).map(e=>[e.path,e])),t=e.get(`packed.md`).vector,n=e.get(`old.md`).vector;if(!t)throw Error("the bake's `v` key must unpack");for(let e=0;e<3;e++)if(Math.abs(t[e]-n[e])>1/127)throw Error(`packed and float forms disagree at ${e}: ${t[e]} vs ${n[e]}`);if(Math.abs(e.get(`packed.md`).norm-e.get(`old.md`).norm)>.02)throw Error(`norm must match across wire forms`)}await u({near:[[a(`0xa`,`a.md`)]],far:[[a(`0xb`,`b.md`)]],old:[[a(`0xc`,`c.md`)]]},{coverage:{near:{vectors:[[1,0]]},far:{vectors:[[0,1]]}}});{let e=await Ne([1,0]);if(e.map(e=>e.name).join(`,`)!==`near,far,old`)throw Error(`suggestDomains misranked: ${e.map(e=>e.name)}`)}await d([[a(`0xa`,`before.md`)]]);{let e=new Map,t=``,n=!1;globalThis.EventSource=class{constructor(e){t=e}addEventListener(t,n){e.set(t,n)}close(){n=!0}};let r=0,i=Me(()=>{r++});if(await new Promise(e=>setTimeout(e,0)),t!==`${s}/stream`)throw Error(`watchShard must open the view's SSE stream, got ${t}`);if(e.has(`snapshot`))throw Error(`snapshot is the connect-time census, not a change`);if(f(await M())!==`before.md`)throw Error(`watchShard fixture never loaded`);if(await d([[a(`0xa`,`after.md`)]],{keep:!0}),e.get(`change`)(),r!==1)throw Error("a `change` event must reach the caller");if(f(await M())!==`after.md`)throw Error("a `change` must drop the cache so the next read re-pulls");if(i(),!n)throw Error(`the unsubscribe must close the stream`)}if(await u({d0:[[a(`0xa`,`home.md`)]],other:[[a(`0xb`,`guest.md`)]]}),await M(),_e(`${s}/cdn`),ve()!==s)throw Error(`setView must trim to the base every route hangs off`);if(_e(null),ve()!==null)throw Error(`setView(null) must go back to the app default`);console.warn=r,console.info=i,console.log(`shard.js self-check ok — resolveView, gzip sniff, shard digests, domain merge + tombstones + allowlist, streaming, late subscriber, cache poisoning, rowText, vectors, suggestDomains, watchShard`)}var Pe=new Set([`text`,`vector`,`norm`,`id`,`owner`,`embed`]),Fe=(e,t,n=[])=>{let r=c(e,t,n),i=d(e,t);return i?{...r,url:i}:r};function Ie(e){let t=new Map,n=0,r=0;for(let i of e){i.vector&&(n++,r||=i.vector.length);for(let e of Object.keys(i))Pe.has(e)||i[e]!=null&&i[e]!==``&&t.set(e,(t.get(e)??0)+1)}return{rows:e.length,withVectors:n,vectorDim:r,owners:[...new Set(e.map(e=>e.owner).filter(Boolean))],fields:[...t].sort((e,t)=>t[1]-e[1]).map(([t,n])=>({name:t,on:n,pct:Math.round(100*n/(e.length||1))}))}}var Le=.15,Re=new WeakMap,ze=(e,t)=>{let n=Re.get(e);return n===void 0&&(n=[s(e,t),i(e,t)??``,...(t.tags??[]).flatMap(t=>a(e[t]??``)),e.text??``].join(` `),Re.set(e,n)),n};function N(e,t,n,{qv:r=null,limit:i=10,fields:a,where:o,lexBoost:s=Le}={}){let c=t.trim().toLowerCase();if(!c)return[];let l=r?E(r):1;o&&(e=e.filter(e=>Be(e,o)));let u=e.map(e=>r&&e.vector?{r:e,score:T(e,r,l)+(s?s*D({text:ze(e,n)},c):0),mode:`semantic`}:{r:e,score:D({text:ze(e,n)},c),mode:`lexical`}),d=u.filter(e=>e.mode===`semantic`).map(e=>e.score),f=Math.min(ie(d),d.length?Math.max(...d):1/0);return u.filter(e=>e.mode===`semantic`?e.score>=f:e.score>0).sort((e,t)=>t.score-e.score).slice(0,i).map(({r:e,score:t,mode:r})=>({...Fe(e,n,a),score:Number(t.toFixed(4)),mode:r}))}var Be=(e,t)=>!t||Object.entries(t).every(([t,n])=>a(e[t]??``).some(e=>e.toLowerCase()===String(n).toLowerCase()));function P(e,t,{limit:n=20,where:r}={}){let i=new Map,o=0;for(let n of e){if(!Be(n,r))continue;let e=n[t];if(e==null||e===``){o++;continue}for(let t of a(e))i.set(t,(i.get(t)??0)+1)}let s=[...i].sort((e,t)=>t[1]-e[1]);return{field:t,distinct:i.size,missing:o,top:s.slice(0,n).map(([e,t])=>({value:e,count:t}))}}function F(e,t,{type:n,where:r,sort:i,desc:a=!0,limit:s=20,offset:c=0,fields:l}={}){let d=e.filter(e=>(!n||o(e)?.toLowerCase()===String(n).toLowerCase())&&Be(e,r)),f=d.length;if(i){let e=e=>e==null||e===``?null:Number(e);d=[...d].sort((t,n)=>{let[r,o]=[t[i],n[i]];if((r==null||r===``)!=(o==null||o===``))return r==null||r===``?1:-1;let[s,c]=[e(r),e(o)],l=Number.isFinite(s)&&Number.isFinite(c)?s-c:String(r??``).localeCompare(String(o??``));return a?-l:l})}return{total:f,offset:c,returned:Math.min(s,Math.max(0,f-c)),collections:n?void 0:u(t,e).map(e=>({type:e.type,plural:e.plural,count:e.count})),rows:d.slice(c,c+s).map(e=>Fe(e,t,l))}}function Ve(e,t,n,{limit:r=10,fields:i}={}){let a=e.find(e=>e.id===t);if(!a?.vector)return{seed:a?Fe(a,n,i):null,near:[]};let o=a.norm;return{seed:Fe(a,n,i),near:e.filter(e=>e.id!==t&&e.vector).map(e=>({r:e,score:T(e,a.vector,o)})).sort((e,t)=>t.score-e.score).slice(0,r).map(({r:e,score:t})=>({...Fe(e,n,i),score:Number(t.toFixed(4))}))}}function He(e,t,n){if(!t)return null;let r=e.find(e=>e.id===t)??(n?.identity?e.find(e=>String(e[n.identity])===String(t)):null)??e.find(e=>e.path===t||e.name===t||s(e,n)===t);if(!r)return null;let{vector:i,norm:a,...o}=r;return{...o,hasVector:!!i,vectorDim:i?.length??0}}if(typeof process<`u`&&import.meta.url===`file://${process.argv[1]}`&&process.argv[1].endsWith(`/tools.js`)){let{rolesFrom:e}=await t(async()=>{let{rolesFrom:e}=await import(`./roles-YDRjv_PY.js`);return{rolesFrom:e}},__vite__mapDeps([4,5]),import.meta.url),n=(e,t,n)=>({id:e,owner:`0xa`,...t,text:[t.name,t.desc].filter(Boolean).join(` `),vector:n??null,norm:n?E(n):1}),r=[n(`1`,{name:`Duet S01E02`,series:`Duet`,year:`1987`,subject:`['Sitcom', 'Comedy']`,desc:`a forgotten fox sitcom`},[1,0,0]),n(`2`,{name:`Duet S01E03`,series:`Duet`,year:`1987`,subject:`['Sitcom']`,desc:`more of the same sitcom`},[.99,.1,0]),n(`3`,{name:`Atomic Cafe`,series:``,year:`1982`,subject:`['Documentary', 'Cold War']`,desc:`nuclear test footage`},[0,1,0]),n(`4`,{name:`untitled reel`,year:`1954`})],i=e([],r);if(i.title[0]!==`name`)throw Error(`fixture roles wrong: ${i.title}`);let o=Ie(r);if(o.rows!==4||o.withVectors!==3||o.vectorDim!==3)throw Error(`describe lost the corpus shape`);let s=o.fields.find(e=>e.name===`series`);if(s.on!==2||s.pct!==50)throw Error(`empty strings must not count as coverage: ${JSON.stringify(s)}`);if(o.fields.some(e=>Pe.has(e.name)))throw Error(`plumbing fields must not be offered as content`);if(o.owners.join()!==`0xa`)throw Error(`owners lost`);let c=N(r,`sitcom`,i,{qv:[1,0,0],limit:10});if(c[0].id!==`1`||c[0].title!==`Duet S01E02`)throw Error(`semantic: wrong top hit ${c[0]?.id}`);if(c[0].mode!==`semantic`)throw Error(`mode must be reported, not inferred`);if(c.some(e=>e.id===`3`))throw Error(`the z-floor must drop the unrelated row: ${JSON.stringify(c)}`);if(!N([r[0],r[1],r[2]],`sitcom`,i,{qv:[1,0,0]}).length)throw Error(`an exact match must never be filtered out by its own floor`);if(c=N(r,`nuclear`,i,{limit:10}),c[0]?.id!==`3`||c[0].mode!==`lexical`)throw Error(`must degrade to lexical, and say so`);if(N(r,` `,i).length)throw Error(`an empty query must return nothing, not everything`);if(N(r,`untitled reel`,i)[0]?.id!==`4`)throw Error(`a vectorless row must still be searchable lexically`);let l=P(r,`subject`);if(l.top[0].value!==`Sitcom`||l.top[0].count!==2)throw Error(`list-ish values must split: ${JSON.stringify(l.top)}`);if(l.missing!==1)throw Error(`a field absent from a row must be counted missing, not skipped silently`);if(P(r,`year`).distinct!==3)throw Error(`plain values must not be split`);if(a(`Sitcom,Comedy,Fox`).length!==3)throw Error(`a machine-joined list must split`);if(a(`Wilson, Michael G.`).length!==1)throw Error(`a comma followed by a space is prose, not a list`);if(a(`1987`).length!==1)throw Error(`a plain value must stay whole`);let u=P(r,`year`,{where:{subject:`Sitcom`}});if(u.distinct!==1||u.top[0].value!==`1987`)throw Error(`where must narrow the facet: ${JSON.stringify(u.top)}`);if(P(r,`year`,{where:{subject:`sitcom`}}).distinct!==1)throw Error(`where must match case-insensitively`);if(P(r,`year`,{where:{subject:`Sit`}}).distinct!==0)throw Error(`where must match whole values, not substrings`);let d=F(r,i,{limit:2});if(d.total!==4||d.rows.length!==2)throw Error(`browse must page: ${JSON.stringify(d)}`);if(d.collections.length!==0)throw Error(`a typeless corpus must report zero collections, not invent one`);if(F(r,i,{offset:3}).rows.length!==1)throw Error(`offset must page to the tail`);if(F(r,i,{offset:99}).rows.length!==0)throw Error(`an offset past the end must be empty, not wrapped`);let f=r.map(e=>({...e,entityType:e.id===`3`?`Film`:`Episode`})),p=F(f,i,{limit:1}).collections;if(p.map(e=>`${e.type}:${e.count}`).join()!==`Episode:3,Film:1`)throw Error(`collections must count and sort: ${JSON.stringify(p)}`);if(F(f,i,{type:`film`}).total!==1)throw Error(`type must match case-insensitively`);if(F(f,i,{type:`film`}).collections!==void 0)throw Error(`a typed browse is already inside a collection`);if(F(f,i,{type:`Episode`,where:{year:`1987`}}).total!==2)throw Error(`browse must honour where`);let m=[{id:`a`,rating:`9`},{id:`b`,rating:`10`},{id:`c`}],h=F(m,i,{sort:`rating`}).rows.map(e=>e.id);if(h.join()!==`b,a,c`)throw Error(`numeric sort + missing-last wrong: ${h}`);if(F(m,i,{sort:`rating`,desc:!1}).rows[0].id!==`a`)throw Error(`ascending sort ignored`);let g=Ve(r,`1`,i);if(g.near[0].id!==`2`)throw Error(`nearest neighbour missed the near-identical row`);if(g.near.some(e=>e.id===`1`))throw Error(`a row must not be its own neighbour`);if(g.near.some(e=>e.id===`4`))throw Error(`a vectorless row cannot be a neighbour`);if(Ve(r,`4`,i).near.length)throw Error(`a vectorless seed must return nothing, not throw`);if(Ve(r,`nope`,i).seed!==null)throw Error(`an unknown seed must be null, not undefined`);let _=He(r,`3`,i);if(_.name!==`Atomic Cafe`||!_.hasVector||_.vectorDim!==3)throw Error(`getRow lost the record`);if(`vector`in _||`norm`in _)throw Error(`the raw vector must never reach a tool result`);if(He(r,`nope`,i)!==null)throw Error(`an unknown id must be null`);let v=Fe(n(`x`,{name:`n`,categories:`d`.repeat(500)}),e([{role_map:{title:`name`,tags:[`categories`]}}]));if(v.categories.length>200)throw Error(`brief must truncate a long field`);if(v.title!==`n`)throw Error(`brief must title from the declared field`);{let e=[{id:`a`,name:`cold war thriller`,year:1965,vector:[1,0],norm:1},{id:`b`,name:`cold war comedy`,year:1999,vector:[1,0],norm:1},{id:`c`,name:`beach party`,year:1964,vector:[0,1],norm:1},{id:`d`,name:`surf movie`,year:1966,vector:[0,1],norm:1}],t={title:[`name`],subtitle:[],tags:[],text:[`name`],measures:[],fields:[`year`]};if(!N(e,`cold war`,t,{qv:[1,0],limit:10}).some(e=>e.title===`cold war comedy`))throw Error(`unfiltered search must see every era`);let n=N(e,`cold war`,t,{qv:[1,0],limit:10,where:{year:1965}});if(n.length!==1||n[0].title!==`cold war thriller`)throw Error(`where must narrow a semantic search: ${JSON.stringify(n)}`);if(N(e,`cold war`,t,{qv:[0,1],limit:10,where:{year:1964}})[0]?.title!==`beach party`)throw Error(`the floor must be measured over the filtered rows`)}{let e=[{id:`a`,name:`cold war thriller`,vector:[1,0],norm:1}],t={title:[`name`],subtitle:[],tags:[],text:[`name`],measures:[],fields:[]},n=N(e,`cold war`,t,{qv:[1,0],limit:5});if(!Array.isArray(n))throw Error(`search must return a flat array, not {hits}`);if(n[0].score===void 0||n[0].mode===void 0)throw Error(`search rows carry score+mode`);for(let n of[void 0,null,``])if(He(e,n,t)!==null)throw Error(`getRow(${JSON.stringify(n)}) must be null`)}console.log(`tools.js self-check ok — coverage not schema, semantic/lexical modes + z-floor, list-ish facets + where, neighbours, whole-row fetch, previews truncate`)}var Ue=e=>e.reduce((e,t)=>Math.max(e,t.length),0);function We(e,t){let n=Ue(e);if(!n)return null;let r=new Float32Array(n),i=0;if(e.forEach((e,n)=>{let a=t[n];i+=a;for(let t=0;t<e.length;t++)r[t]+=e[t]*a}),!i)return null;for(let e=0;e<n;e++)r[e]/=i;return r}var Ge=e=>{if(!e)return null;let t=E(e),n=new Float32Array(e.length);for(let r=0;r<e.length;r++)n[r]=e[r]/t;return n};function I(e=[],t=[],{drift:n=.35}={}){let r=e.filter(e=>e?.vector?.length);if(!r.length)return null;let i=r.map((e,t)=>.5**((r.length-1-t)/8)),a=Ge(We(r.map(e=>e.vector),i)),o=null;if(r.length>=4){let e=Math.floor(r.length/2),t=We(r.slice(e).map(e=>e.vector),i.slice(e)),n=We(r.slice(0,e).map(e=>e.vector),i.slice(0,e));if(t&&n){let e=new Float32Array(t.length);for(let r=0;r<e.length;r++)e[r]=t[r]-n[r];E(e)>1e-6&&(o=Ge(e))}}let s=new Float32Array(a.length);for(let e=0;e<s.length;e++)s[e]=a[e]+(o?n*o[e]:0);let c=t.filter(e=>e?.vector?.length),l=c.length?Ge(We(c.map(e=>e.vector),c.map(()=>1))):null;return{mu:a,v:o,q:Ge(s),no:l,drift:n,from:r.map(e=>e.title??e.id).slice(-12),rejected:c.map(e=>e.title??e.id).slice(-12),n:r.length}}var Ke=.6,qe=(e,t)=>{if(!e?.vector||!t)return null;let n={vector:e.vector,norm:e.norm??E(e.vector)},r=T(n,t.q,E(t.q));return t.no?r-Ke*Math.max(0,T(n,t.no,E(t.no))):r};function L(e,t,{limit:n=10,exclude:r=new Set,lambda:i=.7}={}){return t?ae(e.filter(e=>e.vector&&!r.has(e.id)).map(e=>({row:e,s:qe(e,t)})).sort((e,t)=>t.s-e.s),{limit:n,lambda:i}).map(({row:e,s:t})=>({row:e,score:Number(t.toFixed(4))})):[]}var R=e=>e&&{v:1,model:`nomic-256`,drift:e.drift,n:e.n,q:S(Array.from(e.q)),mu:S(Array.from(e.mu)),...e.v?{d:S(Array.from(e.v))}:{},...e.no?{no:S(Array.from(e.no))}:{},from:e.from,rejected:e.rejected};function Je(e){if(!e?.q)return null;let t=C(e.q),n=C(e.mu??e.q);return t?{q:t,mu:n??t,v:e.d?C(e.d):null,no:e.no?C(e.no):null,drift:e.drift??.35,from:e.from??[],rejected:e.rejected??[],n:e.n??0}:null}var Ye=`(remembered`;function Xe(e,{carried:t=null,likes:n=[],dislikes:r=[]}={}){if(!e)return e;let i=t??{from:[],rejected:[],n:0},a=e=>e.filter(e=>!String(e?.id??``).startsWith(Ye)).map(e=>e.title??e.id),o=a(n);return{...e,v:e.v??i.v??null,from:[...i.from??[],...o].slice(-12),rejected:[...i.rejected??[],...a(r)].slice(-12),n:(i.n??0)+o.length}}if(typeof process<`u`&&import.meta.url===`file://${process.argv[1]}`&&process.argv[1].endsWith(`/taste.js`)){{let e=L([{id:`a`,vector:[1,0],norm:1},{id:`b`,vector:[0,1],norm:1}],{q:[1,0]});if(!e.length)throw Error(`recommend must rank rows with vectors`);if(e[0].row===void 0)throw Error(`recommend rows carry .row`);if(typeof e[0].score!=`number`)throw Error(`recommend rows carry .score (not .s)`)}let e=(e,t,n=e)=>({id:e,title:n,vector:Float32Array.from(t),norm:E(t)}),t=[1,0,0],n=[0,1,0],r=[0,0,1],i=(e,t,n)=>e.map((e,r)=>e*(1-n)+t[r]*n),a=I([e(`a`,t),e(`b`,i(t,r,.2)),e(`c`,i(t,r,.1))]);if(!a)throw Error(`three likes must make a taste`);if(a.v!==null)throw Error(`under four picks there is no older half — direction must not be invented`);if(T({vector:a.q,norm:E(a.q)},Float32Array.from(t),1)<.9)throw Error(`q must sit near what was liked`);let o=[e(`x`,n,`a comedy`),e(`y`,i(t,r,.15),`a bleak documentary`),e(`z`,r,`a documentary`)];if(L(o,a)[0].row.title!==`a bleak documentary`)throw Error(`taste must rank an unseen pool`);let s=[e(`1`,t),e(`2`,i(t,n,.2)),e(`3`,i(t,n,.4)),e(`4`,i(t,n,.6))];if(a=I(s),a.v===null)throw Error(`four picks must yield a direction`);let c=T({vector:a.q,norm:E(a.q)},Float32Array.from(n),1),l=T({vector:a.mu,norm:E(a.mu)},Float32Array.from(n),1);if(!(c>l))throw Error(`q must lean further toward where it is heading than mu: ${c} vs ${l}`);let u=I(s,[],{drift:0});if(T({vector:u.q,norm:E(u.q)},u.mu,E(u.mu))<.999)throw Error(`drift 0 must be mu`);let d=[e(`p`,i(t,n,.7))],f=[e(`m`,t,`bleak`),e(`n`,n,`comic`)];if(L(f,I(d))[0].row.title!==`comic`)throw Error(`sanity: the taste leans comic`);if(L(f,I(d,[e(`q`,n)]))[0].row.title!==`bleak`)throw Error(`rejecting comic must flip the ranking to bleak`);let p=e(`a`,t,`the one they liked`);if(L([p,e(`other`,i(t,r,.1),`similar`)],I([p]),{exclude:new Set([`a`])}).some(e=>e.row.id===`a`))throw Error(`must not recommend what the taste was built from`);a=I(s);let m=R(a),h=JSON.stringify(m).length,g=Je(m);if(!g)throw Error(`a taste must survive the round trip`);let _=L(o,a).map(e=>e.row.id).join(),v=L(o,g).map(e=>e.row.id).join();if(_!==v)throw Error(`ranking changed across the wire: ${_} vs ${v}`);if(!m.from.length)throw Error(`a taste must say what it was built from, or it cannot be corrected`);if(!a.v)throw Error(`this fixture is meant to have a heading`);if(!g.v)throw Error(`a heading must survive the wire — q leans on it, so an export without it lies`);if(T({vector:g.v,norm:E(g.v)},a.v,E(a.v))<.98)throw Error(`…and must survive it intact`);if(Je({})!==null||Je(null)!==null)throw Error(`a malformed taste must be null, not a crash`);if(I([])!==null)throw Error(`no likes is no taste, not an empty one`);if(L(o,null).length)throw Error(`no taste recommends nothing`);if(qe({vector:null},a)!==null)throw Error(`a vectorless row scores null, not NaN`);{let e=(e,t)=>{let n=t*Math.PI/180;return{id:e,vector:[Math.cos(n),Math.sin(n)],norm:1}},t=[e(`run1`,0),e(`run2`,.8),e(`run3`,1.6),e(`run4`,2.4),e(`other`,60)],n=I([{id:`seed`,...e(`seed`,30)}]),r=L(t,n,{limit:3,lambda:1}).map(e=>e.row.id);if(r.some(e=>e===`other`))throw Error(`relevance alone must fill up with the run: ${r}`);let i=L(t,n,{limit:3}).map(e=>e.row.id);if(!i.includes(`other`))throw Error(`MMR must spend a slot on something else: ${i}`);if(i[0]!==r[0])throw Error(`the best match must still come first: ${i[0]} vs ${r[0]}`)}{let a=R(I([e(`a`,t,`The Golem`),e(`b`,n,`Nosferatu`),e(`c`,r,`Metropolis`)],[e(`z`,r,`Wrestling`)]));if(a.n!==3||a.from.length!==3)throw Error(`a fresh export carries what it was built from`);let o=Je(a),s=[{id:`(remembered)`,title:`what you liked before · ${o.n} picks`,vector:o.q}],c=o.no?[{id:`(remembered-no)`,title:`what you passed on before`,vector:o.no}]:[];if(R(I(s,c)).n!==1)throw Error(`this is the loss the next assertion is about`);let l=R(Xe(I(s,c),{carried:o,likes:s,dislikes:c}));if(l.n!==3)throw Error(`a restored taste must not report itself as one pick: ${l.n}`);if(!l.from.includes(`Nosferatu`))throw Error(`…and must still name what it was built from`);if(l.from.some(e=>e.startsWith(`(remembered`)))throw Error(`the synthetic placeholder is not a title`);if(!l.rejected.includes(`Wrestling`))throw Error(`rejections are provenance too`);let u=[...s,e(`d`,n,`Sunrise`)],d=R(Xe(I(u,c),{carried:o,likes:u,dislikes:c}));if(d.n!==4)throw Error(`a new pick must count once: ${d.n}`);if(d.from.filter(e=>e===`Nosferatu`).length!==1)throw Error(`…and must not duplicate the carried ones`);{let r=Je(R(I([e(`p`,t,`1`),e(`q`,i(t,n,.3),`2`),e(`r`,i(t,n,.6),`3`),e(`s`,n,`4`)]))),a=[{id:`(remembered)`,title:`before`,vector:r.q}];if(!Xe(I(a),{carried:r,likes:a}).v)throw Error(`a restored session must keep the heading its own q was built with`);if(I(a).v)throw Error(`…which the kernel alone cannot supply from one entry`)}let f=R(Xe(I(s,c),{carried:Je(l),likes:s,dislikes:c}));if(f.n!==3||f.from.join()!==l.from.join())throw Error(`a second reload must change nothing`)}console.log(`taste.js self-check ok — mu/v/q, drift leans forward, rejection steers, portable in ${h}B, ranks a pool it never saw, MMR keeps a run of near-duplicates from filling the list, provenance and heading survive the restore collapse and do not drift on reload`)}var Ze=e=>({content:[{type:`text`,text:JSON.stringify(e)}]}),Qe=(e,t=[])=>({type:`object`,properties:e,required:t});function $e(e){let t=document.modelContext;t?.registerTool&&(t.registerTool({name:`describe`,description:`What this app holds: its fields and how often each is present. Call it first.`,inputSchema:Qe({}),execute:async()=>Ze(Ie(e.rows))}),t.registerTool({name:`search`,description:"Search by meaning: a question in plain words. Optional equality filters in `where`.",inputSchema:Qe({query:{type:`string`},limit:{type:`number`},where:{type:`object`}},[`query`]),execute:async({query:t,limit:n=10,where:r})=>Ze(N(e.rows,t,e.roles,{qv:await e.queryVector(t),limit:n,where:r}))}),t.registerTool({name:`get`,description:`One record in full, by the id search returns.`,inputSchema:Qe({id:{type:`string`}},[`id`]),execute:async({id:t})=>Ze(He(e.rows,t,e.roles)??{error:`no record ${t}`})}),t.registerTool({name:`count`,description:`Count the distinct values of a field: the shape of the data before reading it.`,inputSchema:Qe({field:{type:`string`},where:{type:`object`},limit:{type:`number`}},[`field`]),execute:async({field:t,where:n,limit:r=20})=>Ze(P(e.rows,t,{where:n,limit:r}))}),t.registerTool({name:`session`,description:`What the person on this page saved and searched this session, and the taste (a vector over this app's records) their choices make.`,inputSchema:Qe({}),execute:async()=>Ze(e.session?.()??{saved:[],searches:[],taste:null})}))}function et(e,t){let n=t=>new Set(e.slice(0,5e3).flatMap(e=>a(e[t]??``))).size;return t.facets?.[0]??t.tags.find(e=>{let t=n(e);return t>=2&&t<=12})??null}function tt(e,t,{facet:r=null,only:a=null,today:o=new Date().toISOString().slice(0,10)}={}){let c=new Map;for(let o of e){let e=String(n(o,t.temporal)??``).slice(0,10);if(!e)continue;let l=r?String(o[r]??``):``;if(a&&l!==a)continue;let u=i(o,t)??``,d=`${l}|${u}|${e}`,f=c.get(d);f||c.set(d,f={key:d,date:e,place:l,label:u,byTitle:new Map});let p=s(o,t).toLowerCase(),m=f.byTitle.get(p);(!m||z(m,t).length<z(o,t).length)&&f.byTitle.set(p,o)}let l=[...c.values()].map(({byTitle:e,...t})=>({...t,items:[...e.values()]}));return{upcoming:l.filter(e=>e.date>o).sort((e,t)=>e.date.localeCompare(t.date)||e.place.localeCompare(t.place)),past:l.filter(e=>e.date<=o).sort((e,t)=>t.date.localeCompare(e.date)||e.place.localeCompare(t.place))}}function z(e,t){let n=s(e,t),r=String(e.text??``),i=r.startsWith(n.replace(/\.$/,``))?r.slice(n.replace(/\.$/,``).length).replace(/^[.\s]+/,``):r;return i===n?``:i.trim()}if(typeof process<`u`&&process.argv[1]?.endsWith(`/feed.js`)){let{rolesFrom:e}=await t(async()=>{let{rolesFrom:e}=await import(`./roles-YDRjv_PY.js`);return{rolesFrom:e}},__vite__mapDeps([4,5]),import.meta.url),n=e([{role_map:{title:`heading`,subtitle:`meeting`,temporal:`date`,tags:[`city`,`body`],text:[`text`]},presentation:{facets:[`city`]}}]),r=(e,t,n,r=n)=>({city:e,date:t,heading:n,meeting:`Board · ${t}`,body:`Board`,text:r}),i=[r(`A`,`2026-01-01`,`Budget`),r(`A`,`2026-01-01`,`Budget`,`Budget. Approved 5-0.`),r(`A`,`2026-01-01`,`Roads`),r(`B`,`2026-02-01`,`Parks`),r(`A`,`2026-03-01`,`Zoning`),{heading:`undated`}],a=(e,t,n)=>{if(JSON.stringify(e)!==JSON.stringify(t))throw Error(`${n}: ${JSON.stringify(e)}`)};a(et(i,n),`city`,`the declared facet wins`);let{upcoming:o,past:s}=tt(i,n,{facet:`city`,today:`2026-02-15`});a(o.map(e=>e.key),[`A|Board · 2026-03-01|2026-03-01`],`a future date is coming up`),a(s.map(e=>`${e.place}${e.date}`),[`B2026-02-01`,`A2026-01-01`],`past, latest first`),a(s[1].items.map(e=>z(e,n)),[`Approved 5-0.`,``],`one item per title, the one that says what happened`),a(tt(i,n,{facet:`city`,only:`B`,today:`2026-02-15`}).past.length,1,`narrowed to one place`),console.log(`feed.js self-check ok — declared facet, upcoming vs past, one item per title keeping the decision, narrowed by place`)}var nt=(e,t,n)=>{try{return JSON.parse(e?.getItem(t)??`null`)??n}catch{return n}},B=(e,t,n)=>{try{e?.setItem(t,JSON.stringify(n))}catch{}};function rt({local:e,session:t,app:n=`app`}={}){let r={saved:`${n}:saved`,passed:`${n}:passed`,history:`${n}:history`};return{saved:()=>nt(e,r.saved,[]),passed:()=>nt(e,r.passed,[]),history:()=>nt(t,r.history,[]),isSaved(e){return this.saved().some(t=>t.key===e)},toggleSave(t){let n=this.saved(),i=n.findIndex(e=>e.key===t.key);return i>=0?n.splice(i,1):n.push({...t,savedAt:new Date().toISOString()}),B(e,r.saved,n),B(e,r.passed,this.passed().filter(e=>e.key!==t.key)),i<0},pass(t){B(e,r.passed,[...this.passed().filter(e=>e.key!==t.key),t].slice(-50)),B(e,r.saved,this.saved().filter(e=>e.key!==t.key))},remember(e){let n=e.trim();n&&B(t,r.history,[{q:n,at:new Date().toISOString()},...this.history().filter(e=>e.q!==n)].slice(0,50))},clear(n){B(n===`history`?t:e,r[n],[])},taste(){let e=e=>({id:e.key,title:e.title,vector:e.v?C(e.v):null});return I(this.saved().map(e),this.passed().map(e))},bundle({name:e=n,url:t=``}={}){return{app:e,url:t,exported:new Date().toISOString(),saved:this.saved().map(({v:e,...t})=>t),searches:this.history(),taste:R(this.taste())}}}}var it=(e,{key:t,title:n,subtitle:r,place:i,date:a,link:o,detail:s})=>({key:t,title:n,subtitle:r,place:i,date:a,url:o??null,detail:s??``,...e.vector?{v:S(Array.from(e.vector))}:{}}),at=e=>`"${String(e??``).replace(/"/g,`""`)}"`,ot=e=>[`title,place,subtitle,date,url,detail`,...e.map(e=>[e.title,e.place,e.subtitle,e.date,e.url,e.detail].map(at).join(`,`))].join(`
7
+ `),st=(e,t=`Saved`)=>[`# ${t}`,``,...e.map(e=>`- **${e.url?`[${e.title}](${e.url})`:e.title}** — ${[e.place,e.subtitle].filter(Boolean).join(` · `)}${e.detail?`\n ${e.detail}`:``}`)].join(`
8
+ `);if(typeof process<`u`&&process.argv[1]?.endsWith(`/store.js`)){let e=()=>{let e=new Map;return{getItem:t=>e.get(t)??null,setItem:(t,n)=>e.set(t,n)}},t=rt({local:e(),session:e(),app:`t`}),n=e=>({vector:Array.from({length:8},(t,n)=>n===e?1:.1)}),r=it(n(0),{key:`a`,title:`Budget`,place:`X`,date:`2026-01-01`}),i=it(n(1),{key:`b`,title:`Roads, "east"`,place:`Y`}),a=(e,t,n)=>{if(JSON.stringify(e)!==JSON.stringify(t))throw Error(`${n}: ${JSON.stringify(e)}`)};if(a(t.taste(),null,`no taste before a save`),t.toggleSave(r),t.toggleSave(i),a(t.saved().map(e=>e.key),[`a`,`b`],`saved in order`),!t.taste()?.q)throw Error(`saves make a taste`);t.pass(i),a([t.saved().map(e=>e.key),t.passed().map(e=>e.key)],[[`a`],[`b`]],`passing on an item unsaves it`),a(t.taste().rejected,[`Roads, "east"`],`and it steers the taste away`),t.remember(`snow`),t.remember(`budget`),t.remember(`snow`),a(t.history().map(e=>e.q),[`snow`,`budget`],`history: newest first, no repeats`);let o=t.bundle({name:`T`});if(o.saved[0].v||!o.taste?.q||o.searches.length!==2)throw Error(`the bundle carries choices and taste, not raw vectors`);if(!ot([i]).includes(`"Roads, ""east"""`))throw Error(`CSV quoting`);if(rt({local:{getItem(){throw Error(`blocked`)},setItem(){throw Error(`blocked`)}}}).saved().length!==0)throw Error(`blocked storage reads as empty`);console.log(`store.js self-check ok — saves and passes steer a taste, history dedupes, the bundle carries no raw vectors, CSV quotes, blocked storage is empty not fatal`)}var ct=`http://www.w3.org/2000/svg`,lt=(e,t={})=>{let n=document.createElementNS(ct,e);for(let[e,r]of Object.entries(t))n.setAttribute(e,r);return n},V=e=>e.toLocaleString(),ut=new Set([`state`]);function dt(e,{el:t,show:n,row:r,placeOf:i,ctx:s}){let c=s.roles,l=Object.keys(e.paths),u=null,d=null,f=null,p={q:``,hits:null};function m(){if(u?.n===s.rows.length)return u;let t=s.rows.map(t=>{let n=a(t[e.field]??``).filter(t=>e.paths[t]);return n.length?n:[e.places?.[i(t)]].filter(Boolean)}),n=new Map;for(let e of s.rows){let t=o(e);n.set(t,(n.get(t)??0)+1)}let r=[...n].sort((e,t)=>t[1]-e[1])[0]?.[0],c=new Map(l.map(e=>[e,[]]));s.rows.forEach((e,n)=>{for(let e of t[n])c.get(e)?.push(n)});let d=0,f=null,p=0,m=new Map;s.rows.forEach((e,n)=>{if(!e.vector||o(e)!==r||!t[n].length)return;d||=e.vector.length,f??=new Float64Array(d);let i=1/(e.norm||Math.hypot(...e.vector)||1);for(let r of t[n]){let t=m.get(r)??{v:new Float64Array(d),n:0};for(let n=0;n<d;n++)t.v[n]+=e.vector[n]*i;t.n++,m.set(r,t)}for(let t=0;t<d;t++)f[t]+=e.vector[t]*i;p++});let h=new Map;for(let[e,t]of m){if(t.n<20)continue;let n=t.v.map((e,n)=>e/t.n-f[n]/p),r=Math.hypot(...n)||1;h.set(e,n.map(e=>e/r))}return u={n:s.rows.length,regionsOf:t,by:c,main:r,types:n,centroid:h},u}function h(e){let{centroid:t}=m(),n=t.get(e);return n?[...t].filter(([t])=>t!==e).map(([e,t])=>({region:e,score:t.reduce((e,t,r)=>e+t*n[r],0)})).sort((e,t)=>t.score-e.score).slice(0,5):[]}let g=(e,t=2)=>{let n=c.types?.[e];return(t===1?n?.singular:n?.plural)??e??`Records`},_=e=>c.types?.[e]?.icon??``;function v(e){let{regionsOf:t}=m(),n=new Map,r=e??s.rows.map((e,t)=>t);for(let e of r)if(!(f&&o(s.rows[e])!==f))for(let r of t[e])n.set(r,(n.get(r)??0)+1);return n}async function y(e){if(!e)return null;if(p.q===e&&p.hits)return p.hits;let t=s.queryVector(e),n=await Promise.race([t,new Promise(e=>setTimeout(()=>e(void 0),400))]);return n===void 0&&t.then(t=>{t&&p.q===e&&!p.meaning&&(p={q:e,hits:ee(e,t),meaning:!0},x(e,d))}),p={q:e,hits:ee(e,n??null),meaning:n!==void 0},p.hits}function ee(e,t){let n=new Map(s.rows.map((e,t)=>[e.id,t]));return N(s.rows,e,c,{qv:t,limit:600}).map(e=>n.get(e.id)).filter(e=>e!=null)}let b=(e,t=d)=>{location.hash=`#/map/${encodeURIComponent(e)}${t?`/${encodeURIComponent(t)}`:``}`};async function x(r=``,i=null){d=i&&e.paths[i]?i:null;let a=await y(r),c=v(a),u=Math.max(1,...c.values()),ee=e=>e?Math.min(5,1+Math.floor((a?e/u:Math.log10(e)/Math.log10(u))*4.999)):0,ne=d?new Set(h(d).map(e=>e.region)):new Set,S=lt(`svg`,{viewBox:`0 0 ${e.width} ${e.height}`,role:`img`,"aria-label":a?`Map: where “${r}” comes up`:`Map: where records are`,class:`map`});for(let t of l){let n=lt(`path`,{d:e.paths[t],class:`h${ee(c.get(t)??0)}${t===d?` on`:``}${ne.has(t)?` near`:``}`,tabindex:0,"aria-label":`${t}: ${V(c.get(t)??0)}`});n.append(lt(`title`)),n.firstChild.textContent=`${t} · ${V(c.get(t)??0)} ${a?`matching`:(f?g(f):`records`).toLowerCase()}`;let i=()=>b(r,d===t?null:t);n.addEventListener(`click`,i),n.addEventListener(`keydown`,e=>{(e.key===`Enter`||e.key===` `)&&(e.preventDefault(),i())}),S.append(n)}let C=t(`input`,{type:`search`,value:r,placeholder:`Search the map: data center, PFAS, solar rezoning…`,"aria-label":`Search the map`});C.addEventListener(`keydown`,e=>{e.key===`Enter`&&b(C.value.trim())});let w=a?new Map:m().types;for(let e of a??[]){let t=o(s.rows[e]);w.set(t,(w.get(t)??0)+1)}let re=w.size>1||a?t(`div`,{className:`chips`},[[null,a?.length??s.rows.length],...w].map(([e,n])=>t(`button`,{type:`button`,className:e===f?`chip on`:`chip`,onclick:()=>{f=e,x(r,d)}},`${e?`${_(e)} ${g(e,n)}`:`All`} (${V(n)})`))):null,T=l.filter(e=>c.get(e)).length,E=a?`“${r}”: ${V([...c.values()].reduce((e,t)=>e+t,0))} matches in ${T} of ${l.length} regions. Darker is more.${p.meaning?``:` (Matching words; search by meaning is loading.)`}`:`${f?g(f):`Records`} in ${T} of ${l.length} regions. Select a region, or search.`;n(t(`div`,{className:`mapbar`},C),re,t(`p`,{className:`hint`},E),t(`div`,{className:`mapwrap`},S,t(`aside`,{className:`panel`},te(d,r,a))),t(`p`,{className:`hint`},`Legend: `,[0,1,2,3,4,5].map(e=>t(`span`,{className:`sw h${e}`})),a?` none → most matches`:` none → most records`))}function te(n,i,l){if(!n)return[t(`h3`,{},`Select a region`),t(`p`,{className:`hint`},`Its records by kind and tag, the ones that match, and the regions most like it.`)];let{by:u}=m(),d=new Set(u.get(n)),p=(l?l.filter(e=>d.has(e)):[...d]).filter(e=>!f||o(s.rows[e])===f).map(e=>s.rows[e]),v=new Map;for(let e of p){let t=o(e);v.set(t,(v.get(t)??0)+1)}let y=(c.tags??[]).filter(t=>t!==e.field&&!ut.has(t)).map(e=>{let t=new Map;for(let n of p)for(let r of a(n[e]??``))t.set(r,(t.get(r)??0)+1);return[e,[...t].sort((e,t)=>t[1]-e[1]).slice(0,6)]}).filter(([,e])=>e.length>1),ee=e=>String(e[c.temporal?.[0]]??``),x=(l?p:p.slice().sort((e,t)=>ee(t).localeCompare(ee(e)))).slice(0,25),te=h(n);return[t(`h3`,{},n,` `,t(`button`,{type:`button`,className:`linkish`,"aria-label":`Close`,onclick:()=>b(i,null)},`✕`)),t(`p`,{className:`hint`},[...v].map(([e,t])=>`${_(e)} ${V(t)} ${g(e,t).toLowerCase()}`).join(` · `)||`Nothing here`+(l?` matches.`:` yet.`)),te.length?t(`div`,{className:`chips`},t(`small`,{title:`Regions whose records are closest in meaning; outlined on the map`},`Reads most like: `),te.map(({region:e})=>t(`button`,{type:`button`,className:`chip`,onclick:()=>b(i,e)},e))):null,y.map(([e,n])=>t(`div`,{className:`chips`},t(`small`,{},`${e.replace(/_/g,` `)}: `),n.map(([e,n])=>t(`button`,{type:`button`,className:`chip`,title:`Search the map for “${e}”`,onclick:()=>b(e)},`${e} (${n})`)))),x.length?t(`ul`,{className:`list`},x.map(e=>r(e))):null,p.length>x.length?t(`p`,{className:`hint`},`${V(p.length-x.length)} more. `,t(`a`,{href:`#/search/${encodeURIComponent(i||n)}`},`Open in search`)):null]}return{render:x}}var H=e=>document.querySelector(e),U=(e,t={},...n)=>{let r=Object.assign(document.createElement(e),t);return r.append(...n.flat(1/0).filter(e=>e!=null&&e!==!1)),r},ft=(e,t=220)=>e.length>t?`${e.slice(0,e.lastIndexOf(` `,t))} …`:e,pt=e=>e?new Date(`${String(e).slice(0,10)}T12:00:00`).toLocaleDateString(void 0,{weekday:`short`,month:`short`,day:`numeric`,year:`numeric`}):``,W={rows:[],roles:r([]),queryVector:e=>ne(e).catch(()=>null)},mt,ht=new Promise(e=>{mt=e});A({onManifests:e=>{W.roles=r(e),mt()},rowText:e=>l(e,W.roles)});var gt=await fetch(`./.well-known/agent-card.json`).then(e=>e.json()).catch(()=>({})),_t=(gt.capabilities?.extensions??[]).map(e=>e.params?.paid).find(Boolean)??null,G=gt.name??`Fangorn app`;document.title=G,H(`#name`).textContent=G,fetch(`./nav.json`).then(e=>e.json()).then(e=>H(`nav`).append(...e.map(e=>U(`a`,{href:e.href},e.label)))).catch(()=>{});var vt=await fetch(`./map.json`).then(e=>(e.headers.get(`content-type`)??``).includes(`json`)?e.json():null).catch(()=>null);vt&&H(`nav`).prepend(U(`a`,{href:`#/map`},`Map`));var K=H(`#q input`);K.disabled=!1;var q=rt({local:globalThis.localStorage,session:globalThis.sessionStorage,app:G.toLowerCase().replace(/\W+/g,`-`)});W.session=()=>q.bundle({name:G,url:location.origin});var yt=new URL(`view`,location.href).href,bt=await fetch(`${yt}/cdn/catalog`).then(e=>e.json()).then(e=>e.domains.reduce((e,t)=>e+(t.count??0),0)).catch(()=>0),xt=!1,St=0,Ct=M(yt,e=>{W.rows=e,Ot(),Date.now()-St>1500&&/^(#\/?)?$/.test(location.hash)&&(St=Date.now(),$t())});await ht;var J=W.roles,wt=e=>String((J.identity&&e[J.identity])??e.id),Tt=new Map,Et=new Map,Y=null,Dt=[];function Ot(){Tt=new Map(W.rows.map(e=>[wt(e),e])),Et=new Map(W.rows.map(e=>[e.id,e])),Y??=et(W.rows,J),Dt=Y?[...W.rows.reduce((e,t)=>{let n=X(t);return n&&e.set(n,(e.get(n)??0)+1),e},new Map)].sort((e,t)=>e[0].localeCompare(t[0])):[];let e=W.rows.length.toLocaleString();K.placeholder=xt?`Search ${e} records${Dt.length>1?` in ${Dt.length} places`:``}…`:`Loading ${e}${bt?` of ${bt.toLocaleString()}`:``} records… (search what is here)`}var X=e=>Y?a(e[Y]??``)[0]??``:``,kt=e=>String(e[J.temporal?.[0]]??``).slice(0,10),At=e=>d(e,J),jt=e=>it(e,{key:wt(e),title:s(e,J),subtitle:i(e,J),place:X(e),date:kt(e),link:At(e),detail:z(e,J)}),Mt=e=>/[a-z]/.test(e)||!/[A-Z]{5}/.test(e)?e:e.toLowerCase().replace(/\b[a-z]{1,3}\b/g,(e,t)=>/^(and|or|of|the|to|for|in|on|at|by|a|an|as|is|be|no|from)$/.test(e)?e:e.toUpperCase()).replace(/^\W*\w/,e=>e.toUpperCase()).replace(/([.:;!?]\s+|\u2013\s+|-\s+)(\w)/g,(e,t,n)=>t+n.toUpperCase()),Nt=e=>Mt(s(e,J)),Pt=e=>`#/item/${encodeURIComponent(wt(e))}`,Ft=(e,t)=>{if(!t.length)return[e];let n=RegExp(`(${t.map(e=>e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`)).join(`|`)})`,`ig`);return String(e).split(n).map((e,t)=>t%2?U(`mark`,{},e):e)},It=e=>{let t=U(`button`,{type:`button`,className:`star`,title:`Save`}),n=()=>{let n=q.isSaved(wt(e));t.textContent=n?`★`:`☆`,t.setAttribute(`aria-label`,n?`Saved`:`Save`),t.classList.toggle(`on`,n)};return t.onclick=t=>{t.preventDefault(),q.toggleSave(jt(e)),n(),Qt()},n(),t},Z=(e,{terms:t=[],also:n=0,meta:r=!0}={})=>{let a=z(e,J);return U(`li`,{className:`row`},U(`div`,{className:`main`},U(`a`,{href:Pt(e),className:`title`},Ft(Nt(e),t)),r?U(`small`,{},[X(e),i(e,J)].filter(Boolean).join(` · `)):null,a?U(`p`,{className:`detail`},Ft(ft(a),t)):null,n?U(`small`,{className:`also`},`and ${n} more record${n>1?`s`:``} of the same item`):null),It(e))},Q=null,Lt=()=>Dt.length>1?U(`div`,{className:`chips`},[[null,W.rows.length],...Dt].map(([e,t])=>U(`button`,{type:`button`,className:e===Q?`chip on`:`chip`,title:`${t.toLocaleString()} records`,onclick:()=>{Q=e,$t()}},e??`All`))):null,Rt=e=>!Q||X(e)===Q,zt=H(`#view`),$=(...e)=>{zt.replaceChildren(...e.flat().filter(Boolean)),window.scrollTo(0,0)};function Bt(e){let t=U(`ul`,{},e.items.slice(0,5).map(e=>Z(e,{meta:!1}))),n=e.items.length>5?U(`button`,{type:`button`,className:`more`,onclick:n=>{t.append(...e.items.slice(5).map(e=>Z(e,{meta:!1}))),n.target.remove()}},`${e.items.length-5} more`):null;return U(`article`,{},U(`header`,{},U(`time`,{dateTime:e.date},pt(e.date)),U(`h3`,{},[e.place,e.label.replace(/\s*·\s*\d{4}-\d\d-\d\d$/,``)].filter(Boolean).join(` — `))),t,n)}var Vt=12;function Ht(){let{upcoming:e,past:t}=tt(W.rows,J,{facet:Y,only:Q}),n=q.taste(),r=n?L(W.rows.filter(Rt),n,{limit:6,exclude:new Set(W.rows.filter(e=>q.isSaved(wt(e))).map(e=>e.id))}):[];$(gt.description?U(`p`,{className:`about`},gt.description):null,Lt(),r.length?U(`section`,{},U(`h2`,{},`For you`),U(`p`,{className:`hint`},`From the ${n.n} item${n.n>1?`s`:``} you saved. `,U(`a`,{href:`#/saved`},`Change`)),U(`ul`,{className:`list`},r.map(({row:e})=>Z(e)))):null,e.length?U(`section`,{},U(`h2`,{},`Coming up`),e.slice(0,8).map(Bt)):null,t.length?U(`section`,{},U(`h2`,{},`Recently`),t.slice(0,Vt).map(Bt),t.length>Vt?U(`button`,{type:`button`,className:`more`,onclick:()=>{Vt+=12,Ht()}},`Show more`):null):null,!e.length&&!t.length?U(`p`,{className:`empty`},xt?`Nothing dated here yet. Search above.`:`Loading the records…`):null)}var Ut=`relevance`,Wt=0,Gt=null;async function Kt(e){document.activeElement!==H(`#q input`)&&(H(`#q input`).value=e);let t=++Wt,n=W.queryVector(e),r=await Promise.race([n,new Promise(e=>setTimeout(()=>e(void 0),400))]);t===Wt&&(r===void 0&&n.then(n=>{n&&t===Wt&&qt(e,n)}),qt(e,r??null,r===void 0))}function qt(e,t,n=!1){let r=N(W.rows,e,J,{qv:t,limit:80,...Q&&Y?{where:{[Y]:Q}}:{}}),i=new Map;for(let e of r){let t=Et.get(e.id);if(!t)continue;let n=`${X(t)}|${s(t,J).toLowerCase()}`,r=i.get(n);r?(r.also++,z(t,J).length>z(r.r,J).length&&(r.r=t)):i.set(n,{r:t,also:0})}let a=[...i.values()];Ut===`newest`&&(a=a.sort((e,t)=>kt(t.r).localeCompare(kt(e.r))));let o=e.toLowerCase().split(/\W+/).filter(e=>e.length>2),c=U(`div`,{className:`sort`},`Sort: `,[`relevance`,`newest`].map(r=>U(`button`,{type:`button`,className:r===Ut?`chip on`:`chip`,onclick:()=>{Ut=r,qt(e,t,n)}},r)));$(Lt(),U(`div`,{className:`resultsbar`},U(`span`,{},`${a.length} result${a.length===1?``:`s`} for “${e}”${Q?` in ${Q}`:``}`,n?U(`small`,{},` · matching words; search by meaning is loading`):null),c),a.length?U(`ul`,{className:`list`},a.slice(0,40).map(({r:e,also:t})=>Z(e,{terms:o,also:t}))):U(`p`,{className:`empty`},`No matches. Try other words, or All places.`))}function Jt(e){let t=Tt.get(e);if(!t)return $(U(`p`,{className:`empty`},`That record is not in this app any more. `,U(`a`,{href:`#/`},`Home`)));let n=At(t),r=z(t,J),a=new Set([`id`,`owner`,`text`,`vector`,`norm`,`embed`,`entityType`,J.identity,...J.title,...J.subtitle,...J.media,...J.temporal??[]]),o=Object.entries(t).filter(([e,t])=>!a.has(e)&&!/(_id|_sha256)$/.test(e)&&(typeof t==`string`||typeof t==`number`)&&String(t).length<120),c=U(`div`,{className:`doc`}),l=n?U(`button`,{type:`button`,className:`act`,onclick:()=>{if(c.firstChild){c.replaceChildren(),l.textContent=`View document`;return}c.append(U(`iframe`,{src:`./doc?u=${encodeURIComponent(n)}${Number(t.page)>1?`#page=${t.page}`:``}`,title:`Source document`,loading:`lazy`})),l.textContent=`Hide document`}},`View document`):null,u=U(`button`,{type:`button`,className:`act`,onclick:()=>{q.pass(jt(t)),u.textContent=`Noted: less like this`,Qt()}},`Not interested`),d=tt(W.rows.filter(e=>X(e)===X(t)&&kt(e)===kt(t)&&i(e,J)===i(t,J)),J,{facet:Y}),f=[...d.upcoming,...d.past].flatMap(e=>e.items).filter(e=>s(e,J)!==s(t,J)),p=Ve(W.rows,t.id,J,{limit:12}).near.map(e=>Et.get(e.id)).filter(e=>e&&s(e,J)!==s(t,J)).slice(0,6);$(U(`p`,{},U(`a`,{href:`#/`,onclick:e=>{history.length>1&&(e.preventDefault(),history.back())}},`← Back`)),U(`article`,{className:`record`},U(`small`,{},[X(t),i(t,J)].filter(Boolean).join(` · `)),U(`h2`,{},Nt(t)),r?U(`p`,{},r):U(`p`,{className:`hint`},`No decision is recorded under this item (it may be on an agenda, before the meeting).`),o.length?U(`dl`,{},o.map(([e,t])=>[U(`dt`,{},e.replace(/_/g,` `)),U(`dd`,{},String(t))])):null,U(`div`,{className:`acts`},It(t),l,u,n?U(`a`,{href:n,target:`_blank`,rel:`noopener`,className:`act`},`Source ↗`):null),_t&&t.paid_sha256?U(`p`,{className:`hint`},`Agents can buy this item's structured record for ${Number(_t.price)/10**(_t.decimals??6)} ${_t.symbol??`USDC`} (x402).`):null,c),f.length?U(`section`,{},U(`h2`,{},`Same meeting`),U(`ul`,{className:`list`},f.slice(0,12).map(e=>Z(e,{meta:!1})))):null,p.length?U(`section`,{},U(`h2`,{},`Similar`),U(`ul`,{className:`list`},p.map(e=>Z(e)))):null)}function Yt(e,t,n){let r=U(`a`,{href:URL.createObjectURL(new Blob([t],{type:n})),download:e});document.body.append(r),r.click(),r.remove()}function Xt(){let e=q.saved(),t=q.taste(),n=G.toLowerCase().replace(/\W+/g,`-`),r=()=>JSON.stringify(q.bundle({name:G,url:location.origin}),null,1),i=U(`button`,{type:`button`,className:`act`,onclick:async()=>{try{await navigator.clipboard.writeText(r()),i.textContent=`Copied`}catch{Yt(`${n}-session.json`,r(),`application/json`)}}},`Copy for an agent`);$(U(`h2`,{className:`page`},`Saved (${e.length})`),e.length?U(`div`,{className:`acts`},U(`button`,{type:`button`,className:`act`,onclick:()=>Yt(`${n}-saved.json`,r(),`application/json`)},`Export JSON`),U(`button`,{type:`button`,className:`act`,onclick:()=>Yt(`${n}-saved.csv`,ot(e),`text/csv`)},`CSV`),U(`button`,{type:`button`,className:`act`,onclick:()=>Yt(`${n}-saved.md`,st(e,`${G}: saved`),`text/markdown`)},`Markdown`),i):U(`p`,{className:`empty`},`Nothing saved yet. Save items with ☆; they stay on this device.`),t?U(`p`,{className:`hint`},`"For you" on the front page is built from these ${t.n}${t.rejected.length?`, and away from ${t.rejected.length} you passed on`:``}. It lives in this browser; the export carries it, so an agent can use it too. `,U(`button`,{type:`button`,className:`linkish`,onclick:()=>{q.clear(`passed`),Xt(),Qt()}},`Forget what I passed on`)):null,U(`ul`,{className:`list`},e.slice().reverse().map(e=>{let t=Tt.get(e.key);return U(`li`,{className:`row`},U(`div`,{className:`main`},U(`a`,{href:t?Pt(t):e.url??`#/`,className:`title`},e.title),U(`small`,{},[e.place,e.subtitle].filter(Boolean).join(` · `)),e.detail?U(`p`,{className:`detail`},ft(e.detail)):null),U(`button`,{type:`button`,className:`star on`,title:`Remove`,onclick:()=>{q.toggleSave(e),Xt(),Qt()}},`★`))})))}function Zt(){let e=q.history();$(U(`h2`,{className:`page`},`Searches this session`),e.length?U(`ul`,{className:`list`},e.map(({q:e,at:t})=>U(`li`,{className:`row`},U(`div`,{className:`main`},U(`a`,{href:`#/search/${encodeURIComponent(e)}`,className:`title`},e),U(`small`,{},new Date(t).toLocaleTimeString()))))):U(`p`,{className:`empty`},`No searches yet.`),e.length?U(`button`,{type:`button`,className:`act`,onclick:()=>{q.clear(`history`),Zt()}},`Clear`):null)}function Qt(){let e=q.saved().length;H(`#nsaved`).textContent=e?` (${e})`:``}function $t(){let[,e=``,t=``]=location.hash.match(/^#\/([^/]*)\/?(.*)$/)??[],n=decodeURIComponent(t);for(let t of document.querySelectorAll(`nav a`))t.classList.toggle(`on`,t.getAttribute(`href`)===`#/${e}`);return document.body.classList.toggle(`wide`,e===`map`),e===`search`&&n?Kt(n):e===`item`&&n?Jt(n):e===`saved`?Xt():e===`history`?Zt():e===`map`&&vt?(Gt??=dt(vt,{el:U,show:$,row:Z,placeOf:X,ctx:W})).render(...t.split(`/`).map(decodeURIComponent)):(H(`#q input`).value=``,Ht())}var en;K.oninput=()=>{clearTimeout(en);let e=K.value.trim();en=setTimeout(()=>{if(!e){location.hash.startsWith(`#/search`)&&(location.hash=`#/`);return}history.replaceState(null,``,`#/search/${encodeURIComponent(e)}`),Kt(e)},250)},H(`#q`).onsubmit=e=>{e.preventDefault();let t=K.value.trim();t&&(q.remember(t),location.hash=`#/search/${encodeURIComponent(t)}`)},zt.addEventListener(`click`,e=>{e.target.closest(`a.title`)&&location.hash.startsWith(`#/search/`)&&q.remember(K.value)}),window.onhashchange=$t,Qt(),$t(),W.rows=await Ct,xt=!0,Ot(),$e(W),$t();
@@ -1,6 +1,6 @@
1
1
  <!doctype html>
2
2
  <html lang="en">
3
- <script type="module" crossorigin src="./assets/index-DlzVyQ_9.js"></script>
3
+ <script type="module" crossorigin src="./assets/index-CaHGhiKK.js"></script>
4
4
  <link rel="modulepreload" crossorigin href="./assets/rolldown-runtime-Dd_uD5pT.js">
5
5
  <link rel="modulepreload" crossorigin href="./assets/preload-helper-uBIymjUX.js">
6
6
  <link rel="modulepreload" crossorigin href="./assets/roles-C9V_uDr_.js">
@@ -8,13 +8,13 @@
8
8
  <meta charset="utf-8">
9
9
  <meta name="viewport" content="width=device-width, initial-scale=1">
10
10
  <title>Fangorn app</title>
11
- <style>:root{--bg:#fbfaf7;--card:#fff;--fg:#1d1d1b;--muted:#6b6a66;--accent:#2b5d8a;--line:#e4e1da;--chip:#efece5}@media (prefers-color-scheme:dark){:root{--bg:#151514;--card:#1c1c1a;--fg:#ecebe6;--muted:#9a988f;--accent:#7fb0dc;--line:#2c2b28;--chip:#262522}}body{background:var(--bg);color:var(--fg);max-width:800px;margin:0 auto;padding:32px 16px;font:16px/1.5 system-ui,sans-serif}h1{letter-spacing:-.01em;margin:0 0 4px;font-size:1.7rem}#about{color:var(--muted);margin:0 0 20px}input{box-sizing:border-box;width:100%;font:inherit;border:1px solid var(--line);background:var(--card);color:inherit;border-radius:10px;padding:11px 14px}#status{color:var(--muted);margin:8px 0 12px;font-size:.85rem}#chips{flex-wrap:wrap;gap:6px;margin-bottom:20px;display:flex}button{font:inherit;cursor:pointer}.chip{border:1px solid var(--line);background:var(--chip);color:var(--fg);border-radius:999px;padding:4px 12px;font-size:.88rem}.chip.on{background:var(--accent);border-color:var(--accent);color:var(--bg)}h2{text-transform:uppercase;letter-spacing:.08em;color:var(--muted);margin:28px 0 10px;font-size:.8rem}article{background:var(--card);border:1px solid var(--line);border-radius:12px;margin-bottom:12px;padding:14px 16px}article header{flex-direction:column;margin-bottom:6px;display:flex}article time{color:var(--muted);font-size:.8rem}h3{margin:0;font-size:1.02rem}ul{margin:0;padding:0;list-style:none}li{border-top:1px solid var(--line);overflow-wrap:anywhere;flex-direction:column;gap:2px;padding:8px 0;display:flex}article li:first-child,#results li:first-child{border-top:0}.detail{color:var(--muted);margin:0;font-size:.9rem}a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}small{color:var(--muted)}.more{color:var(--accent);background:0 0;border:0;padding:6px 0 0;font-size:.9rem}.empty{color:var(--muted)}footer{color:var(--muted);margin-top:32px;font-size:.8rem}</style>
12
- <h1>Fangorn app</h1>
13
- <p id="about"></p>
14
- <input type="search" autofocus disabled placeholder="Loading…" aria-label="Search">
15
- <div id="status">Loading the data…</div>
16
- <div id="chips"></div>
17
- <ul id="results" hidden></ul>
18
- <main id="feed"></main>
19
- <footer>Search runs in your browser; your query is never sent anywhere. Agents: <a href="./.well-known/agent-card.json">agent card</a>.</footer>
11
+ <style>:root{--bg:#fbfaf7;--card:#fff;--fg:#1d1d1b;--muted:#6b6a66;--accent:#2b5d8a;--line:#e4e1da;--chip:#efece5;--mark:#fdeaa8;--hot:#c0392b;--edge:#b9b5ab}@media (prefers-color-scheme:dark){:root{--bg:#151514;--card:#1c1c1a;--fg:#ecebe6;--muted:#9a988f;--accent:#7fb0dc;--line:#2c2b28;--chip:#262522;--mark:#5a4a12;--hot:#e0584c;--edge:#55534e}}*{box-sizing:border-box}body{background:var(--bg);color:var(--fg);margin:0;font:16px/1.5 system-ui,sans-serif}.bar{z-index:2;background:var(--bg);border-bottom:1px solid var(--line);position:sticky;top:0}.bar>div{flex-wrap:wrap;align-items:center;gap:12px;max-width:820px;margin:0 auto;padding:10px 16px;display:flex}#home{color:var(--fg);white-space:nowrap;font-size:1.15rem;font-weight:700;text-decoration:none}#q{flex:260px}#q input{width:100%;font:inherit;border:1px solid var(--line);background:var(--card);color:inherit;border-radius:10px;padding:9px 12px}nav{gap:14px;font-size:.92rem;display:flex}nav a{color:var(--muted);text-decoration:none}nav a.on,nav a:hover{color:var(--accent)}main{max-width:820px;margin:0 auto;padding:16px 16px 48px}.about{color:var(--muted);margin:4px 0 14px}#status{color:var(--muted)}button{font:inherit;cursor:pointer}.chips{flex-wrap:wrap;gap:6px;margin:4px 0 16px;display:flex}.chip{border:1px solid var(--line);background:var(--chip);color:var(--fg);border-radius:999px;padding:3px 11px;font-size:.86rem}.chip.on{background:var(--accent);border-color:var(--accent);color:var(--bg)}h2{text-transform:uppercase;letter-spacing:.08em;color:var(--muted);margin:26px 0 10px;font-size:.78rem}h2.page{text-transform:none;letter-spacing:0;color:var(--fg);font-size:1.2rem}article{background:var(--card);border:1px solid var(--line);border-radius:12px;margin-bottom:12px;padding:14px 16px}article header{flex-direction:column;margin-bottom:4px;display:flex}article time,small{color:var(--muted);font-size:.8rem}h3{margin:0;font-size:1.02rem}ul{margin:0;padding:0;list-style:none}.row{border-top:1px solid var(--line);align-items:flex-start;gap:10px;padding:9px 0;display:flex}article .row:first-child,.list .row:first-child{border-top:0}.list{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:4px 16px}.main{overflow-wrap:anywhere;flex-direction:column;flex:1;gap:2px;min-width:0;display:flex}.title{color:var(--accent);text-decoration:none}.title:hover{text-decoration:underline}.detail{color:var(--muted);margin:0;font-size:.9rem}mark{background:var(--mark);color:inherit;border-radius:2px}.star{color:var(--muted);background:0 0;border:0;padding:2px 4px;font-size:1.15rem;line-height:1}.star.on{color:#d4a017}.more,.linkish{color:var(--accent);background:0 0;border:0;padding:6px 0 0;font-size:.9rem}.resultsbar{color:var(--muted);flex-wrap:wrap;justify-content:space-between;align-items:center;gap:8px;margin-bottom:10px;font-size:.9rem;display:flex}.sort{align-items:center;gap:6px;display:flex}.hint,.empty{color:var(--muted);font-size:.9rem}.record h2{text-transform:none;letter-spacing:0;color:var(--fg);margin:4px 0 10px;font-size:1.3rem}.record dl{grid-template-columns:max-content 1fr;gap:2px 14px;margin:12px 0;font-size:.88rem;display:grid}.record dt{color:var(--muted);text-transform:capitalize}.record dd{margin:0}.acts{flex-wrap:wrap;align-items:center;gap:8px;margin:12px 0 4px;display:flex}.act{border:1px solid var(--line);background:var(--chip);color:var(--fg);border-radius:8px;padding:5px 12px;font-size:.88rem;text-decoration:none}.doc iframe{border:1px solid var(--line);background:#fff;border-radius:8px;width:100%;height:80vh;margin-top:12px}body.wide main,body.wide .bar>div{max-width:1200px}.mapbar input{width:100%;font:inherit;border:1px solid var(--line);background:var(--card);color:inherit;border-radius:10px;margin-bottom:8px;padding:9px 12px}.mapwrap{grid-template-columns:minmax(0,1fr) minmax(0,1fr);align-items:start;gap:16px;display:grid}@media (width<=760px){.mapwrap{grid-template-columns:1fr}}svg.map{width:100%;height:auto;display:block;position:sticky;top:70px}svg.map path{stroke:var(--edge);stroke-width:.8px;cursor:pointer}svg.map path:hover,svg.map path:focus{stroke:var(--accent);stroke-width:2px;outline:none}svg.map path.near{stroke:var(--accent);stroke-width:2px;stroke-dasharray:4 3}svg.map path.on{stroke:var(--fg);stroke-width:2.6px}.h0{fill:var(--card);background:var(--card)}.h1{fill:color-mix(in srgb, var(--hot) 18%, var(--card));background:color-mix(in srgb, var(--hot) 18%, var(--card))}.h2{fill:color-mix(in srgb, var(--hot) 36%, var(--card));background:color-mix(in srgb, var(--hot) 36%, var(--card))}.h3{fill:color-mix(in srgb, var(--hot) 56%, var(--card));background:color-mix(in srgb, var(--hot) 56%, var(--card))}.h4{fill:color-mix(in srgb, var(--hot) 78%, var(--card));background:color-mix(in srgb, var(--hot) 78%, var(--card))}.h5{fill:var(--hot);background:var(--hot)}.sw{border:1px solid var(--edge);vertical-align:-1px;width:14px;height:12px;display:inline-block}.panel h3{margin:4px 0}.panel .chips{align-items:center;margin:6px 0}.panel .list{margin-top:8px}footer{max-width:820px;color:var(--muted);margin:0 auto;padding:0 16px 32px;font-size:.8rem}footer a{color:var(--accent)}</style>
12
+ <header class="bar"><div>
13
+ <a id="home" href="#/"><span id="name">Fangorn app</span></a>
14
+ <form id="q" role="search"><input type="search" disabled placeholder="Loading…" aria-label="Search"></form>
15
+ <nav><a href="#/saved">Saved<span id="nsaved"></span></a><a href="#/history">History</a></nav>
16
+ </div></header>
17
+ <main id="view"><p id="status">Loading the data…</p></main>
18
+ <footer>Search runs in your browser; your searches and saved items stay on this device unless you export them.
19
+ Documents are the publishers' own, shown as they published them. Agents: <a href="./.well-known/agent-card.json">agent card</a>.</footer>
20
20
  </html>