@fangorn-network/westmarch 0.1.0 → 0.1.2

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.
@@ -49,7 +49,8 @@ update: each step checks the chain (or Cloudflare) first and acts only on a diff
49
49
  "tags": ["category"], "text": ["summary"] },
50
50
  "presentation": { "facets": ["category"] } } },
51
51
  "relations": [],
52
- "sources": [{ "namespace": "things", "command": ["python", "-m", "my_source"] }],
52
+ "sources": [{ "namespace": "things", "command": ["python", "-m", "my_source"] }], // your wallet's repos only
53
+ "live": ["readings"], // optional: namespaces other wallets push to; "0x<wallet>/readings" for one wallet's
53
54
  "network": "baseSepolia", // required
54
55
  "fangorn": "fangorn", // the CLI that signs
55
56
  "paid": { "price": "0.01", "description": "the full record" } // optional: USDC on the app's network, x402 v2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fangorn-network/westmarch",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "all-in-one builder for fangorn apps",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -41,7 +41,7 @@
41
41
  "@fangorn-network/markov": "^0.1.0"
42
42
  },
43
43
  "peerDependencies": {
44
- "@fangorn-network/sdk": "^2026.11.4-dev",
44
+ "@fangorn-network/sdk": "^0.1.1",
45
45
  "@huggingface/transformers": "^4.2.0",
46
46
  "@noble/curves": "^1.9.1",
47
47
  "@semaphore-protocol/core": "^4.14.5",
@@ -56,7 +56,7 @@
56
56
  }
57
57
  },
58
58
  "devDependencies": {
59
- "@fangorn-network/sdk": "2026.11.4-dev",
59
+ "@fangorn-network/sdk": "^0.1.1",
60
60
  "@huggingface/transformers": "^4.2.0",
61
61
  "@noble/curves": "^1.9.1",
62
62
  "@semaphore-protocol/core": "^4.14.5",
@@ -90,5 +90,6 @@
90
90
  },
91
91
  "publishConfig": {
92
92
  "access": "public"
93
- }
93
+ },
94
+ "packageManager": "pnpm@10.30.1+sha512.3590e550d5384caa39bd5c7c739f72270234b2f6059e13018f975c313b1eb9fefcc09714048765d4d9efe961382c312e624572c0420762bdc5d5940cdf9be73a"
94
95
  }
