@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.
- package/docs/app-to-agent.md +2 -1
- package/package.json +5 -4
- package/src/agent/agent-card.js +2 -0
- package/src/publish/ship.js +32 -22
- 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",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"@fangorn-network/markov": "^0.1.0"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"@fangorn-network/sdk": "^
|
|
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": "
|
|
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
|
}
|
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
|
|
@@ -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.
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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);
|
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
|
|