world-building-framework 0.2.0 → 0.2.1

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.
@@ -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;
@@ -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}`);
@@ -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);
@@ -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) {
@@ -2,54 +2,18 @@ import { z } from "zod";
2
2
  export declare const dateBoundSchema: z.ZodObject<{
3
3
  year: z.ZodNumber;
4
4
  day: z.ZodOptional<z.ZodNumber>;
5
- }, "strip", z.ZodTypeAny, {
6
- year: number;
7
- day?: number | undefined;
8
- }, {
9
- year: number;
10
- day?: number | undefined;
11
- }>;
5
+ }, z.core.$strip>;
12
6
  export type DateBound = z.infer<typeof dateBoundSchema>;
13
7
  export declare const dateRangeQuerySchema: z.ZodObject<{
14
8
  from: z.ZodOptional<z.ZodObject<{
15
9
  year: z.ZodNumber;
16
10
  day: z.ZodOptional<z.ZodNumber>;
17
- }, "strip", z.ZodTypeAny, {
18
- year: number;
19
- day?: number | undefined;
20
- }, {
21
- year: number;
22
- day?: number | undefined;
23
- }>>;
11
+ }, z.core.$strip>>;
24
12
  to: z.ZodOptional<z.ZodObject<{
25
13
  year: z.ZodNumber;
26
14
  day: z.ZodOptional<z.ZodNumber>;
27
- }, "strip", z.ZodTypeAny, {
28
- year: number;
29
- day?: number | undefined;
30
- }, {
31
- year: number;
32
- day?: number | undefined;
33
- }>>;
34
- }, "strip", z.ZodTypeAny, {
35
- from?: {
36
- year: number;
37
- day?: number | undefined;
38
- } | undefined;
39
- to?: {
40
- year: number;
41
- day?: number | undefined;
42
- } | undefined;
43
- }, {
44
- from?: {
45
- year: number;
46
- day?: number | undefined;
47
- } | undefined;
48
- to?: {
49
- year: number;
50
- day?: number | undefined;
51
- } | undefined;
52
- }>;
15
+ }, z.core.$strip>>;
16
+ }, z.core.$strip>;
53
17
  export type DateRangeQuery = z.infer<typeof dateRangeQuerySchema>;
54
18
  /** An in-world date, as stored in an event's `date` field. */
55
19
  export interface DateTuple {
@@ -6,70 +6,16 @@ import { z } from "zod";
6
6
  * its `buildVariants` entries against — one definition, no separate DSL to parse.
7
7
  */
8
8
  export declare const buildVariantQuerySchema: z.ZodObject<{
9
- tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
9
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
10
10
  dateRange: z.ZodOptional<z.ZodObject<{
11
11
  from: z.ZodOptional<z.ZodObject<{
12
12
  year: z.ZodNumber;
13
13
  day: z.ZodOptional<z.ZodNumber>;
14
- }, "strip", z.ZodTypeAny, {
15
- year: number;
16
- day?: number | undefined;
17
- }, {
18
- year: number;
19
- day?: number | undefined;
20
- }>>;
14
+ }, z.core.$strip>>;
21
15
  to: z.ZodOptional<z.ZodObject<{
22
16
  year: z.ZodNumber;
23
17
  day: z.ZodOptional<z.ZodNumber>;
24
- }, "strip", z.ZodTypeAny, {
25
- year: number;
26
- day?: number | undefined;
27
- }, {
28
- year: number;
29
- day?: number | undefined;
30
- }>>;
31
- }, "strip", z.ZodTypeAny, {
32
- from?: {
33
- year: number;
34
- day?: number | undefined;
35
- } | undefined;
36
- to?: {
37
- year: number;
38
- day?: number | undefined;
39
- } | undefined;
40
- }, {
41
- from?: {
42
- year: number;
43
- day?: number | undefined;
44
- } | undefined;
45
- to?: {
46
- year: number;
47
- day?: number | undefined;
48
- } | undefined;
49
- }>>;
50
- }, "strip", z.ZodTypeAny, {
51
- tags?: string[] | undefined;
52
- dateRange?: {
53
- from?: {
54
- year: number;
55
- day?: number | undefined;
56
- } | undefined;
57
- to?: {
58
- year: number;
59
- day?: number | undefined;
60
- } | undefined;
61
- } | undefined;
62
- }, {
63
- tags?: string[] | undefined;
64
- dateRange?: {
65
- from?: {
66
- year: number;
67
- day?: number | undefined;
68
- } | undefined;
69
- to?: {
70
- year: number;
71
- day?: number | undefined;
72
- } | undefined;
73
- } | undefined;
74
- }>;
18
+ }, z.core.$strip>>;
19
+ }, z.core.$strip>>;
20
+ }, z.core.$strip>;
75
21
  export type BuildVariantQuery = z.infer<typeof buildVariantQuerySchema>;
@@ -1,8 +1,34 @@
1
+ /** Example `wbf create` inputs, one per kind. A test validates them, so they can't drift from the schema either. */
2
+ export declare const SCHEMA_EXAMPLES: {
3
+ entity: {
4
+ frontmatter: {
5
+ type: string;
6
+ entityType: string;
7
+ title: string;
8
+ tags: string[];
9
+ home: string;
10
+ };
11
+ body: string;
12
+ };
13
+ event: {
14
+ frontmatter: {
15
+ type: string;
16
+ title: string;
17
+ date: {
18
+ year: number;
19
+ day: number;
20
+ };
21
+ after: string[];
22
+ entities: string[];
23
+ };
24
+ body: string;
25
+ };
26
+ };
1
27
  /**
2
- * Prints the live, current schema: field names come straight from the zod
3
- * schemas in fields.ts, so this can't structurally drift from what
28
+ * Prints the live, current schema: field names and value shapes come straight
29
+ * from the zod schemas in fields.ts, so this can't structurally drift from what
4
30
  * validateFrontmatter actually enforces. This is what the CLI's `schema`
5
- * command exposes so Claude always sees the real, current rules — see
31
+ * command exposes so an agent always sees the real, current rules — see
6
32
  * claude-dev/PROJECT_STRUCTURE.md.
7
33
  */
8
34
  export declare function describeSchema(entityTypeVocabulary?: string[]): string;
@@ -1,45 +1,92 @@
1
- import { commonFieldsSchema, entityFrontmatterSchema, eventFrontmatterSchema } from "./fields.js";
1
+ import { z } from "zod";
2
+ import { commonFieldsSchema, entityFrontmatterSchema, eventFrontmatterSchema, wikilinkSchema, } from "./fields.js";
2
3
  const FIELD_MEANINGS = {
3
- type: 'Top-level kind: "entity" or "event".',
4
- title: "Human-readable display name (required — filenames are UUIDs, this is the real name).",
4
+ type: "Top-level kind.",
5
+ title: "Human-readable display name (filenames are UUIDs, this is the real name).",
5
6
  tags: "Project-defined tags used for build-time filtering.",
6
7
  aliases: "Alternate names/spellings, for search recall and Obsidian autocomplete only — not used for link resolution.",
7
8
  entityType: "Category (e.g. character, location). Lint-checked against this vault's declared vocabulary, if any.",
8
9
  supersedes: "Marks this file as a new version of an existing entity — points at the file it replaces.",
9
10
  causedBy: "The event responsible for this version existing. Required alongside supersedes, and vice versa.",
10
11
  after: "Causal predecessor event(s) — the required ordering/branching backbone.",
11
- date: "Optional in-world date anchor: { year, day }.",
12
+ date: "In-world date anchor. Give both year and day.",
12
13
  entities: "The entities this event is tied to.",
13
14
  };
14
- function shapeKeys(schema) {
15
- return Object.keys(schema.shape);
15
+ /** Example `wbf create` inputs, one per kind. A test validates them, so they can't drift from the schema either. */
16
+ export const SCHEMA_EXAMPLES = {
17
+ entity: {
18
+ frontmatter: {
19
+ type: "entity",
20
+ entityType: "character",
21
+ title: "Alea Marrow",
22
+ tags: ["player"],
23
+ home: "[[66a7bd3f-a744-4565-acbe-006957625792|Port Kavas]]",
24
+ },
25
+ body: "Captain of the *Gull*.",
26
+ },
27
+ event: {
28
+ frontmatter: {
29
+ type: "event",
30
+ title: "The Great Storm",
31
+ date: { year: 150, day: 200 },
32
+ after: ["[[bb1ddebd-3a7c-4b6c-847a-b4de59a71e66|Alea takes command of the Gull]]"],
33
+ entities: ["[[adfdd8db-0bf1-4053-928f-52709232859f|Alea Marrow]]"],
34
+ },
35
+ body: "A three-day storm floods the lower docks.",
36
+ },
37
+ };
38
+ /** A schema's value shape in words, read from the zod schema itself. */
39
+ function describeShape(schema) {
40
+ if (schema === wikilinkSchema)
41
+ return "link";
42
+ if (schema instanceof z.ZodOptional)
43
+ return describeShape(schema.unwrap());
44
+ if (schema instanceof z.ZodLiteral)
45
+ return schema.values.size === 1 ? JSON.stringify([...schema.values][0]) : "literal";
46
+ if (schema instanceof z.ZodString)
47
+ return "string";
48
+ if (schema instanceof z.ZodNumber)
49
+ return schema.isInt ? "integer" : "number";
50
+ if (schema instanceof z.ZodArray)
51
+ return `list of ${describeShape(schema.element)}s`;
52
+ if (schema instanceof z.ZodObject) {
53
+ const keys = Object.entries(schema.shape).map(([key, value]) => `${key}${value instanceof z.ZodOptional ? "?" : ""}: ${describeShape(value)}`);
54
+ return `{ ${keys.join(", ")} }`;
55
+ }
56
+ return "value";
16
57
  }
17
- function describeFields(names) {
18
- return names.map((name) => ` - ${name}: ${FIELD_MEANINGS[name] ?? "(see SCHEMA.md)"}`);
58
+ function describeFields(shape, names) {
59
+ return names.map((name) => {
60
+ const schema = shape[name];
61
+ const required = schema instanceof z.ZodOptional ? "optional" : "required";
62
+ return ` - ${name} (${describeShape(schema)}, ${required}): ${FIELD_MEANINGS[name] ?? "(see SCHEMA.md)"}`;
63
+ });
19
64
  }
20
65
  /**
21
- * Prints the live, current schema: field names come straight from the zod
22
- * schemas in fields.ts, so this can't structurally drift from what
66
+ * Prints the live, current schema: field names and value shapes come straight
67
+ * from the zod schemas in fields.ts, so this can't structurally drift from what
23
68
  * validateFrontmatter actually enforces. This is what the CLI's `schema`
24
- * command exposes so Claude always sees the real, current rules — see
69
+ * command exposes so an agent always sees the real, current rules — see
25
70
  * claude-dev/PROJECT_STRUCTURE.md.
26
71
  */
27
72
  export function describeSchema(entityTypeVocabulary) {
28
- const commonKeys = shapeKeys(commonFieldsSchema);
73
+ const commonKeys = Object.keys(commonFieldsSchema.shape);
29
74
  const excluded = new Set([...commonKeys, "type"]);
30
- const entityKeys = shapeKeys(entityFrontmatterSchema).filter((key) => !excluded.has(key));
31
- const eventKeys = shapeKeys(eventFrontmatterSchema).filter((key) => !excluded.has(key));
75
+ const entityKeys = Object.keys(entityFrontmatterSchema.shape).filter((key) => !excluded.has(key));
76
+ const eventKeys = Object.keys(eventFrontmatterSchema.shape).filter((key) => !excluded.has(key));
32
77
  const lines = [
33
78
  "Common fields (every file):",
34
- ...describeFields(["type", ...commonKeys]),
79
+ ` - type ("entity" or "event", required): ${FIELD_MEANINGS.type}`,
80
+ ...describeFields(commonFieldsSchema.shape, commonKeys),
35
81
  "",
36
82
  "Entity-only fields:",
37
- ...describeFields(entityKeys),
83
+ ...describeFields(entityFrontmatterSchema.shape, entityKeys),
38
84
  "",
39
85
  "Event-only fields:",
40
- ...describeFields(eventKeys),
86
+ ...describeFields(eventFrontmatterSchema.shape, eventKeys),
41
87
  "",
42
- 'Any other frontmatter field whose value is a wikilink ("[[uuid|Display Name]]") is automatically a relation.',
88
+ 'A link is a string "[[uuid|Display Name]]". A list field takes a list even for one link: after: ["[[uuid|Name]]"].',
89
+ "Any other frontmatter field whose value is a link is automatically a relation.",
43
90
  ];
44
91
  if (entityTypeVocabulary && entityTypeVocabulary.length > 0) {
45
92
  lines.push("", `This vault's declared entityType vocabulary: ${entityTypeVocabulary.join(", ")}`);