@@ -38,6 +38,8 @@ export async function captureTools(register) {
38
38
  * tools. With it, `{app, owner, fromBlock, repos, views, mcp, config}`:
39
39
  * app the app's repo name
40
40
  * owner the address that publishes it
41
+ * repos the repos the app presents: `0x<owner>/<repo>` (that publisher's) or a
42
+ * bare `<repo>` (any publisher's); [] for every repo
41
43
  * fromBlock a block at or before the app's registration. The reader's log
42
44
  * scan starts here; leave it at 0 and a reader makes one RPC call
43
45
  * per 1000 blocks of chain history.
@@ -31,10 +31,10 @@
31
31
 
32
32
  import { spawn, spawnSync } from "node:child_process";
33
33
  import { createHash } from "node:crypto";
34
- import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
34
+ import { cpSync, existsSync, mkdtempSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
35
35
  import { createRequire } from "node:module";
36
36
  import { dirname, join, relative, resolve } from "node:path";
37
- import { homedir } from "node:os";
37
+ import { homedir, tmpdir } from "node:os";
38
38
  import { fileURLToPath, pathToFileURL } from "node:url";
39
39
 
40
40
  const HERE = dirname(fileURLToPath(import.meta.url));
@@ -63,9 +63,11 @@ const log = (s) => console.error(`[ship] ${s}`);
63
63
  // "sources": [{ "namespace": "…", "cwd": "…", "command": ["python", "-m", "…", …] }],
64
64
  // "live": ["readings"], // optional: namespaces members push to from their own wallets (devices,
65
65
  // // contributors), presented beside the sources'; true = every repo they push.
66
+ // // A bare name takes any wallet's repo of it; "0x<wallet>/readings" takes only
67
+ // // that wallet's. Sources are always the app's own wallet's repos.
66
68
  // // Without sources the app already presents every member's repo.
67
69
  // "network": "baseSepolia", // required: the SDK's network name
68
- // "owner": "0x…", // the wallet that publishes the app's repo; only `westmarch build` (the
70
+ // "owner": "0x…", // the wallet that publishes the app's repo and its sources; only `westmarch build` (the
69
71
  // // deploy button) needs it, westmarch-ship asks the CLI
70
72
  // "fangorn": "fangorn", // the CLI that signs; e.g. "node ~/fangorn/fangorn/lib/cli/cli.js",
71
73
  // // or "env HOME=<dir> node …/cli.js" to sign with <dir>/.fangorn/config.json
@@ -111,16 +113,17 @@ export function loadConfig(path) {
111
113
  if (!c.site.project) throw new Error(`${path}: site.project names the Cloudflare Worker`);
112
114
  for (const s of c.sources) if (!s.namespace || !Array.isArray(s.command))
113
115
  throw new Error(`${path}: each source needs a namespace and a command (an argv array)`);
114
- if (c.live != null && c.live !== true && !(Array.isArray(c.live) && c.live.every((n) => typeof n === "string")))
115
- throw new Error(`${path}: live is true (every repo members push) or a list of the namespaces they push to`);
116
+ if (c.live != null && c.live !== true && !(Array.isArray(c.live) && c.live.every((n) => typeof n === "string" && n && !/^0x(?![0-9a-fA-F]{40}\/.)/.test(n))))
117
+ throw new Error(`${path}: live is true (every repo members push) or a list of the namespaces they push to: "name" (any wallet's) or "0x<40 hex>/name" (that wallet's)`);
116
118
  return { relations: [], tags: [], types: {}, ...c };
117
119
  }
118
120
 
119
- /** The repo names the view presents: the sources' namespaces plus `live`'s, the ones members push
120
- * to themselves (devices, contributors). [] means every repo the app presents: no sources, or live: true. */
121
- export function presentedRepos(cfg) {
121
+ /** The repos the view presents (view.js `presents`): the sources' namespaces, pinned to `owner`, the
122
+ * wallet that publishes them, plus `live`'s, the ones members push to themselves (devices, contributors).
123
+ * [] means every repo the app presents: no sources, or live: true. */
124
+ export function presentedRepos(cfg, owner) {
122
125
  if (cfg.live === true) return [];
123
- return [...new Set([...cfg.sources.map((s) => s.namespace), ...(cfg.live ?? [])])];
126
+ return [...new Set([...cfg.sources.map((s) => `${owner.toLowerCase()}/${s.namespace}`), ...(cfg.live ?? [])])];
124
127
  }
125
128
 
126
129
  /** The Worker's wrangler config: the stock worker, the site as its assets, its bucket, the AI
@@ -173,8 +176,7 @@ let CLI = ["fangorn"];
173
176
  const fangornCli = (args, opts = {}) => run(CLI[0], [...CLI.slice(1), ...args], { capture: true, ...opts });
174
177
 
175
178
  /** The CLI must be the SDK version westmarch reads with. A different one signs against
176
- * another deployment's contracts: its commits land where nothing here looks. (It happened: a global 2026.08.18-dev CLI
177
- * claimed and published onto the previous registries.) */
179
+ * another deployment's contracts: its commits land where nothing here looks. */
178
180
  function assertSameDeployment(cli) {
179
181
  // "env HOME=… node cli.js": a CLI with its own config (another wallet); find the script past the env.
180
182
  let i = cli[0] === "env" ? 1 : 0;
@@ -226,11 +228,13 @@ export async function buildForDeploy(configPath, wranglerPath = join(dirname(res
226
228
 
227
229
  const base = dirname(resolve(configPath));
228
230
  const { site, writeCard } = await buildSite(cfg, base);
229
- writeCard(view.ORIGIN, { owner: cfg.owner, fromBlock, repos: [] });
231
+ // Sources pinned to the owner beside `live`, as a ship presents them; [] (every repo) with neither, or live: true.
232
+ const repos = presentedRepos(cfg, cfg.owner);
233
+ writeCard(view.ORIGIN, { owner: cfg.owner, fromBlock, repos });
230
234
  const gateway = process.env.FANGORN_IPFS_GATEWAY;
231
235
  w.assets = { ...w.assets, directory: relative(dirname(resolve(wranglerPath)), site) };
232
236
  w.vars = { ...w.vars, CONFIG: { app: cfg.app, name: cfg.name, description: cfg.description, fromBlock, network: cfg.network,
233
- schema: { types: cfg.types, relations: cfg.relations }, repos: [], paid: false },
237
+ schema: { types: cfg.types, relations: cfg.relations }, repos, paid: false },
234
238
  ...(gateway ? { IPFS_GATEWAY: gateway } : {}) };
235
239
  writeFileSync(wranglerPath, JSON.stringify(w, null, 1) + "\n");
236
240
  log(`built ${cfg.app} (fromBlock ${fromBlock}) into ${site}; wrangler deploy ships it`);
@@ -267,7 +271,7 @@ export async function ship(configPath, { crawl = true, deploy = true, dry = fals
267
271
  const repoDir = join(dir, "repo");
268
272
  if (!(await fangorn.onChainTip(me, cfg.app))) {
269
273
  // Its own records start now; an app presenting every repo (the explorer) reads from the deployment's start.
270
- if (presentedRepos(cfg).length) state.fromBlock = String(await chain.getBlockNumber());
274
+ if (presentedRepos(cfg, me).length) state.fromBlock = String(await chain.getBlockNumber());
271
275
  log(`creating the app's repo ${me}:${cfg.app}`);
272
276
  mkdirSync(repoDir, { recursive: true });
273
277
  fangornCli(["repo", "init", cfg.app], { cwd: repoDir, dry });
@@ -296,7 +300,7 @@ export async function ship(configPath, { crawl = true, deploy = true, dry = fals
296
300
  // site.url: the app's own domain, routed to the Worker. Otherwise its workers.dev URL,
297
301
  // which is only known once it is deployed: the first deploy learns it and the card follows.
298
302
  let url = cfg.site.url?.replace(/\/$/, "") ?? state.url ?? null;
299
- const repos = presentedRepos(cfg);
303
+ const repos = presentedRepos(cfg, me);
300
304
  const writeSite = (u) => writeCard(u, { owner: me, fromBlock: state.fromBlock, repos, paid: !!cfg.paid });
301
305
  writeSite(url ?? `https://${cfg.site.project}.workers.dev`);
302
306
 
@@ -471,7 +475,7 @@ export async function devnetShip(configPath, { crawl = false, only = null, port
471
475
  }
472
476
 
473
477
  const url = `http://127.0.0.1:${port}`;
474
- const repos = Object.keys(staged);
478
+ const repos = Object.keys(staged).map((ns) => `${owner.getAddress().toLowerCase()}/${ns}`);
475
479
  const { site, writeCard } = await buildSite(cfg, base);
476
480
  writeCard(url, { owner: owner.getAddress(), fromBlock: "0", repos, paid: !!services });
477
481
  const wcfg = wranglerConfig({ name: cfg.site.project, site, config: { app: cfg.app, name: cfg.name, description: cfg.description,
@@ -584,9 +588,10 @@ export async function buildLocal(configPath, out, { crawl = false, only = null,
584
588
  const statePath = join(base, ".ship/state.json");
585
589
  const fromBlock = (existsSync(statePath) ? JSON.parse(readFileSync(statePath, "utf8")).fromBlock : null) ?? cfg.fromBlock;
586
590
  const staged = new Set(cfg.sources.map((s) => s.namespace));
591
+ const { presents } = view;
587
592
  for (const t of await fangorn.repos(fromBlock == null ? {} : { fromBlock: BigInt(fromBlock) })) {
588
593
  if (t.repo === "fangorn.schema" || staged.has(t.repo) || (only && !only.includes(t.repo))
589
- || (cfg.live !== true && !cfg.live.includes(t.repo))) continue;
594
+ || (cfg.live !== true && !presents(cfg.live, t.owner, t.repo))) continue;
590
595
  const { contents } = await fangorn.readRepo(t.owner, t.repo);
591
596
  chain[`${t.owner.toLowerCase()}/${t.repo}`] = { records: contents.records };
592
597
  say(`live ${t.owner}/${t.repo}: ${contents.records.length} records, from the chain`);
@@ -596,7 +601,7 @@ export async function buildLocal(configPath, out, { crawl = false, only = null,
596
601
  rmSync(out, { recursive: true, force: true });
597
602
  mkdirSync(join(base, ".ship"), { recursive: true });
598
603
  const embed = view.cachedEmbed(join(base, ".ship/vectors.ndjson"));
599
- const repos = presentedRepos(cfg).filter((n) => !only || only.includes(n));
604
+ const repos = presentedRepos(cfg, owner).filter((n) => !only || only.includes(n.replace(/^0x[0-9a-f]{40}\//i, "")));
600
605
  const report = await view.publishView({ fangorn: view.localFangorn(chain), app: cfg.app,
601
606
  schema: { types: cfg.types, relations: cfg.relations }, repos, out, embed, log: say });
602
607
  const rows = Object.values(report).reduce((n, r) => n + r.added, 0);
@@ -616,10 +621,15 @@ if (process.argv[1]?.endsWith("ship.js") || process.argv[1]?.endsWith("westmarch
616
621
  || !w.compatibility_flags.includes("nodejs_compat") || !existsSync(w.main) || !existsSync(w.alias["@huggingface/transformers"]))
617
622
  throw new Error(`wrangler config: ${JSON.stringify(w)}`);
618
623
  const src = [{ namespace: "towns", command: ["py"] }];
619
- if (presentedRepos({ sources: src }).join() !== "towns" || presentedRepos({ sources: src, live: ["readings", "towns"] }).join() !== "towns,readings"
620
- || presentedRepos({ sources: src, live: true }).length || presentedRepos({ sources: [] }).length)
621
- throw new Error("presentedRepos: sources plus live namespaces; [] (every repo) for live: true or no sources");
622
- console.log("ship.js self-check ok — a source template fills per row, the Worker config binds its bucket, AI and cron, and presentedRepos merges sources and live namespaces");
624
+ const O = `0x${"Ab".repeat(20)}`, o = O.toLowerCase(), M = `0x${"cd".repeat(20)}`;
625
+ if (presentedRepos({ sources: src }, O).join() !== `${o}/towns` || presentedRepos({ sources: src, live: ["readings", `${M}/towns`] }, O).join() !== `${o}/towns,readings,${M}/towns`
626
+ || presentedRepos({ sources: src, live: true }, O).length || presentedRepos({ sources: [] }, O).length)
627
+ throw new Error("presentedRepos: sources pinned to the owner plus live namespaces as written; [] (every repo) for live: true or no sources");
628
+ const tmp = join(mkdtempSync(join(tmpdir(), "ship-")), "app.json");
629
+ const takes = (live) => { writeFileSync(tmp, JSON.stringify({ app: "a", name: "a", description: "d", site: { project: "a" }, live })); try { return !!loadConfig(tmp); } catch { return false; } };
630
+ if (![takes(["readings"]), takes([`${M}/readings`]), takes(true), !takes(["0x12/readings"]), !takes([`${M}/`]), !takes([""])].every(Boolean))
631
+ throw new Error("live: a bare name or 0x<40 hex>/name, nothing half-qualified");
632
+ console.log("ship.js self-check ok — a source template fills per row, the Worker config binds its bucket, AI and cron, and presentedRepos pins sources to the owner beside live namespaces, bare or owner-qualified");
623
633
  process.exit(0);
624
634
  }
625
635
  const [path, ...flags] = process.argv.slice(2);
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // westmarch-view: publish what an app presents as a view (see view.js).
3
3
  //
4
- // westmarch-view --app my-app --out site/view [--config app.json] [--repo catalog …] [--from-block N] [--rebake]
4
+ // westmarch-view --app my-app --out site/view [--config app.json] [--repo 0x<owner>/catalog …] [--from-block N] [--rebake]
5
5
  //
6
6
  // The schema (types, relations) is read from --config; without it every type is guessed.
7
7
  // Reads only; needs no key. Deploy <out> with the site, and put its URL in the card's
@@ -20,7 +20,7 @@ for (let i = 2; i < process.argv.length; i++) {
20
20
  if (m[1] === "repo") flags.repo.push(v); else flags[m[1]] = v;
21
21
  }
22
22
  if (flags.help || !flags.app || !flags.out) {
23
- console.error("usage: westmarch-view --app <name|0xid> --out <dir> [--config app.json] [--repo <name> …] [--from-block N] [--rebake]");
23
+ console.error("usage: westmarch-view --app <name|0xid> --out <dir> [--config app.json] [--repo <0xowner/name | name> …] [--from-block N] [--rebake]");
24
24
  process.exit(flags.help ? 0 : 2);
25
25
  }
26
26
 
@@ -1,11 +1,11 @@
1
1
  // An app's presented data → a view any Fangorn reader loads: the embedding lens
2
2
  // (WHITEPAPER_1 §3.4), computed by whoever runs this.
3
3
  //
4
- // westmarch-view --app my-app [--repo catalog …] --out site/view (a laptop, CI)
4
+ // westmarch-view --app my-app [--repo 0x<owner>/catalog …] --out site/view (a laptop, CI)
5
5
  // site/worker.js, on a cron (the free tier)
6
6
  //
7
- // Reads the registry and storage and nothing else: every repo the app presents (any
8
- // publisher's repo of a name it lists), at their latest heads; the schema
7
+ // Reads the registry and storage and nothing else: every repo the app presents (`presents`:
8
+ // `0x<owner>/<repo>` for one publisher's, a bare name for any publisher's), at their latest heads; the schema
9
9
  // (app.json's `types` + `relations`) comes from the caller. Embeds only records the view does not hold
10
10
  // yet, drops removed ones, and writes, through a store (a directory or an R2 bucket):
11
11
  //
@@ -54,6 +54,11 @@ const slug = (s) => (String(s ?? "").replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^
54
54
  /** A domain's name in the view: the publisher's address prefix and the repo. */
55
55
  export const domainFor = (owner, namespace) => `${slug(owner.slice(2, 10))}-${slug(namespace)}`;
56
56
 
57
+ /** Whether an app's `repos` list takes `owner`'s `repo`: an entry is `0x<owner>/<repo>` (that
58
+ * publisher's only) or a bare `<repo>` (any publisher's, for open namespaces). [] takes every repo. */
59
+ export const presents = (repos, owner, repo) =>
60
+ !repos?.length || repos.includes(repo) || repos.some((r) => r.toLowerCase() === `${owner.toLowerCase()}/${repo}`.toLowerCase());
61
+
57
62
  /** The text a row is embedded from, minus the `search_document: ` prefix that
58
63
  * `embedDocumentDirect` adds itself. */
59
64
  export function composeText(fields, rm) {
@@ -279,8 +284,8 @@ const sha256 = (s) => createHash("sha256").update(s).digest("hex");
279
284
 
280
285
  /**
281
286
  * Bring the view up to date with what the app presents. `fangorn` is an SDK client (reads
282
- * only); `schema` is app.json's { types, relations } (a type it leaves out is guessed); the view goes to `store`, or the directory `out`. `repos` narrows it to those repo
283
- * names (every publisher's); without it, every repo. `fromBlock` must be
287
+ * only); `schema` is app.json's { types, relations } (a type it leaves out is guessed); the view goes to `store`, or the directory `out`. `repos` narrows it
288
+ * (`presents`: `0x<owner>/<repo>` for one publisher's, a bare name for anyone's); without it, every repo. `fromBlock` must be
284
289
  * at or before the first update to a repo the app presents, or a repo the scan misses is left out;
285
290
  * left out, the SDK's deployment start. `embedBatch(texts)` embeds many at once where the
286
291
  * encoder batches (a Worker's AI binding); `embedder` is what the manifest records.
@@ -297,11 +302,10 @@ export async function publishView({ fangorn, app, schema, repos, out, store = fs
297
302
  const schemaHash = sha256(JSON.stringify(schema));
298
303
 
299
304
  // Every repo the app presents, at its latest head: one log scan.
300
- // ponytail: open: any publisher's repo of a listed name, and with no names, every repo.
301
- // Read the app's repo of citations instead once the SDK reads citations.
305
+ // ponytail: read the app's repo of citations instead once the SDK reads citations.
302
306
  const listed = (presented ?? await retry(() => fangorn.repos(fromBlock == null ? {} : { fromBlock: BigInt(fromBlock) })))
303
307
  // ponytail: "fangorn.schema" is where apps shipped before 2026-10-22 committed their schema; never data.
304
- .filter((t) => t.repo !== "fangorn.schema" && (!repos?.length || repos.includes(t.repo)));
308
+ .filter((t) => t.repo !== "fangorn.schema" && presents(repos, t.owner, t.repo));
305
309
  log(`${listed.length} repo(s) presented${repos?.length ? ` of ${repos.join(", ")}` : ""}`);
306
310
 
307
311
  const report = {};
@@ -328,7 +332,8 @@ export async function publishView({ fangorn, app, schema, repos, out, store = fs
328
332
  if (live.has(d)) continue;
329
333
  const m = await readJson(store, `cdn/domains/${d}/manifest`, null);
330
334
  const repo = m?.filter?.repo?.[0] ?? m?.filter?.namespace?.[0];
331
- if (repos?.length && !repos.includes(repo)) continue;
335
+ // A name this run doesn't cover (an --only build) is left alone; another publisher's repo of a covered name is dropped.
336
+ if (repos?.length && !repos.some((r) => r.replace(/^0x[0-9a-f]{40}\//i, "") === repo)) continue;
332
337
  await store.del(`cdn/domains/${d}`);
333
338
  report[d] = { added: 0, removed: m?.count ?? 0, dropped: true };
334
339
  log(`${d}: no longer presented by the app; dropped`);
@@ -647,6 +652,20 @@ if (import.meta.url === `file://${globalThis.process?.argv?.[1]}` && process.arg
647
652
  chain[`${PUB}/shop`] = shop;
648
653
  eq((await run())[d], { added: 2, removed: 0 }, "and presenting it again brings it back");
649
654
 
655
+ // Two publishers, one repo name: `0x<owner>/<repo>` takes only that publisher's; a bare name takes both.
656
+ eq([presents([`${PUB}/shop`], PUB, "shop"), presents([`${PUB.toUpperCase().replace("0X", "0x")}/shop`], PUB, "shop"),
657
+ presents([`${PUB}/shop`], OWNER, "shop"), presents(["shop"], OWNER, "shop"), presents([], OWNER, "x")],
658
+ [true, true, false, true, true], "presents: owner-qualified, case-insensitive address, bare name, empty list");
659
+ {
660
+ const twins = { [`${PUB}/shop`]: shop, [`${OWNER}/shop`]: { records: [rec("s1", TAG, { sku: "z9", name: "Squat", about: "not the shop you meant" })] } };
661
+ const o = mkdtempSync(`${tmpdir()}/view-twins-`);
662
+ const go = (repos) => publishView({ fangorn: localFangorn(twins), app: APP, schema, out: o, repos, embed, log: () => {} });
663
+ eq(Object.keys(await go(["shop"])).sort(), [domainFor(OWNER, "shop"), domainFor(PUB, "shop")].sort(), "a bare name presents every publisher's repo of it");
664
+ const r = await go([`${PUB}/shop`]);
665
+ eq([r[domainFor(OWNER, "shop")]?.dropped, existsSync(`${o}/cdn/domains/${domainFor(PUB, "shop")}`)], [true, true],
666
+ "pinning the name to one publisher drops the other's, already indexed");
667
+ }
668
+
650
669
  const setEmbedder = (e) => { const x = manifestOf(out, d); x.embedder = e; x.head = "moved"; writeFileSync(`${out}/cdn/domains/${d}/manifest`, JSON.stringify(x)); };
651
670
  setEmbedder({ ...EMBEDDER, runtime: "transformers.js cuda" });
652
671
  if ((await run())[d]?.error) throw new Error("another runtime of the same q8 model must be accepted");
@@ -766,5 +785,5 @@ if (import.meta.url === `file://${globalThis.process?.argv?.[1]}` && process.arg
766
785
  await r2.del(`cdn/domains/${d}`);
767
786
  eq([...objs.keys()].filter((k) => k.includes(d)), [], "del removes a folder and everything under it");
768
787
  }
769
- console.log("view.js self-check ok — domain names and embedded text are stable, the schema is applied, an unmoved head reads nothing, additions ship as delta shards, removals compact the domain, an excluded repo is dropped and a restored one returns, the real reader sees exactly the live records by the publisher's id, another encoder's view is refused, declared references land on their source rows, and the view builds the same into R2");
788
+ console.log("view.js self-check ok — domain names and embedded text are stable, the schema is applied, an unmoved head reads nothing, additions ship as delta shards, removals compact the domain, an excluded repo is dropped and a restored one returns, a repo name pinned to one publisher drops the others, the real reader sees exactly the live records by the publisher's id, another encoder's view is refused, declared references land on their source rows, and the view builds the same into R2");
770
789
  }
@@ -37,7 +37,10 @@ fangorn push
37
37
  ```
38
38
 
39
39
  The view picks it up within 5 minutes at `<url>/view`. A source that crawls something
40
- on a schedule goes in `app.json` `sources`, and publishes with
40
+ on a schedule goes in `app.json` `sources`. Once there are sources, the view presents
41
+ only those repos, published by your wallet, plus the namespaces listed in `"live"`
42
+ (`"live": ["items"]` for any wallet's `items`, `"0x<wallet>/items"` for one wallet's).
43
+ A source publishes with
41
44
  `npx westmarch-ship app.json --no-deploy` (the push to `main` deploys; without the flag it
42
45
  would deploy a second copy over this Worker). See westmarch's docs.
43
46
 
@@ -6,7 +6,7 @@
6
6
  "deploy": "wrangler deploy"
7
7
  },
8
8
  "dependencies": {
9
- "@fangorn-network/westmarch": "2026.11.3",
9
+ "@fangorn-network/westmarch": "0.1.2",
10
10
  "@huggingface/transformers": "^4.3.1",
11
11
  "wrangler": "^4.83.0"
12
12
  }