@fangorn-network/westmarch 0.1.1 → 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.
- package/docs/app-to-agent.md +2 -1
- package/package.json +3 -2
- package/src/agent/agent-card.js +2 -0
- package/src/publish/ship.js +31 -20
- package/src/publish/view-cli.js +2 -2
- package/src/publish/view.js +29 -10
- package/template/README.md +4 -1
- package/template/package.json +1 -1
package/docs/app-to-agent.md
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "all-in-one builder for fangorn apps",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -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
|
}
|
package/src/agent/agent-card.js
CHANGED
|
@@ -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.
|
package/src/publish/ship.js
CHANGED
|
@@ -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
|
|
120
|
-
*
|
|
121
|
-
|
|
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
|
|
@@ -225,11 +228,13 @@ export async function buildForDeploy(configPath, wranglerPath = join(dirname(res
|
|
|
225
228
|
|
|
226
229
|
const base = dirname(resolve(configPath));
|
|
227
230
|
const { site, writeCard } = await buildSite(cfg, base);
|
|
228
|
-
|
|
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 });
|
|
229
234
|
const gateway = process.env.FANGORN_IPFS_GATEWAY;
|
|
230
235
|
w.assets = { ...w.assets, directory: relative(dirname(resolve(wranglerPath)), site) };
|
|
231
236
|
w.vars = { ...w.vars, CONFIG: { app: cfg.app, name: cfg.name, description: cfg.description, fromBlock, network: cfg.network,
|
|
232
|
-
schema: { types: cfg.types, relations: cfg.relations }, repos
|
|
237
|
+
schema: { types: cfg.types, relations: cfg.relations }, repos, paid: false },
|
|
233
238
|
...(gateway ? { IPFS_GATEWAY: gateway } : {}) };
|
|
234
239
|
writeFileSync(wranglerPath, JSON.stringify(w, null, 1) + "\n");
|
|
235
240
|
log(`built ${cfg.app} (fromBlock ${fromBlock}) into ${site}; wrangler deploy ships it`);
|
|
@@ -266,7 +271,7 @@ export async function ship(configPath, { crawl = true, deploy = true, dry = fals
|
|
|
266
271
|
const repoDir = join(dir, "repo");
|
|
267
272
|
if (!(await fangorn.onChainTip(me, cfg.app))) {
|
|
268
273
|
// Its own records start now; an app presenting every repo (the explorer) reads from the deployment's start.
|
|
269
|
-
if (presentedRepos(cfg).length) state.fromBlock = String(await chain.getBlockNumber());
|
|
274
|
+
if (presentedRepos(cfg, me).length) state.fromBlock = String(await chain.getBlockNumber());
|
|
270
275
|
log(`creating the app's repo ${me}:${cfg.app}`);
|
|
271
276
|
mkdirSync(repoDir, { recursive: true });
|
|
272
277
|
fangornCli(["repo", "init", cfg.app], { cwd: repoDir, dry });
|
|
@@ -295,7 +300,7 @@ export async function ship(configPath, { crawl = true, deploy = true, dry = fals
|
|
|
295
300
|
// site.url: the app's own domain, routed to the Worker. Otherwise its workers.dev URL,
|
|
296
301
|
// which is only known once it is deployed: the first deploy learns it and the card follows.
|
|
297
302
|
let url = cfg.site.url?.replace(/\/$/, "") ?? state.url ?? null;
|
|
298
|
-
const repos = presentedRepos(cfg);
|
|
303
|
+
const repos = presentedRepos(cfg, me);
|
|
299
304
|
const writeSite = (u) => writeCard(u, { owner: me, fromBlock: state.fromBlock, repos, paid: !!cfg.paid });
|
|
300
305
|
writeSite(url ?? `https://${cfg.site.project}.workers.dev`);
|
|
301
306
|
|
|
@@ -470,7 +475,7 @@ export async function devnetShip(configPath, { crawl = false, only = null, port
|
|
|
470
475
|
}
|
|
471
476
|
|
|
472
477
|
const url = `http://127.0.0.1:${port}`;
|
|
473
|
-
const repos = Object.keys(staged);
|
|
478
|
+
const repos = Object.keys(staged).map((ns) => `${owner.getAddress().toLowerCase()}/${ns}`);
|
|
474
479
|
const { site, writeCard } = await buildSite(cfg, base);
|
|
475
480
|
writeCard(url, { owner: owner.getAddress(), fromBlock: "0", repos, paid: !!services });
|
|
476
481
|
const wcfg = wranglerConfig({ name: cfg.site.project, site, config: { app: cfg.app, name: cfg.name, description: cfg.description,
|
|
@@ -583,9 +588,10 @@ export async function buildLocal(configPath, out, { crawl = false, only = null,
|
|
|
583
588
|
const statePath = join(base, ".ship/state.json");
|
|
584
589
|
const fromBlock = (existsSync(statePath) ? JSON.parse(readFileSync(statePath, "utf8")).fromBlock : null) ?? cfg.fromBlock;
|
|
585
590
|
const staged = new Set(cfg.sources.map((s) => s.namespace));
|
|
591
|
+
const { presents } = view;
|
|
586
592
|
for (const t of await fangorn.repos(fromBlock == null ? {} : { fromBlock: BigInt(fromBlock) })) {
|
|
587
593
|
if (t.repo === "fangorn.schema" || staged.has(t.repo) || (only && !only.includes(t.repo))
|
|
588
|
-
|| (cfg.live !== true && !cfg.live.
|
|
594
|
+
|| (cfg.live !== true && !presents(cfg.live, t.owner, t.repo))) continue;
|
|
589
595
|
const { contents } = await fangorn.readRepo(t.owner, t.repo);
|
|
590
596
|
chain[`${t.owner.toLowerCase()}/${t.repo}`] = { records: contents.records };
|
|
591
597
|
say(`live ${t.owner}/${t.repo}: ${contents.records.length} records, from the chain`);
|
|
@@ -595,7 +601,7 @@ export async function buildLocal(configPath, out, { crawl = false, only = null,
|
|
|
595
601
|
rmSync(out, { recursive: true, force: true });
|
|
596
602
|
mkdirSync(join(base, ".ship"), { recursive: true });
|
|
597
603
|
const embed = view.cachedEmbed(join(base, ".ship/vectors.ndjson"));
|
|
598
|
-
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, "")));
|
|
599
605
|
const report = await view.publishView({ fangorn: view.localFangorn(chain), app: cfg.app,
|
|
600
606
|
schema: { types: cfg.types, relations: cfg.relations }, repos, out, embed, log: say });
|
|
601
607
|
const rows = Object.values(report).reduce((n, r) => n + r.added, 0);
|
|
@@ -615,10 +621,15 @@ if (process.argv[1]?.endsWith("ship.js") || process.argv[1]?.endsWith("westmarch
|
|
|
615
621
|
|| !w.compatibility_flags.includes("nodejs_compat") || !existsSync(w.main) || !existsSync(w.alias["@huggingface/transformers"]))
|
|
616
622
|
throw new Error(`wrangler config: ${JSON.stringify(w)}`);
|
|
617
623
|
const src = [{ namespace: "towns", command: ["py"] }];
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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");
|
|
622
633
|
process.exit(0);
|
|
623
634
|
}
|
|
624
635
|
const [path, ...flags] = process.argv.slice(2);
|
package/src/publish/view-cli.js
CHANGED
|
@@ -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
|
|
package/src/publish/view.js
CHANGED
|
@@ -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 (
|
|
8
|
-
// publisher's
|
|
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
|
|
283
|
-
*
|
|
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:
|
|
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" && (
|
|
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
|
-
|
|
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
|
}
|
package/template/README.md
CHANGED
|
@@ -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
|
|
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
|
|