@@ -47,5 +94,6 @@ export function describeSchema(entityTypeVocabulary) {
47
94
  else {
48
95
  lines.push("", "This vault has not declared an entityType vocabulary — any entityType value is accepted.");
49
96
  }
97
+ lines.push("", "Example input for `wbf create` (replace the UUIDs with real ids, e.g. from `wbf search`):", ` entity: ${JSON.stringify(SCHEMA_EXAMPLES.entity)}`, ` event: ${JSON.stringify(SCHEMA_EXAMPLES.event)}`);
50
98
  return lines.join("\n");
51
99
  }
@@ -1,249 +1,57 @@
1
1
  import { z } from "zod";
2
2
  /** Any field whose value looks like this is auto-detected as a graph edge (SCHEMA.md rule 2) — no fixed vocabulary of relation-field names. */
3
- export declare const wikilinkSchema: z.ZodEffects<z.ZodString, string, string>;
3
+ export declare const wikilinkSchema: z.ZodString & z.ZodType<string, string, z.core.$ZodTypeInternals<string, string>>;
4
4
  export declare const inWorldDateSchema: z.ZodObject<{
5
5
  year: z.ZodNumber;
6
6
  day: z.ZodNumber;
7
- }, "strip", z.ZodTypeAny, {
8
- year: number;
9
- day: number;
10
- }, {
11
- year: number;
12
- day: number;
13
- }>;
7
+ }, z.core.$strip>;
14
8
  /** Fields every knowledge file carries (SCHEMA.md "Common frontmatter"). */
