world-building-framework 0.2.0 → 0.2.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/README.md CHANGED
@@ -6,6 +6,10 @@ Build a fictional world as a folder of Markdown files, and publish it as a wiki
6
6
 
7
7
  You need [Node.js 22](https://nodejs.org/) or newer and git.
8
8
 
9
+ **With an AI agent:** in an empty folder, ask it: "Set up a world here with the npm package world-building-framework." It follows the steps in the next section and installs the skills it needs itself.
10
+
11
+ **By hand:**
12
+
9
13
  ```
10
14
  mkdir my-world
11
15
  cd my-world
@@ -14,7 +18,7 @@ npx world-building-framework init --agent claude
14
18
  npm install
15
19
  ```
16
20
 
17
- `init` sets the folder up as a world. `--agent claude` also installs skills for Claude Code. Leave it out if you use another agent; the generated `AGENTS.md` works with any agent.
21
+ `init` sets the folder up as a world. `--agent claude` also installs skills for Claude Code; for another agent, use `--skills-dir <folder>` with the folder that agent reads project skills from. Leave both out if you don't want skills; the generated `AGENTS.md` works with any agent.
18
22
 
19
23
  `init` never overwrites a file you already have. It creates:
20
24
  - `package.json`, with the framework version pinned
@@ -28,7 +32,21 @@ Then put it on GitHub:
28
32
 
29
33
  1. Create a GitHub repo, commit everything (including `package-lock.json` and `.wbf/managed.json`), and push.
30
34
  2. In the repo on GitHub: **Settings → Pages → Source: GitHub Actions**. On a free account, Pages needs the repo to be public.
31
- 3. Every push to your default branch now publishes the wiki to `https://<you>.github.io/<repo>/wiki/`, with a landing page at `https://<you>.github.io/<repo>/`.
35
+ 3. Every push to your default branch now publishes the wiki to `https://<you>.github.io/<repo>/wiki/`, with a landing page at `https://<you>.github.io/<repo>/`. To publish without a push, run it by hand: **Actions → Publish wiki → Run workflow**.
36
+
37
+ You don't enter the URL anywhere; the workflow asks GitHub for it. For a custom domain, set it under **Settings → Pages → Custom domain**; the next run picks it up.
38
+
39
+ ## For AI agents: setting up a world
40
+
41
+ If a user asks you to set up a world with this package:
42
+
43
+ 1. In the folder they chose (run `git init` first if it isn't a git repo), run `npx world-building-framework init`, adding the option for **your own** skills:
44
+ - Claude Code: `--agent claude`
45
+ - any other agent that reads Agent Skills (`SKILL.md`): `--skills-dir <your project skills folder>`, relative to the repo. Only use a folder you know your agent reads; if you don't know one, leave the option out.
46
+ - Add `--title "<the world's name>"` if the user named it.
47
+ 2. Run `npm install`.
48
+ 3. Read the generated `AGENTS.md` and, if installed, the `world-vault` skill, then follow them for all further work.
49
+ 4. Tell the user the remaining steps from "Then put it on GitHub" above.
32
50
 
33
51
  ## Writing your world
34
52
 
@@ -49,8 +67,31 @@ Then put it on GitHub:
49
67
 
50
68
  Run `npx wbf` only inside a world folder. Elsewhere it would fetch an unrelated npm package that happens to be called `wbf`.
51
69
 
70
+ **With an AI agent that has no terminal (MCP):** see [Agents without a terminal](#agents-without-a-terminal-mcp) below.
71
+
52
72
  **In Obsidian:** open the world folder as a vault to browse and read. Create and change files through `wbf`, so they get valid UUID names and checked links.
53
73
 
74
+ ## Agents without a terminal (MCP)
75
+
76
+ Some AI apps can't run commands, but can use tools from an MCP server. `wbf mcp` offers the same functions as the commands above as tools: `schema`, `search`, `read`, `validate`, `create`, `update` and `list_variants`. The app starts the server itself; you only add it to the app's MCP settings once. You need [Node.js 22](https://nodejs.org/) or newer installed.
77
+
78
+ For Claude Desktop, open **Settings → Developer → Edit Config** and add:
79
+
80
+ ```json
81
+ {
82
+ "mcpServers": {
83
+ "my-world": {
84
+ "command": "npx",
85
+ "args": ["-y", "world-building-framework@0.2.2", "mcp", "--vault", "C:\\Users\\you\\worlds\\my-world"]
86
+ }
87
+ }
88
+ }
89
+ ```
90
+
91
+ - Use the **absolute path** to your world folder, and the framework version from your world's `package.json`, so the app checks files with the same rules as your published wiki. If they differ, the `schema` tool says so.
92
+ - Restart the app afterwards. Other apps use the same `command` and `args`; see their docs for where they go.
93
+ - Setting a world up (`init`, `npm install`) and publishing (commit and push) still happen outside the app, as described above.
94
+
54
95
  ## How a world is organized
55
96
 
56
97
  - **Files are named by UUID,** e.g. `7c1e9b2a-….md`. The readable name is the `title` field, so two characters can share a name.
@@ -81,6 +122,8 @@ Run `npx wbf` only inside a world folder. Elsewhere it would fetch an unrelated
81
122
 
82
123
  **Every wiki is public.** Anyone who knows or guesses a variant's address can open it. Variants decide what each wiki contains; they don't lock anything.
83
124
 
125
+ **Variants filter whole files.** A file that's included is shown in full, body text and all. Put a secret in its own file with narrower tags rather than in the text of a file everyone sees. A link to a file a wiki leaves out turns into plain text, so its title still shows there.
126
+
84
127
  ## Preview locally
85
128
 
86
129
  ```
@@ -98,7 +141,7 @@ npm install world-building-framework@latest
98
141
  npx wbf refresh
99
142
  ```
100
143
 
101
- `refresh` updates the files `init` generated (the workflow, `AGENTS.md`, skills) to the new version. It leaves any file you've edited alone and lists it. `npx wbf init --agent <name>` adds skills for another agent later.
144
+ `refresh` updates the files `init` generated (the workflow, `AGENTS.md`, skills) to the new version. It leaves any file you've edited alone and lists it. `npx wbf init --agent <name>` or `--skills-dir <folder>` adds skills for another agent later; `refresh` keeps those current too.
102
145
 
103
146
  ## License
104
147
 
@@ -4,13 +4,7 @@ import { type ValidationIssue } from "../../core/schema/index.js";
4
4
  export declare const fileInputSchema: z.ZodObject<{
5
5
  frontmatter: z.ZodRecord<z.ZodString, z.ZodUnknown>;
6
6
  body: z.ZodDefault<z.ZodString>;
7
- }, "strip", z.ZodTypeAny, {
8
- frontmatter: Record<string, unknown>;
9
- body: string;
10
- }, {
11
- frontmatter: Record<string, unknown>;
12
- body?: string | undefined;
13
- }>;
7
+ }, z.core.$strip>;
14
8
  export interface CreateOutcome {
15
9
  written: boolean;
16
10
  id?: string;
@@ -5,6 +5,8 @@ export interface InitOptions {
5
5
  title?: string;
6
6
  /** Agents to install skills for (see AGENT_SKILL_DIRS). */
7
7
  agents?: string[];
8
+ /** Repo-relative folders to install skills into directly, for agents without a shortcut. */
9
+ skillsDirs?: string[];
8
10
  /** Branch the publish workflow runs on; defaults to the git repo's current branch, else "main". */
9
11
  branch?: string;
10
12
  }
@@ -3,7 +3,7 @@ import { existsSync } from "node:fs";
3
3
  import { readFile, writeFile } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { WORLD_CONFIG_FILENAME } from "../../core/config/index.js";
6
- import { AGENT_SKILL_DIRS, contentHash, readFrameworkVersion, readManifest, renderManagedFiles, writeManifest, writeRepoFile, PACKAGE_ROOT, } from "../managed-files.js";
6
+ import { AGENT_SKILL_DIRS, contentHash, normalizeSkillsDir, readFrameworkVersion, readManifest, renderManagedFiles, writeManifest, writeRepoFile, PACKAGE_ROOT, } from "../managed-files.js";
7
7
  const PACKAGE_NAME = "world-building-framework";
8
8
  function titleFromFolder(dir) {
9
9
  const words = path.basename(path.resolve(dir)).split(/[-_\s]+/).filter(Boolean);
@@ -29,9 +29,11 @@ export async function runInit(options = {}) {
29
29
  const agents = [...new Set(options.agents ?? [])];
30
30
  for (const agent of agents) {
31
31
  if (!AGENT_SKILL_DIRS[agent]) {
32
- throw new Error(`Unknown agent "${agent}". Supported: ${Object.keys(AGENT_SKILL_DIRS).join(", ")}`);
32
+ throw new Error(`Unknown agent "${agent}". Supported: ${Object.keys(AGENT_SKILL_DIRS).join(", ")}. ` +
33
+ "For another agent, give its project skills folder instead: --skills-dir <folder>");
33
34
  }
34
35
  }
36
+ const skillsDirs = [...new Set((options.skillsDirs ?? []).map(normalizeSkillsDir))];
35
37
  const version = await readFrameworkVersion();
36
38
  const outcome = { dir, created: [], updated: [], skipped: [], nextSteps: [] };
37
39
  const exists = (rel) => existsSync(path.join(dir, rel));
@@ -94,6 +96,7 @@ export async function runInit(options = {}) {
94
96
  const settings = {
95
97
  branch: options.branch ?? previous?.branch ?? detectBranch(dir),
96
98
  agents: [...new Set([...(previous?.agents ?? []), ...agents])],
99
+ skillsDirs: [...new Set([...(previous?.skillsDirs ?? []), ...skillsDirs])],
97
100
  };
98
101
  const manifest = { frameworkVersion: version, ...settings, files: { ...(previous?.files ?? {}) } };
99
102
  for (const [rel, content] of await renderManagedFiles(version, settings)) {
@@ -112,8 +115,9 @@ export async function runInit(options = {}) {
112
115
  "Commit everything, including package-lock.json and .wbf/managed.json.",
113
116
  "On GitHub: Settings → Pages → Source: GitHub Actions. Then push to publish.",
114
117
  ];
115
- if (agents.length === 0) {
116
- outcome.nextSteps.push(`Optional: install skills for your AI agent with npx wbf init --agent <${Object.keys(AGENT_SKILL_DIRS).join("|")}>.`);
118
+ if (settings.agents.length === 0 && settings.skillsDirs.length === 0) {
119
+ outcome.nextSteps.push(`Optional: install skills for your AI agent with npx wbf init --agent <${Object.keys(AGENT_SKILL_DIRS).join("|")}>, ` +
120
+ "or for any other agent with npx wbf init --skills-dir <its project skills folder>.");
117
121
  }
118
122
  return outcome;
119
123
  }
@@ -49,7 +49,7 @@ export async function runPublish(vaultOption, options) {
49
49
  await setQuartzPageTitle(quartzDir, title);
50
50
  for (const name of variantNames) {
51
51
  const query = getBuildVariant(config, name);
52
- const pages = buildDerivedTree(files, query);
52
+ const pages = buildDerivedTree(files, query, { title });
53
53
  await syncDerivedTreeToQuartz(path.join(quartzDir, "content"), pages);
54
54
  if (options.baseUrl) {
55
55
  await setQuartzBaseUrl(quartzDir, `${normalizeBaseUrl(options.baseUrl)}/${name}`);
@@ -25,7 +25,11 @@ export async function runRefresh(options = {}) {
25
25
  kept: [],
26
26
  retired: [],
27
27
  };
28
- const rendered = await renderManagedFiles(toVersion, { branch: manifest.branch, agents: manifest.agents });
28
+ const rendered = await renderManagedFiles(toVersion, {
29
+ branch: manifest.branch,
30
+ agents: manifest.agents,
31
+ skillsDirs: manifest.skillsDirs,
32
+ });
29
33
  const files = {};
30
34
  for (const [rel, content] of rendered) {
31
35
  const newHash = contentHash(content);
@@ -11,7 +11,7 @@ export async function runRender(vaultOption, options) {
11
11
  const config = await loadWorldConfig(vaultDir);
12
12
  const query = getBuildVariant(config, options.variant);
13
13
  const files = await readVault(vaultDir);
14
- const pages = buildDerivedTree(files, query);
14
+ const pages = buildDerivedTree(files, query, { title: worldTitle(config) });
15
15
  // Before syncing: a fresh clone must not land on top of a content folder we just wrote.
16
16
  if (options.build)
17
17
  await ensureQuartz(quartzDir);
package/dist/cli/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
3
  import { Command } from "commander";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { createMcpServer } from "../mcp/server.js";
4
6
  import { runSchema } from "./commands/schema.js";
5
7
  import { runInit } from "./commands/init.js";
6
8
  import { runRefresh } from "./commands/refresh.js";
@@ -35,9 +37,16 @@ program
35
37
  .option("--dir <path>", "folder to set up (default: current folder)")
36
38
  .option("--title <title>", "the world's display title (default: derived from the folder name)")
37
39
  .option("--agent <name>", "install skills for this AI agent; repeat for several", (value, previous) => [...previous, value], [])
40
+ .option("--skills-dir <folder>", "install skills into this repo-relative folder, for an agent --agent doesn't know; repeat for several", (value, previous) => [...previous, value], [])
38
41
  .option("--branch <name>", "branch the publish workflow runs on (default: the repo's current branch, else main)")
39
42
  .action(handle(async (opts) => {
40
- const outcome = await runInit({ dir: opts.dir, title: opts.title, agents: opts.agent, branch: opts.branch });
43
+ const outcome = await runInit({
44
+ dir: opts.dir,
45
+ title: opts.title,
46
+ agents: opts.agent,
47
+ skillsDirs: opts.skillsDir,
48
+ branch: opts.branch,
49
+ });
41
50
  console.log(JSON.stringify(outcome, null, 2));
42
51
  }));
43
52
  program
@@ -154,4 +163,12 @@ program
154
163
  });
155
164
  console.log(JSON.stringify(outcome, null, 2));
156
165
  }));
166
+ program
167
+ .command("mcp")
168
+ .description("Run an MCP server over stdio, offering the vault commands as tools to agents without a terminal. Started by the agent's host, not by hand")
169
+ .option("-v, --vault <path>", "world folder (default: WBF_VAULT_DIR, else the current folder)")
170
+ .action(handle(async (opts) => {
171
+ // stdout carries the protocol from here on; nothing else may write to it.
172
+ await createMcpServer({ vault: opts.vault, version }).connect(new StdioServerTransport());
173
+ }));
157
174
  await program.parseAsync(process.argv);
@@ -10,7 +10,17 @@ export declare const AGENT_SKILL_DIRS: Record<string, string>;
10
10
  export interface ManagedSettings {
11
11
  branch: string;
12
12
  agents: string[];
13
+ /** Skill folders given directly (`--skills-dir`), for agents without a shortcut in AGENT_SKILL_DIRS. Missing in manifests from 0.2.1 and older. */
14
+ skillsDirs?: string[];
13
15
  }
16
+ /**
17
+ * Checks a `--skills-dir` value and returns it repo-relative with forward
18
+ * slashes, e.g. ".agent\skills\" -> ".agent/skills". It must stay inside the
19
+ * repo: no absolute path, no "..".
20
+ */
21
+ export declare function normalizeSkillsDir(dir: string): string;
22
+ /** Every folder skills get copied into: the agents' known folders plus any given directly, without duplicates. */
23
+ export declare function skillTargets(settings: ManagedSettings): string[];
14
24
  /** The record of generated files, kept in the world repo (committed) at MANIFEST_PATH. */
15
25
  export interface Manifest extends ManagedSettings {
16
26
  frameworkVersion: string;
@@ -27,7 +37,7 @@ export declare function readFrameworkVersion(): Promise<string>;
27
37
  /**
28
38
  * The files wbf owns in a world repo, rendered for this framework version:
29
39
  * the publish workflow, AGENTS.md, and the package's skills copied into each
30
- * chosen agent's skill folder. Keyed by repo-relative path.
40
+ * skill folder (see skillTargets). Keyed by repo-relative path.
31
41
  */
32
42
  export declare function renderManagedFiles(version: string, settings: ManagedSettings): Promise<Map<string, string>>;
33
43
  export declare function readManifest(rootDir: string): Promise<Manifest | undefined>;
@@ -13,6 +13,31 @@ export const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.
13
13
  export const AGENT_SKILL_DIRS = {
14
14
  claude: ".claude/skills",
15
15
  };
16
+ /**
17
+ * Checks a `--skills-dir` value and returns it repo-relative with forward
18
+ * slashes, e.g. ".agent\skills\" -> ".agent/skills". It must stay inside the
19
+ * repo: no absolute path, no "..".
20
+ */
21
+ export function normalizeSkillsDir(dir) {
22
+ if (path.isAbsolute(dir) || /^[a-zA-Z]:/.test(dir)) {
23
+ throw new Error(`Skills folder "${dir}" must be relative to the world repo, e.g. .agent/skills`);
24
+ }
25
+ const parts = dir.split(/[\\/]+/).filter((part) => part !== "" && part !== ".");
26
+ if (parts.length === 0 || parts.includes("..")) {
27
+ throw new Error(`Skills folder "${dir}" must be a folder inside the world repo, e.g. .agent/skills`);
28
+ }
29
+ return parts.join("/");
30
+ }
31
+ /** Every folder skills get copied into: the agents' known folders plus any given directly, without duplicates. */
32
+ export function skillTargets(settings) {
33
+ const targets = settings.agents.map((agent) => {
34
+ const target = AGENT_SKILL_DIRS[agent];
35
+ if (!target)
36
+ throw new Error(`Unknown agent "${agent}". Supported: ${Object.keys(AGENT_SKILL_DIRS).join(", ")}`);
37
+ return target;
38
+ });
39
+ return [...new Set([...targets, ...(settings.skillsDirs ?? [])])];
40
+ }
16
41
  export const MANIFEST_PATH = ".wbf/managed.json";
17
42
  /**
18
43
  * Content hash, ignoring line endings: git may check a file out with CRLF on
@@ -44,7 +69,7 @@ async function listFiles(dir, prefix = "") {
44
69
  /**
45
70
  * The files wbf owns in a world repo, rendered for this framework version:
46
71
  * the publish workflow, AGENTS.md, and the package's skills copied into each
47
- * chosen agent's skill folder. Keyed by repo-relative path.
72
+ * skill folder (see skillTargets). Keyed by repo-relative path.
48
73
  */
49
74
  export async function renderManagedFiles(version, settings) {
50
75
  const files = new Map();
@@ -52,10 +77,7 @@ export async function renderManagedFiles(version, settings) {
52
77
  files.set("AGENTS.md", await template("AGENTS.md", version, settings.branch));
53
78
  const skillsDir = path.join(PACKAGE_ROOT, "skills");
54
79
  const skillFiles = await listFiles(skillsDir);
55
- for (const agent of settings.agents) {
56
- const target = AGENT_SKILL_DIRS[agent];
57
- if (!target)
58
- throw new Error(`Unknown agent "${agent}". Supported: ${Object.keys(AGENT_SKILL_DIRS).join(", ")}`);
80
+ for (const target of skillTargets(settings)) {
59
81
  for (const rel of skillFiles) {
60
82
  files.set(`${target}/${rel}`, await readFile(path.join(skillsDir, rel), "utf8"));
61
83
  }
@@ -3,5 +3,5 @@
3
3
  * that are useful without a vault (e.g. `schema`).
4
4
  */
5
5
  export declare function findVaultDir(cliOption: string | undefined): string | undefined;
6
- /** Resolves which vault to operate on: explicit flag, then WBF_VAULT_DIR, then the current folder if it has a world.config.json, then a single directory under ./vaults/. */
6
+ /** Resolves which vault to operate on: explicit flag, then WBF_VAULT_DIR, then the current folder if it has a world.config.json. */
7
7
  export declare function resolveVaultDir(cliOption: string | undefined): string;
@@ -1,24 +1,6 @@
1
- import { existsSync, readdirSync, statSync } from "node:fs";
1
+ import { existsSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { WORLD_CONFIG_FILENAME } from "../core/config/index.js";
4
- function findSingleVaultUnderVaultsDir() {
5
- const vaultsRoot = path.join(process.cwd(), "vaults");
6
- let entries;
7
- try {
8
- entries = readdirSync(vaultsRoot).filter((name) => {
9
- try {
10
- return statSync(path.join(vaultsRoot, name)).isDirectory();
11
- }
12
- catch {
13
- return false;
14
- }
15
- });
16
- }
17
- catch {
18
- return undefined;
19
- }
20
- return entries.length === 1 ? path.join(vaultsRoot, entries[0]) : undefined;
21
- }
22
4
  /** The current folder, if it's a vault — i.e. it has a world.config.json (the vault repo is the project, see claude-dev/DISTRIBUTION_PLAN.md). */
23
5
  function currentFolderIfVault() {
24
6
  const cwd = process.cwd();
@@ -29,16 +11,14 @@ function currentFolderIfVault() {
29
11
  * that are useful without a vault (e.g. `schema`).
30
12
  */
31
13
  export function findVaultDir(cliOption) {
32
- // The ./vaults/ fallback is the old fork-the-framework layout, kept until the
33
- // world-repo layout fully replaces it.
34
- return cliOption ?? process.env.WBF_VAULT_DIR ?? currentFolderIfVault() ?? findSingleVaultUnderVaultsDir();
14
+ return cliOption ?? process.env.WBF_VAULT_DIR ?? currentFolderIfVault();
35
15
  }
36
- /** Resolves which vault to operate on: explicit flag, then WBF_VAULT_DIR, then the current folder if it has a world.config.json, then a single directory under ./vaults/. */
16
+ /** Resolves which vault to operate on: explicit flag, then WBF_VAULT_DIR, then the current folder if it has a world.config.json. */
37
17
  export function resolveVaultDir(cliOption) {
38
18
  const vaultDir = findVaultDir(cliOption);
39
19
  if (!vaultDir) {
40
- throw new Error("No vault specified — run this inside a vault (a folder with world.config.json), pass --vault <path>, " +
41
- "set WBF_VAULT_DIR, or have exactly one directory under ./vaults/.");
20
+ throw new Error("No vault specified — run this inside a world (a folder with world.config.json; `wbf init` creates one), " +
21
+ "pass --vault <path>, or set WBF_VAULT_DIR.");
42
22
  }
43
23
  return vaultDir;
44
24
  }
@@ -3,226 +3,26 @@ import { z } from "zod";
3
3
  * A vault's own project-specific setup parameters (PROJECT_CONFIG.md).
4
4
  * `title`, `entityTypes` and `buildVariants` are the fields with a decided consumer
5
5
  * so far — tag vocabulary itself (as opposed to where it's used, buildVariants)
6
- * and relation-field naming are still open, so this stays passthrough rather
6
+ * and relation-field naming are still open, so this stays loose (unknown keys kept) rather
7
7
  * than locking in a shape for those prematurely.
8
8
  */
9
9
  export declare const worldConfigSchema: z.ZodObject<{
10
- /** The world's name, shown as the published site's title (every variant and the landing page). */
11
10
  title: z.ZodOptional<z.ZodString>;
12
- entityTypes: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
13
- /** Named filter queries (render-pipeline's BuildVariantQuery) — PROJECT_CONFIG.md #3. */
11
+ entityTypes: z.ZodOptional<z.ZodArray<z.ZodString>>;
14
12
  buildVariants: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
15
- tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
13
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
16
14
  dateRange: z.ZodOptional<z.ZodObject<{
17
15
  from: z.ZodOptional<z.ZodObject<{
18
16
  year: z.ZodNumber;
19
17
  day: z.ZodOptional<z.ZodNumber>;
20
- }, "strip", z.ZodTypeAny, {
21
- year: number;
22
- day?: number | undefined;
23
- }, {
24
- year: number;
25
- day?: number | undefined;
26
- }>>;
18
+ }, z.core.$strip>>;
27
19
  to: z.ZodOptional<z.ZodObject<{
28
20
  year: z.ZodNumber;
29
21
  day: z.ZodOptional<z.ZodNumber>;
30
- }, "strip", z.ZodTypeAny, {
31
- year: number;
32
- day?: number | undefined;
33
- }, {
34
- year: number;
35
- day?: number | undefined;
36
- }>>;
37
- }, "strip", z.ZodTypeAny, {
38
- from?: {
39
- year: number;
40
- day?: number | undefined;
41
- } | undefined;
42
- to?: {
43
- year: number;
44
- day?: number | undefined;
45
- } | undefined;
46
- }, {
47
- from?: {
48
- year: number;
49
- day?: number | undefined;
50
- } | undefined;
51
- to?: {
52
- year: number;
53
- day?: number | undefined;
54
- } | undefined;
55
- }>>;
56
- }, "strip", z.ZodTypeAny, {
57
- tags?: string[] | undefined;
58
- dateRange?: {
59
- from?: {
60
- year: number;
61
- day?: number | undefined;
62
- } | undefined;
63
- to?: {
64
- year: number;
65
- day?: number | undefined;
66
- } | undefined;
67
- } | undefined;
68
- }, {
69
- tags?: string[] | undefined;
70
- dateRange?: {
71
- from?: {
72
- year: number;
73
- day?: number | undefined;
74
- } | undefined;
75
- to?: {
76
- year: number;
77
- day?: number | undefined;
78
- } | undefined;
79
- } | undefined;
80
- }>>>;
81
- }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
82
- /** The world's name, shown as the published site's title (every variant and the landing page). */
83
- title: z.ZodOptional<z.ZodString>;
84
- entityTypes: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
85
- /** Named filter queries (render-pipeline's BuildVariantQuery) — PROJECT_CONFIG.md #3. */
86
- buildVariants: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
87
- tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
88
- dateRange: z.ZodOptional<z.ZodObject<{
89
- from: z.ZodOptional<z.ZodObject<{
90
- year: z.ZodNumber;
91
- day: z.ZodOptional<z.ZodNumber>;
92
- }, "strip", z.ZodTypeAny, {
93
- year: number;
94
- day?: number | undefined;
95
- }, {
96
- year: number;
97
- day?: number | undefined;
98
- }>>;
99
- to: z.ZodOptional<z.ZodObject<{
100
- year: z.ZodNumber;
101
- day: z.ZodOptional<z.ZodNumber>;
102
- }, "strip", z.ZodTypeAny, {
103
- year: number;
104
- day?: number | undefined;
105
- }, {
106
- year: number;
107
- day?: number | undefined;
108
- }>>;
109
- }, "strip", z.ZodTypeAny, {
110
- from?: {
111
- year: number;
112
- day?: number | undefined;
113
- } | undefined;
114
- to?: {
115
- year: number;
116
- day?: number | undefined;
117
- } | undefined;
118
- }, {
119
- from?: {
120
- year: number;
121
- day?: number | undefined;
122
- } | undefined;
123
- to?: {
124
- year: number;
125
- day?: number | undefined;
126
- } | undefined;
127
- }>>;
128
- }, "strip", z.ZodTypeAny, {
129
- tags?: string[] | undefined;
130
- dateRange?: {
131
- from?: {
132
- year: number;
133
- day?: number | undefined;
134
- } | undefined;
135
- to?: {
136
- year: number;
137
- day?: number | undefined;
138
- } | undefined;
139
- } | undefined;
140
- }, {
141
- tags?: string[] | undefined;
142
- dateRange?: {
143
- from?: {
144
- year: number;
145
- day?: number | undefined;
146
- } | undefined;
147
- to?: {
148
- year: number;
149
- day?: number | undefined;
150
- } | undefined;
151
- } | undefined;
152
- }>>>;
153
- }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
154
- /** The world's name, shown as the published site's title (every variant and the landing page). */
155
- title: z.ZodOptional<z.ZodString>;
156
- entityTypes: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
157
- /** Named filter queries (render-pipeline's BuildVariantQuery) — PROJECT_CONFIG.md #3. */
158
- buildVariants: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
159
- tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
160
- dateRange: z.ZodOptional<z.ZodObject<{
161
- from: z.ZodOptional<z.ZodObject<{
162
- year: z.ZodNumber;
163
- day: z.ZodOptional<z.ZodNumber>;
164
- }, "strip", z.ZodTypeAny, {
165
- year: number;
166
- day?: number | undefined;
167
- }, {
168
- year: number;
169
- day?: number | undefined;
170
- }>>;
171
- to: z.ZodOptional<z.ZodObject<{
172
- year: z.ZodNumber;
173
- day: z.ZodOptional<z.ZodNumber>;
174
- }, "strip", z.ZodTypeAny, {
175
- year: number;
176
- day?: number | undefined;
177
- }, {
178
- year: number;
179
- day?: number | undefined;
180
- }>>;
181
- }, "strip", z.ZodTypeAny, {
182
- from?: {
183
- year: number;
184
- day?: number | undefined;
185
- } | undefined;
186
- to?: {
187
- year: number;
188
- day?: number | undefined;
189
- } | undefined;
190
- }, {
191
- from?: {
192
- year: number;
193
- day?: number | undefined;
194
- } | undefined;
195
- to?: {
196
- year: number;
197
- day?: number | undefined;
198
- } | undefined;
199
- }>>;
200
- }, "strip", z.ZodTypeAny, {
201
- tags?: string[] | undefined;
202
- dateRange?: {
203
- from?: {
204
- year: number;
205
- day?: number | undefined;
206
- } | undefined;
207
- to?: {
208
- year: number;
209
- day?: number | undefined;
210
- } | undefined;
211
- } | undefined;
212
- }, {
213
- tags?: string[] | undefined;
214
- dateRange?: {
215
- from?: {
216
- year: number;
217
- day?: number | undefined;
218
- } | undefined;
219
- to?: {
220
- year: number;
221
- day?: number | undefined;
222
- } | undefined;
223
- } | undefined;
224
- }>>>;
225
- }, z.ZodTypeAny, "passthrough">>;
22
+ }, z.core.$strip>>;
23
+ }, z.core.$strip>>;
24
+ }, z.core.$strip>>>;
25
+ }, z.core.$loose>;
226
26
  export type WorldConfig = z.infer<typeof worldConfigSchema>;
227
27
  export declare const DEFAULT_WORLD_TITLE = "World Wiki";
228
28
  /** The world's display title, falling back to a neutral default (never Quartz's own "Quartz 5"). */
@@ -4,7 +4,7 @@ import { buildVariantQuerySchema } from "../query/index.js";
4
4
  * A vault's own project-specific setup parameters (PROJECT_CONFIG.md).
5
5
  * `title`, `entityTypes` and `buildVariants` are the fields with a decided consumer
6
6
  * so far — tag vocabulary itself (as opposed to where it's used, buildVariants)
7
- * and relation-field naming are still open, so this stays passthrough rather
7
+ * and relation-field naming are still open, so this stays loose (unknown keys kept) rather
8
8
  * than locking in a shape for those prematurely.
9
9
  */
10
10
  export const worldConfigSchema = z
@@ -15,7 +15,7 @@ export const worldConfigSchema = z
15
15
  /** Named filter queries (render-pipeline's BuildVariantQuery) — PROJECT_CONFIG.md #3. */
16
16
  buildVariants: z.record(z.string(), buildVariantQuerySchema).optional(),
17
17
  })
18
- .passthrough();
18
+ .loose();
19
19
  export const DEFAULT_WORLD_TITLE = "World Wiki";
20
20
  /** The world's display title, falling back to a neutral default (never Quartz's own "Quartz 5"). */
21
21
  export function worldTitle(config) {