15
9
  export declare const commonFieldsSchema: z.ZodObject<{
16
10
  title: z.ZodString;
17
- tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
18
- aliases: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
19
- }, "strip", z.ZodTypeAny, {
20
- title: string;
21
- tags?: string[] | undefined;
22
- aliases?: string[] | undefined;
23
- }, {
24
- title: string;
25
- tags?: string[] | undefined;
26
- aliases?: string[] | undefined;
27
- }>;
11
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
12
+ aliases: z.ZodOptional<z.ZodArray<z.ZodString>>;
13
+ }, z.core.$strip>;
28
14
  export declare const entityFrontmatterSchema: z.ZodObject<{
29
15
  title: z.ZodString;
30
- tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
31
- aliases: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
32
- } & {
16
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
17
+ aliases: z.ZodOptional<z.ZodArray<z.ZodString>>;
33
18
  type: z.ZodLiteral<"entity">;
34
19
  entityType: z.ZodOptional<z.ZodString>;
35
- supersedes: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
36
- causedBy: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
37
- }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
38
- title: z.ZodString;
39
- tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
40
- aliases: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
41
- } & {
42
- type: z.ZodLiteral<"entity">;
43
- entityType: z.ZodOptional<z.ZodString>;
44
- supersedes: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
45
- causedBy: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
46
- }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
47
- title: z.ZodString;
48
- tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
49
- aliases: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
50
- } & {
51
- type: z.ZodLiteral<"entity">;
52
- entityType: z.ZodOptional<z.ZodString>;
53
- supersedes: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
54
- causedBy: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
55
- }, z.ZodTypeAny, "passthrough">>;
20
+ supersedes: z.ZodOptional<z.ZodString & z.ZodType<string, string, z.core.$ZodTypeInternals<string, string>>>;
21
+ causedBy: z.ZodOptional<z.ZodString & z.ZodType<string, string, z.core.$ZodTypeInternals<string, string>>>;
22
+ }, z.core.$loose>;
56
23
  export declare const eventFrontmatterSchema: z.ZodObject<{
57
24
  title: z.ZodString;
58
- tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
59
- aliases: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
60
- } & {
61
- type: z.ZodLiteral<"event">;
62
- after: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
63
- date: z.ZodOptional<z.ZodObject<{
64
- year: z.ZodNumber;
65
- day: z.ZodNumber;
66
- }, "strip", z.ZodTypeAny, {
67
- year: number;
68
- day: number;
69
- }, {
70
- year: number;
71
- day: number;
72
- }>>;
73
- entities: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
74
- }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
75
- title: z.ZodString;
76
- tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
77
- aliases: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
78
- } & {
79
- type: z.ZodLiteral<"event">;
80
- after: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
81
- date: z.ZodOptional<z.ZodObject<{
82
- year: z.ZodNumber;
83
- day: z.ZodNumber;
84
- }, "strip", z.ZodTypeAny, {
85
- year: number;
86
- day: number;
87
- }, {
88
- year: number;
89
- day: number;
90
- }>>;
91
- entities: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
92
- }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
93
- title: z.ZodString;
94
- tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
95
- aliases: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
96
- } & {
97
- type: z.ZodLiteral<"event">;
98
- after: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
99
- date: z.ZodOptional<z.ZodObject<{
100
- year: z.ZodNumber;
101
- day: z.ZodNumber;
102
- }, "strip", z.ZodTypeAny, {
103
- year: number;
104
- day: number;
105
- }, {
106
- year: number;
107
- day: number;
108
- }>>;
109
- entities: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
110
- }, z.ZodTypeAny, "passthrough">>;
111
- export declare const knowledgeFileFrontmatterSchema: z.ZodEffects<z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
112
- title: z.ZodString;
113
- tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
114
- aliases: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
115
- } & {
116
- type: z.ZodLiteral<"entity">;
117
- entityType: z.ZodOptional<z.ZodString>;
118
- supersedes: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
119
- causedBy: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
120
- }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
121
- title: z.ZodString;
122
- tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
123
- aliases: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
124
- } & {
125
- type: z.ZodLiteral<"entity">;
126
- entityType: z.ZodOptional<z.ZodString>;
127
- supersedes: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
128
- causedBy: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
129
- }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
130
- title: z.ZodString;
131
- tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
132
- aliases: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
133
- } & {
134
- type: z.ZodLiteral<"entity">;
135
- entityType: z.ZodOptional<z.ZodString>;
136
- supersedes: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
137
- causedBy: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
138
- }, z.ZodTypeAny, "passthrough">>, z.ZodObject<{
139
- title: z.ZodString;
140
- tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
141
- aliases: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
142
- } & {
143
- type: z.ZodLiteral<"event">;
144
- after: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
145
- date: z.ZodOptional<z.ZodObject<{
146
- year: z.ZodNumber;
147
- day: z.ZodNumber;
148
- }, "strip", z.ZodTypeAny, {
149
- year: number;
150
- day: number;
151
- }, {
152
- year: number;
153
- day: number;
154
- }>>;
155
- entities: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
156
- }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
157
- title: z.ZodString;
158
- tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
159
- aliases: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
160
- } & {
161
- type: z.ZodLiteral<"event">;
162
- after: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
163
- date: z.ZodOptional<z.ZodObject<{
164
- year: z.ZodNumber;
165
- day: z.ZodNumber;
166
- }, "strip", z.ZodTypeAny, {
167
- year: number;
168
- day: number;
169
- }, {
170
- year: number;
171
- day: number;
172
- }>>;
173
- entities: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
174
- }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
175
- title: z.ZodString;
176
- tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
177
- aliases: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
178
- } & {
179
- type: z.ZodLiteral<"event">;
180
- after: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
181
- date: z.ZodOptional<z.ZodObject<{
182
- year: z.ZodNumber;
183
- day: z.ZodNumber;
184
- }, "strip", z.ZodTypeAny, {
185
- year: number;
186
- day: number;
187
- }, {
188
- year: number;
189
- day: number;
190
- }>>;
191
- entities: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
192
- }, z.ZodTypeAny, "passthrough">>]>, z.objectOutputType<{
193
- title: z.ZodString;
194
- tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
195
- aliases: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
196
- } & {
197
- type: z.ZodLiteral<"entity">;
198
- entityType: z.ZodOptional<z.ZodString>;
199
- supersedes: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
200
- causedBy: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
201
- }, z.ZodTypeAny, "passthrough"> | z.objectOutputType<{
202
- title: z.ZodString;
203
- tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
204
- aliases: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
205
- } & {
25
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
26
+ aliases: z.ZodOptional<z.ZodArray<z.ZodString>>;
206
27
  type: z.ZodLiteral<"event">;
207
- after: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
28
+ after: z.ZodOptional<z.ZodArray<z.ZodString & z.ZodType<string, string, z.core.$ZodTypeInternals<string, string>>>>;
208
29
  date: z.ZodOptional<z.ZodObject<{
209
30
  year: z.ZodNumber;
210
31
  day: z.ZodNumber;
211
- }, "strip", z.ZodTypeAny, {
212
- year: number;
213
- day: number;
214
- }, {
215
- year: number;
216
- day: number;
217
- }>>;
218
- entities: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
219
- }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
32
+ }, z.core.$strip>>;
33
+ entities: z.ZodOptional<z.ZodArray<z.ZodString & z.ZodType<string, string, z.core.$ZodTypeInternals<string, string>>>>;
34
+ }, z.core.$loose>;
35
+ export declare const knowledgeFileFrontmatterSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
220
36
  title: z.ZodString;
221
- tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
222
- aliases: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
223
- } & {
37
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
38
+ aliases: z.ZodOptional<z.ZodArray<z.ZodString>>;
224
39
  type: z.ZodLiteral<"entity">;
225
40
  entityType: z.ZodOptional<z.ZodString>;
226
- supersedes: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
227
- causedBy: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
228
- }, z.ZodTypeAny, "passthrough"> | z.objectInputType<{
41
+ supersedes: z.ZodOptional<z.ZodString & z.ZodType<string, string, z.core.$ZodTypeInternals<string, string>>>;
42
+ causedBy: z.ZodOptional<z.ZodString & z.ZodType<string, string, z.core.$ZodTypeInternals<string, string>>>;
43
+ }, z.core.$loose>, z.ZodObject<{
229
44
  title: z.ZodString;
230
- tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
231
- aliases: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
232
- } & {
45
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
46
+ aliases: z.ZodOptional<z.ZodArray<z.ZodString>>;
233
47
  type: z.ZodLiteral<"event">;
234
- after: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
48
+ after: z.ZodOptional<z.ZodArray<z.ZodString & z.ZodType<string, string, z.core.$ZodTypeInternals<string, string>>>>;
235
49
  date: z.ZodOptional<z.ZodObject<{
236
50
  year: z.ZodNumber;
237
51
  day: z.ZodNumber;
238
- }, "strip", z.ZodTypeAny, {
239
- year: number;
240
- day: number;
241
- }, {
242
- year: number;
243
- day: number;
244
- }>>;
245
- entities: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
246
- }, z.ZodTypeAny, "passthrough">>;
52
+ }, z.core.$strip>>;
53
+ entities: z.ZodOptional<z.ZodArray<z.ZodString & z.ZodType<string, string, z.core.$ZodTypeInternals<string, string>>>>;
54
+ }, z.core.$loose>], "type">;
247
55
  export type EntityFrontmatter = z.infer<typeof entityFrontmatterSchema>;
248
56
  export type EventFrontmatter = z.infer<typeof eventFrontmatterSchema>;
249
57
  export type KnowledgeFileFrontmatter = z.infer<typeof knowledgeFileFrontmatterSchema>;
@@ -21,7 +21,7 @@ export const entityFrontmatterSchema = commonFieldsSchema
21
21
  supersedes: wikilinkSchema.optional(),
22
22
  causedBy: wikilinkSchema.optional(),
23
23
  })
24
- .passthrough();
24
+ .loose();
25
25
  export const eventFrontmatterSchema = commonFieldsSchema
26
26
  .extend({
27
27
  type: z.literal("event"),
@@ -29,13 +29,13 @@ export const eventFrontmatterSchema = commonFieldsSchema
29
29
  date: inWorldDateSchema.optional(),
30
30
  entities: z.array(wikilinkSchema).optional(),
31
31
  })
32
- .passthrough();
32
+ .loose();
33
33
  export const knowledgeFileFrontmatterSchema = z
34
34
  .discriminatedUnion("type", [entityFrontmatterSchema, eventFrontmatterSchema])
35
35
  .superRefine((data, ctx) => {
36
36
  if (data.type === "entity" && Boolean(data.supersedes) !== Boolean(data.causedBy)) {
37
37
  ctx.addIssue({
38
- code: z.ZodIssueCode.custom,
38
+ code: "custom",
39
39
  message: '"supersedes" and "causedBy" must both be present, or both absent',
40
40
  path: ["causedBy"],
41
41
  });
@@ -4,6 +4,9 @@ import type { BuildVariantQuery, RenderedPage } from "./types.js";
4
4
  * Stage 1: vault -> derived, Quartz-ready page set (RENDER_PIPELINE.md). Filters
5
5
  * by the build variant's query, merges each included entity's version history
6
6
  * into one page, and rewrites every link to a slug (or strips it if the target
7
- * isn't included).
7
+ * isn't included). Adds a start page listing everything included, titled
8
+ * `options.title` (the world's title).
8
9
  */
9
- export declare function buildDerivedTree(files: VaultFile[], query: BuildVariantQuery): RenderedPage[];
10
+ export declare function buildDerivedTree(files: VaultFile[], query: BuildVariantQuery, options?: {
11
+ title?: string;
12
+ }): RenderedPage[];
@@ -1,9 +1,10 @@
1
- import { buildGraph } from "../core/graph/index.js";
1
+ import { buildGraph, topologicalOrder } from "../core/graph/index.js";
2
2
  import { filterEvents, filterEntityAnchors } from "./filter.js";
3
3
  import { mergeEntityBody } from "./merge.js";
4
4
  import { assignSlugs } from "./slugs.js";
5
5
  import { rewriteLinks, rewriteFrontmatterLinks } from "./rewrite.js";
6
6
  import { renameQuartzReservedFields } from "./quartz-compat.js";
7
+ import { buildStartPage, START_PAGE_SLUG } from "./start-page.js";
7
8
  function titleOf(file) {
8
9
  return typeof file.frontmatter.title === "string" ? file.frontmatter.title : file.id;
9
10
  }
@@ -11,20 +12,22 @@ function titleOf(file) {
11
12
  * Stage 1: vault -> derived, Quartz-ready page set (RENDER_PIPELINE.md). Filters
12
13
  * by the build variant's query, merges each included entity's version history
13
14
  * into one page, and rewrites every link to a slug (or strips it if the target
14
- * isn't included).
15
+ * isn't included). Adds a start page listing everything included, titled
16
+ * `options.title` (the world's title).
15
17
  */
16
- export function buildDerivedTree(files, query) {
18
+ export function buildDerivedTree(files, query, options = {}) {
17
19
  const graph = buildGraph(files);
18
20
  const includedEventIds = filterEvents(graph, files, query);
19
21
  const includedAnchorIds = filterEntityAnchors(graph, files, query);
20
22
  // Sorted for a deterministic slug assignment, independent of vault read order.
21
23
  const includedIds = [...includedEventIds, ...includedAnchorIds].sort();
22
24
  const titles = new Map(includedIds.map((id) => [id, titleOf(graph.nodes.get(id))]));
23
- const slugsById = assignSlugs(titles);
24
- const pages = [];
25
- for (const id of includedEventIds) {
25
+ const slugsById = assignSlugs(titles, [START_PAGE_SLUG]);
26
+ const eventPages = [];
27
+ const entityPages = [];
28
+ for (const id of topologicalOrder(graph).filter((eventId) => includedEventIds.has(eventId))) {
26
29
  const file = graph.nodes.get(id);
27
- pages.push({
30
+ eventPages.push({
28
31
  id,
29
32
  slug: slugsById.get(id),
30
33
  frontmatter: renameQuartzReservedFields(rewriteFrontmatterLinks(file.frontmatter, slugsById)),
@@ -34,12 +37,13 @@ export function buildDerivedTree(files, query) {
34
37
  for (const anchorId of includedAnchorIds) {
35
38
  const anchor = graph.nodes.get(anchorId);
36
39
  const mergedBody = mergeEntityBody(graph, anchorId, includedEventIds);
37
- pages.push({
40
+ entityPages.push({
38
41
  id: anchorId,
39
42
  slug: slugsById.get(anchorId),
40
43
  frontmatter: renameQuartzReservedFields(rewriteFrontmatterLinks(anchor.frontmatter, slugsById)),
41
44
  body: rewriteLinks(mergedBody, slugsById),
42
45
  });
43
46
  }
44
- return pages;
47
+ const startPage = buildStartPage(options.title ?? "World Wiki", entityPages, eventPages);
48
+ return [...eventPages, ...entityPages, startPage];
45
49
  }
@@ -5,6 +5,7 @@ export * from "./merge.js";
5
5
  export * from "./slugs.js";
6
6
  export * from "./rewrite.js";
7
7
  export * from "./quartz-compat.js";
8
+ export * from "./start-page.js";
8
9
  export * from "./build.js";
9
10
  export * from "./write.js";
10
11
  export * from "./sync.js";
@@ -5,6 +5,7 @@ export * from "./merge.js";
5
5
  export * from "./slugs.js";
6
6
  export * from "./rewrite.js";
7
7
  export * from "./quartz-compat.js";
8
+ export * from "./start-page.js";
8
9
  export * from "./build.js";
9
10
  export * from "./write.js";
10
11
  export * from "./sync.js";
@@ -2,6 +2,7 @@
2
2
  * Assigns a unique, readable slug per id — vault files are UUID-named (SCHEMA.md),
3
3
  * meaningless as a published URL; this is what Stage 1 uses instead for the
4
4
  * derived tree. Iterates `titles` in whatever order the caller provides, so pass
5
- * ids in a deterministic order for reproducible builds.
5
+ * ids in a deterministic order for reproducible builds. No page gets a slug
6
+ * in `reserved` (e.g. the start page's "index").
6
7
  */
7
- export declare function assignSlugs(titles: Map<string, string>): Map<string, string>;
8
+ export declare function assignSlugs(titles: Map<string, string>, reserved?: string[]): Map<string, string>;
@@ -10,10 +10,11 @@ function slugify(title) {
10
10
  * Assigns a unique, readable slug per id — vault files are UUID-named (SCHEMA.md),
11
11
  * meaningless as a published URL; this is what Stage 1 uses instead for the
12
12
  * derived tree. Iterates `titles` in whatever order the caller provides, so pass
13
- * ids in a deterministic order for reproducible builds.
13
+ * ids in a deterministic order for reproducible builds. No page gets a slug
14
+ * in `reserved` (e.g. the start page's "index").
14
15
  */
15
- export function assignSlugs(titles) {
16
- const used = new Set();
16
+ export function assignSlugs(titles, reserved = []) {
17
+ const used = new Set(reserved);
17
18
  const slugs = new Map();
18
19
  for (const [id, title] of titles) {
19
20
  const base = slugify(title);
@@ -0,0 +1,9 @@
1
+ import type { RenderedPage } from "./types.js";
2
+ /** Quartz builds a site's root page from `content/index.md`; without it, the wiki's own address is a 404. */
3
+ export declare const START_PAGE_SLUG = "index";
4
+ /**
5
+ * The wiki's start page: every included entity, grouped by entityType and sorted
6
+ * by title, then every included event in causal order. `events` must already be
7
+ * in that order; only pages of this build go in, so nothing filtered out shows up.
8
+ */
9
+ export declare function buildStartPage(title: string, entities: RenderedPage[], events: RenderedPage[]): RenderedPage;
@@ -0,0 +1,44 @@
1
+ /** Quartz builds a site's root page from `content/index.md`; without it, the wiki's own address is a 404. */
2
+ export const START_PAGE_SLUG = "index";
3
+ function titleOf(page) {
4
+ return typeof page.frontmatter.title === "string" ? page.frontmatter.title : page.slug;
5
+ }
6
+ function linkTo(page) {
7
+ return `[[${page.slug}|${titleOf(page)}]]`;
8
+ }
9
+ function describeDate(page) {
10
+ const date = page.frontmatter.worldDate;
11
+ if (typeof date?.year !== "number")
12
+ return "";
13
+ return typeof date.day === "number" ? ` (year ${date.year}, day ${date.day})` : ` (year ${date.year})`;
14
+ }
15
+ function heading(entityType) {
16
+ return entityType.charAt(0).toUpperCase() + entityType.slice(1);
17
+ }
18
+ /**
19
+ * The wiki's start page: every included entity, grouped by entityType and sorted
20
+ * by title, then every included event in causal order. `events` must already be
21
+ * in that order; only pages of this build go in, so nothing filtered out shows up.
22
+ */
23
+ export function buildStartPage(title, entities, events) {
24
+ const groups = new Map();
25
+ for (const page of entities) {
26
+ const entityType = typeof page.frontmatter.entityType === "string" ? page.frontmatter.entityType : "";
27
+ groups.set(entityType, [...(groups.get(entityType) ?? []), page]);
28
+ }
29
+ // Typed groups alphabetically, untyped entities last.
30
+ const groupKeys = [...groups.keys()].sort((a, b) => (a === "" ? 1 : b === "" ? -1 : a.localeCompare(b)));
31
+ const sections = groupKeys.map((key) => {
32
+ const pages = groups.get(key).sort((a, b) => titleOf(a).localeCompare(titleOf(b)));
33
+ return `## ${key === "" ? "Other" : heading(key)}\n\n${pages.map((page) => `- ${linkTo(page)}`).join("\n")}`;
34
+ });
35
+ if (events.length > 0) {
36
+ sections.push(`## Events\n\n${events.map((page) => `- ${linkTo(page)}${describeDate(page)}`).join("\n")}`);
37
+ }
38
+ return {
39
+ id: START_PAGE_SLUG,
40
+ slug: START_PAGE_SLUG,
41
+ frontmatter: { title },
42
+ body: sections.length > 0 ? `${sections.join("\n\n")}\n` : "Nothing here yet.\n",
43
+ };
44
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "world-building-framework",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Build, search and publish fictional worlds from a vault of Markdown files: the wbf CLI and its core engine.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -36,7 +36,7 @@
36
36
  "commander": "^15.0.0",
37
37
  "gray-matter": "^4.0.3",
38
38
  "uuid": "^14.0.2",
39
- "zod": "^3.23.8"
39
+ "zod": "^4.6.7"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/better-sqlite3": "^9.6.0",