world-building-framework 0.0.0-stage → 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.
- package/LICENSE +9 -0
- package/README.md +104 -2
- package/dist/cli/check.d.ts +13 -0
- package/dist/cli/check.js +11 -0
- package/dist/cli/commands/create.d.ts +20 -0
- package/dist/cli/commands/create.js +35 -0
- package/dist/cli/commands/init.d.ts +26 -0
- package/dist/cli/commands/init.js +119 -0
- package/dist/cli/commands/list-variants.d.ts +1 -0
- package/dist/cli/commands/list-variants.js +7 -0
- package/dist/cli/commands/publish.d.ts +24 -0
- package/dist/cli/commands/publish.js +71 -0
- package/dist/cli/commands/refresh.d.ts +29 -0
- package/dist/cli/commands/refresh.js +70 -0
- package/dist/cli/commands/render.d.ts +14 -0
- package/dist/cli/commands/render.js +27 -0
- package/dist/cli/commands/schema.d.ts +2 -0
- package/dist/cli/commands/schema.js +9 -0
- package/dist/cli/commands/search.d.ts +9 -0
- package/dist/cli/commands/search.js +20 -0
- package/dist/cli/commands/update.d.ts +23 -0
- package/dist/cli/commands/update.js +69 -0
- package/dist/cli/commands/validate.d.ts +21 -0
- package/dist/cli/commands/validate.js +44 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +157 -0
- package/dist/cli/io.d.ts +4 -0
- package/dist/cli/io.js +25 -0
- package/dist/cli/managed-files.d.ts +35 -0
- package/dist/cli/managed-files.js +80 -0
- package/dist/cli/quartz-runner.d.ts +22 -0
- package/dist/cli/quartz-runner.js +76 -0
- package/dist/cli/quartz-setup.d.ts +10 -0
- package/dist/cli/quartz-setup.js +59 -0
- package/dist/cli/vault-path.d.ts +7 -0
- package/dist/cli/vault-path.js +44 -0
- package/dist/core/config/build-variants.d.ts +5 -0
- package/dist/core/config/build-variants.js +13 -0
- package/dist/core/config/index.d.ts +4 -0
- package/dist/core/config/index.js +4 -0
- package/dist/core/config/load.d.ts +4 -0
- package/dist/core/config/load.js +25 -0
- package/dist/core/config/schema.d.ts +29 -0
- package/dist/core/config/schema.js +23 -0
- package/dist/core/config/validate-options.d.ts +4 -0
- package/dist/core/config/validate-options.js +4 -0
- package/dist/core/graph/build.d.ts +8 -0
- package/dist/core/graph/build.js +42 -0
- package/dist/core/graph/causal.d.ts +22 -0
- package/dist/core/graph/causal.js +106 -0
- package/dist/core/graph/dating.d.ts +18 -0
- package/dist/core/graph/dating.js +30 -0
- package/dist/core/graph/entities.d.ts +13 -0
- package/dist/core/graph/entities.js +22 -0
- package/dist/core/graph/index.d.ts +7 -0
- package/dist/core/graph/index.js +7 -0
- package/dist/core/graph/queries.d.ts +3 -0
- package/dist/core/graph/queries.js +8 -0
- package/dist/core/graph/types.d.ts +14 -0
- package/dist/core/graph/types.js +1 -0
- package/dist/core/graph/versioning.d.ts +15 -0
- package/dist/core/graph/versioning.js +50 -0
- package/dist/core/id.d.ts +3 -0
- package/dist/core/id.js +9 -0
- package/dist/core/lint/after-loops.d.ts +15 -0
- package/dist/core/lint/after-loops.js +34 -0
- package/dist/core/lint/dangling-links.d.ts +9 -0
- package/dist/core/lint/dangling-links.js +46 -0
- package/dist/core/lint/events-without-entities.d.ts +4 -0
- package/dist/core/lint/events-without-entities.js +9 -0
- package/dist/core/lint/forked-versions.d.ts +14 -0
- package/dist/core/lint/forked-versions.js +29 -0
- package/dist/core/lint/index.d.ts +6 -0
- package/dist/core/lint/index.js +6 -0
- package/dist/core/lint/lint.d.ts +24 -0
- package/dist/core/lint/lint.js +50 -0
- package/dist/core/lint/target-types.d.ts +9 -0
- package/dist/core/lint/target-types.js +47 -0
- package/dist/core/query/dates.d.ts +34 -0
- package/dist/core/query/dates.js +43 -0
- package/dist/core/query/index.d.ts +2 -0
- package/dist/core/query/index.js +2 -0
- package/dist/core/query/types.d.ts +21 -0
- package/dist/core/query/types.js +12 -0
- package/dist/core/schema/describe.d.ts +34 -0
- package/dist/core/schema/describe.js +99 -0
- package/dist/core/schema/fields.d.ts +57 -0
- package/dist/core/schema/fields.js +43 -0
- package/dist/core/schema/index.d.ts +4 -0
- package/dist/core/schema/index.js +4 -0
- package/dist/core/schema/validate.d.ts +22 -0
- package/dist/core/schema/validate.js +34 -0
- package/dist/core/schema/wikilink.d.ts +8 -0
- package/dist/core/schema/wikilink.js +23 -0
- package/dist/core/search/build.d.ts +11 -0
- package/dist/core/search/build.js +26 -0
- package/dist/core/search/columns.d.ts +9 -0
- package/dist/core/search/columns.js +14 -0
- package/dist/core/search/index.d.ts +3 -0
- package/dist/core/search/index.js +3 -0
- package/dist/core/search/search.d.ts +3 -0
- package/dist/core/search/search.js +35 -0
- package/dist/core/search/types.d.ts +18 -0
- package/dist/core/search/types.js +1 -0
- package/dist/core/vault/index.d.ts +6 -0
- package/dist/core/vault/index.js +6 -0
- package/dist/core/vault/parse.d.ts +8 -0
- package/dist/core/vault/parse.js +11 -0
- package/dist/core/vault/read.d.ts +13 -0
- package/dist/core/vault/read.js +62 -0
- package/dist/core/vault/relink.d.ts +10 -0
- package/dist/core/vault/relink.js +33 -0
- package/dist/core/vault/serialize.d.ts +4 -0
- package/dist/core/vault/serialize.js +4 -0
- package/dist/core/vault/types.d.ts +5 -0
- package/dist/core/vault/types.js +1 -0
- package/dist/core/vault/write.d.ts +18 -0
- package/dist/core/vault/write.js +37 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +11 -0
- package/dist/render-pipeline/build.d.ts +12 -0
- package/dist/render-pipeline/build.js +49 -0
- package/dist/render-pipeline/filter.d.ts +21 -0
- package/dist/render-pipeline/filter.js +80 -0
- package/dist/render-pipeline/index.d.ts +11 -0
- package/dist/render-pipeline/index.js +11 -0
- package/dist/render-pipeline/merge.d.ts +11 -0
- package/dist/render-pipeline/merge.js +21 -0
- package/dist/render-pipeline/quartz-compat.d.ts +1 -0
- package/dist/render-pipeline/quartz-compat.js +18 -0
- package/dist/render-pipeline/rewrite.d.ts +8 -0
- package/dist/render-pipeline/rewrite.js +23 -0
- package/dist/render-pipeline/slugs.d.ts +8 -0
- package/dist/render-pipeline/slugs.js +31 -0
- package/dist/render-pipeline/start-page.d.ts +9 -0
- package/dist/render-pipeline/start-page.js +44 -0
- package/dist/render-pipeline/sync.d.ts +9 -0
- package/dist/render-pipeline/sync.js +18 -0
- package/dist/render-pipeline/types.d.ts +8 -0
- package/dist/render-pipeline/types.js +1 -0
- package/dist/render-pipeline/write.d.ts +3 -0
- package/dist/render-pipeline/write.js +12 -0
- package/package.json +46 -4
- package/skills/world-vault/SKILL.md +100 -0
- package/templates/AGENTS.md +11 -0
- package/templates/gitignore +5 -0
- package/templates/publish.yml +54 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { isWikilink, parseWikilink } from "../schema/wikilink.js";
|
|
2
|
+
function extractFrontmatterLinks(value, path, out) {
|
|
3
|
+
if (typeof value === "string" && isWikilink(value)) {
|
|
4
|
+
out.push({ path, targetId: parseWikilink(value).targetId });
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
if (Array.isArray(value)) {
|
|
8
|
+
value.forEach((entry, i) => extractFrontmatterLinks(entry, `${path}[${i}]`, out));
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const BODY_WIKILINK_RE = /\[\[([0-9a-f-]{36})\|[^\]]*\]\]/gi;
|
|
12
|
+
function extractBodyLinkTargets(body) {
|
|
13
|
+
return [...body.matchAll(BODY_WIKILINK_RE)].map((match) => match[1]);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Finds wikilinks — frontmatter or body — pointing at an id that doesn't exist
|
|
17
|
+
* anywhere in the vault. Checks body text too, not just frontmatter relations:
|
|
18
|
+
* a broken link is still bad to read even though body mentions don't count as
|
|
19
|
+
* relation edges (REQUIREMENTS.md's explicit-only relatedness rule).
|
|
20
|
+
*/
|
|
21
|
+
export function findDanglingLinks(file, knownIds) {
|
|
22
|
+
const issues = [];
|
|
23
|
+
const frontmatterLinks = [];
|
|
24
|
+
for (const [key, value] of Object.entries(file.frontmatter)) {
|
|
25
|
+
extractFrontmatterLinks(value, key, frontmatterLinks);
|
|
26
|
+
}
|
|
27
|
+
for (const link of frontmatterLinks) {
|
|
28
|
+
if (!knownIds.has(link.targetId)) {
|
|
29
|
+
issues.push({
|
|
30
|
+
path: link.path,
|
|
31
|
+
message: `Dangling link: no file with id "${link.targetId}" exists in the vault`,
|
|
32
|
+
severity: "error",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
for (const targetId of extractBodyLinkTargets(file.body)) {
|
|
37
|
+
if (!knownIds.has(targetId)) {
|
|
38
|
+
issues.push({
|
|
39
|
+
path: "body",
|
|
40
|
+
message: `Dangling link: no file with id "${targetId}" exists in the vault`,
|
|
41
|
+
severity: "error",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return issues;
|
|
46
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { VaultFile } from "../vault/types.js";
|
|
2
|
+
import type { ValidationIssue } from "../schema/validate.js";
|
|
3
|
+
/** Flags an event with no `entities` — SCHEMA.md's own example of a plausible lint rule. A warning, not an error: an event may be written before its entities exist. */
|
|
4
|
+
export declare function findEventsWithoutEntities(file: Pick<VaultFile, "frontmatter">): ValidationIssue[];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Flags an event with no `entities` — SCHEMA.md's own example of a plausible lint rule. A warning, not an error: an event may be written before its entities exist. */
|
|
2
|
+
export function findEventsWithoutEntities(file) {
|
|
3
|
+
if (file.frontmatter.type !== "event")
|
|
4
|
+
return [];
|
|
5
|
+
const entities = file.frontmatter.entities;
|
|
6
|
+
if (Array.isArray(entities) && entities.length > 0)
|
|
7
|
+
return [];
|
|
8
|
+
return [{ path: "entities", message: "Event has no entities — consider tying it to at least one", severity: "warning" }];
|
|
9
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { VaultFile } from "../vault/types.js";
|
|
2
|
+
import type { ValidationIssue } from "../schema/validate.js";
|
|
3
|
+
/** The id a file's `supersedes` points at, if it's a valid wikilink. */
|
|
4
|
+
export declare function supersedesTarget(frontmatter: Record<string, unknown>): string | undefined;
|
|
5
|
+
/**
|
|
6
|
+
* Flags a version that supersedes a file some other version already supersedes —
|
|
7
|
+
* a fork in the entity's version chain. A warning, not an error: the data model
|
|
8
|
+
* allows story branches (GD-1), but the v1 wiki renders a single story, so a fork
|
|
9
|
+
* is far more often a version pointed at an older predecessor than at the latest.
|
|
10
|
+
* `supersededBy` maps each file id to the ids of the vault files superseding it.
|
|
11
|
+
*/
|
|
12
|
+
export declare function findForkedVersions(file: Pick<VaultFile, "frontmatter"> & {
|
|
13
|
+
id?: string;
|
|
14
|
+
}, supersededBy: Map<string, string[]>): ValidationIssue[];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { isWikilink, parseWikilink } from "../schema/wikilink.js";
|
|
2
|
+
/** The id a file's `supersedes` points at, if it's a valid wikilink. */
|
|
3
|
+
export function supersedesTarget(frontmatter) {
|
|
4
|
+
const value = frontmatter.supersedes;
|
|
5
|
+
return isWikilink(value) ? parseWikilink(value).targetId : undefined;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Flags a version that supersedes a file some other version already supersedes —
|
|
9
|
+
* a fork in the entity's version chain. A warning, not an error: the data model
|
|
10
|
+
* allows story branches (GD-1), but the v1 wiki renders a single story, so a fork
|
|
11
|
+
* is far more often a version pointed at an older predecessor than at the latest.
|
|
12
|
+
* `supersededBy` maps each file id to the ids of the vault files superseding it.
|
|
13
|
+
*/
|
|
14
|
+
export function findForkedVersions(file, supersededBy) {
|
|
15
|
+
const target = supersedesTarget(file.frontmatter);
|
|
16
|
+
if (!target)
|
|
17
|
+
return [];
|
|
18
|
+
const others = (supersededBy.get(target) ?? []).filter((id) => id !== file.id);
|
|
19
|
+
if (others.length === 0)
|
|
20
|
+
return [];
|
|
21
|
+
return [
|
|
22
|
+
{
|
|
23
|
+
path: "supersedes",
|
|
24
|
+
message: `Fork in the version chain: ${others.join(", ")} also supersede${others.length === 1 ? "s" : ""} ${target}. ` +
|
|
25
|
+
"A new version should normally supersede the entity's latest version.",
|
|
26
|
+
severity: "warning",
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { VaultFile } from "../vault/types.js";
|
|
2
|
+
import type { ValidationIssue } from "../schema/validate.js";
|
|
3
|
+
/** Whole-vault facts some lint rules need. Build once per vault snapshot with createLintContext. */
|
|
4
|
+
export interface LintContext {
|
|
5
|
+
knownIds: Set<string>;
|
|
6
|
+
/** File id -> ids of the files whose `supersedes` points at it. */
|
|
7
|
+
supersededBy: Map<string, string[]>;
|
|
8
|
+
/** Event id -> the ids its `after` points at. */
|
|
9
|
+
afterOf: Map<string, string[]>;
|
|
10
|
+
/** File id -> its frontmatter `type`. */
|
|
11
|
+
types: Map<string, unknown>;
|
|
12
|
+
/** File id -> title, for readable messages. */
|
|
13
|
+
titles: Map<string, string>;
|
|
14
|
+
}
|
|
15
|
+
export declare function createLintContext(files: VaultFile[]): LintContext;
|
|
16
|
+
/**
|
|
17
|
+
* Runs every lint rule against one file. Pass the file's own `id` when it already
|
|
18
|
+
* exists in the vault (validate, update), so it isn't compared against itself.
|
|
19
|
+
*/
|
|
20
|
+
export declare function lintFile(file: Pick<VaultFile, "frontmatter" | "body"> & {
|
|
21
|
+
id?: string;
|
|
22
|
+
}, context: LintContext): ValidationIssue[];
|
|
23
|
+
/** Lints every file in a vault snapshot. Returns only files with at least one issue. */
|
|
24
|
+
export declare function lintVault(files: VaultFile[]): Map<string, ValidationIssue[]>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { findDanglingLinks } from "./dangling-links.js";
|
|
2
|
+
import { findEventsWithoutEntities } from "./events-without-entities.js";
|
|
3
|
+
import { findForkedVersions, supersedesTarget } from "./forked-versions.js";
|
|
4
|
+
import { findAfterLoops, afterTargets } from "./after-loops.js";
|
|
5
|
+
import { findWrongTargetTypes } from "./target-types.js";
|
|
6
|
+
export function createLintContext(files) {
|
|
7
|
+
const supersededBy = new Map();
|
|
8
|
+
const afterOf = new Map();
|
|
9
|
+
const titles = new Map();
|
|
10
|
+
for (const file of files) {
|
|
11
|
+
const target = supersedesTarget(file.frontmatter);
|
|
12
|
+
if (target)
|
|
13
|
+
supersededBy.set(target, [...(supersededBy.get(target) ?? []), file.id]);
|
|
14
|
+
if (file.frontmatter.type === "event")
|
|
15
|
+
afterOf.set(file.id, afterTargets(file.frontmatter));
|
|
16
|
+
if (typeof file.frontmatter.title === "string")
|
|
17
|
+
titles.set(file.id, file.frontmatter.title);
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
knownIds: new Set(files.map((file) => file.id)),
|
|
21
|
+
supersededBy,
|
|
22
|
+
afterOf,
|
|
23
|
+
types: new Map(files.map((file) => [file.id, file.frontmatter.type])),
|
|
24
|
+
titles,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Runs every lint rule against one file. Pass the file's own `id` when it already
|
|
29
|
+
* exists in the vault (validate, update), so it isn't compared against itself.
|
|
30
|
+
*/
|
|
31
|
+
export function lintFile(file, context) {
|
|
32
|
+
return [
|
|
33
|
+
...findDanglingLinks(file, context.knownIds),
|
|
34
|
+
...findEventsWithoutEntities(file),
|
|
35
|
+
...findForkedVersions(file, context.supersededBy),
|
|
36
|
+
...findAfterLoops(file, context.afterOf, context.titles),
|
|
37
|
+
...findWrongTargetTypes(file, context.types, context.titles),
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
/** Lints every file in a vault snapshot. Returns only files with at least one issue. */
|
|
41
|
+
export function lintVault(files) {
|
|
42
|
+
const context = createLintContext(files);
|
|
43
|
+
const result = new Map();
|
|
44
|
+
for (const file of files) {
|
|
45
|
+
const issues = lintFile(file, context);
|
|
46
|
+
if (issues.length > 0)
|
|
47
|
+
result.set(file.id, issues);
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { VaultFile } from "../vault/types.js";
|
|
2
|
+
import type { ValidationIssue } from "../schema/validate.js";
|
|
3
|
+
/**
|
|
4
|
+
* Flags each reserved relation whose target exists but is the wrong type. An
|
|
5
|
+
* error, since every case above silently breaks the wiki. Targets that don't
|
|
6
|
+
* exist at all are left to the dangling-link rule. `types` maps file id -> its
|
|
7
|
+
* frontmatter `type`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function findWrongTargetTypes(file: Pick<VaultFile, "frontmatter">, types: Map<string, unknown>, titles: Map<string, string>): ValidationIssue[];
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { isWikilink, parseWikilink } from "../schema/wikilink.js";
|
|
2
|
+
/**
|
|
3
|
+
* Reserved relation fields whose targets must be a specific type (SCHEMA.md),
|
|
4
|
+
* keyed by the type of the file holding the field. The schema only checks the
|
|
5
|
+
* values are wikilinks; a wrong-typed target passes that and then misbehaves
|
|
6
|
+
* at render time:
|
|
7
|
+
* - `after` -> non-event: causal ordering fails.
|
|
8
|
+
* - `entities` -> non-entity: the event isn't tied to any entity.
|
|
9
|
+
* - `causedBy` -> non-event: merge never shows the version (its "causing event" is never included).
|
|
10
|
+
* - `supersedes` -> non-entity: the version is unreachable from any anchor, so it never renders.
|
|
11
|
+
* On any other file type these field names are just free-form relations.
|
|
12
|
+
*/
|
|
13
|
+
const REQUIRED_TARGET_TYPES = {
|
|
14
|
+
event: { after: "event", entities: "entity" },
|
|
15
|
+
entity: { causedBy: "event", supersedes: "entity" },
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Flags each reserved relation whose target exists but is the wrong type. An
|
|
19
|
+
* error, since every case above silently breaks the wiki. Targets that don't
|
|
20
|
+
* exist at all are left to the dangling-link rule. `types` maps file id -> its
|
|
21
|
+
* frontmatter `type`.
|
|
22
|
+
*/
|
|
23
|
+
export function findWrongTargetTypes(file, types, titles) {
|
|
24
|
+
const fileType = file.frontmatter.type;
|
|
25
|
+
if (fileType !== "entity" && fileType !== "event")
|
|
26
|
+
return [];
|
|
27
|
+
const issues = [];
|
|
28
|
+
for (const [field, requiredType] of Object.entries(REQUIRED_TARGET_TYPES[fileType])) {
|
|
29
|
+
const value = file.frontmatter[field];
|
|
30
|
+
const entries = Array.isArray(value)
|
|
31
|
+
? value.map((entry, i) => [`${field}[${i}]`, entry])
|
|
32
|
+
: [[field, value]];
|
|
33
|
+
for (const [path, entry] of entries) {
|
|
34
|
+
if (!isWikilink(entry))
|
|
35
|
+
continue;
|
|
36
|
+
const { targetId } = parseWikilink(entry);
|
|
37
|
+
if (!types.has(targetId) || types.get(targetId) === requiredType)
|
|
38
|
+
continue;
|
|
39
|
+
issues.push({
|
|
40
|
+
path,
|
|
41
|
+
message: `\`${field}\` must point at an ${requiredType}, but "${titles.get(targetId) ?? "untitled"}" (${targetId}) isn't one`,
|
|
42
|
+
severity: "error",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return issues;
|
|
47
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const dateBoundSchema: z.ZodObject<{
|
|
3
|
+
year: z.ZodNumber;
|
|
4
|
+
day: z.ZodOptional<z.ZodNumber>;
|
|
5
|
+
}, z.core.$strip>;
|
|
6
|
+
export type DateBound = z.infer<typeof dateBoundSchema>;
|
|
7
|
+
export declare const dateRangeQuerySchema: z.ZodObject<{
|
|
8
|
+
from: z.ZodOptional<z.ZodObject<{
|
|
9
|
+
year: z.ZodNumber;
|
|
10
|
+
day: z.ZodOptional<z.ZodNumber>;
|
|
11
|
+
}, z.core.$strip>>;
|
|
12
|
+
to: z.ZodOptional<z.ZodObject<{
|
|
13
|
+
year: z.ZodNumber;
|
|
14
|
+
day: z.ZodOptional<z.ZodNumber>;
|
|
15
|
+
}, z.core.$strip>>;
|
|
16
|
+
}, z.core.$strip>;
|
|
17
|
+
export type DateRangeQuery = z.infer<typeof dateRangeQuerySchema>;
|
|
18
|
+
/** An in-world date, as stored in an event's `date` field. */
|
|
19
|
+
export interface DateTuple {
|
|
20
|
+
year: number;
|
|
21
|
+
day: number;
|
|
22
|
+
}
|
|
23
|
+
/** Negative if a is earlier than b, positive if later, 0 if the same date. */
|
|
24
|
+
export declare function compareDates(a: DateTuple, b: DateTuple): number;
|
|
25
|
+
/** True if a dated value falls within the range's bounds (inclusive). Either bound is optional. */
|
|
26
|
+
export declare function isDateWithinRange(date: DateTuple, range: DateRangeQuery): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* True if a dated value is at or before the range's upper bound. Used for the
|
|
29
|
+
* entity/version inclusion gate — the lower bound never excludes entities
|
|
30
|
+
* (REQUIREMENTS.md: an entity introduced long ago can still be relevant now).
|
|
31
|
+
*/
|
|
32
|
+
export declare function isAtOrBeforeUpperBound(date: DateTuple, range: DateRangeQuery): boolean;
|
|
33
|
+
/** True if a dated value is at or after the range's lower bound (or the range has none). */
|
|
34
|
+
export declare function isAtOrAfterLowerBound(date: DateTuple, range: DateRangeQuery): boolean;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const dateBoundSchema = z.object({
|
|
3
|
+
year: z.number().int(),
|
|
4
|
+
day: z.number().int().optional(),
|
|
5
|
+
});
|
|
6
|
+
export const dateRangeQuerySchema = z.object({
|
|
7
|
+
from: dateBoundSchema.optional(),
|
|
8
|
+
to: dateBoundSchema.optional(),
|
|
9
|
+
});
|
|
10
|
+
function lowerTuple(bound) {
|
|
11
|
+
return { year: bound.year, day: bound.day ?? -Infinity };
|
|
12
|
+
}
|
|
13
|
+
function upperTuple(bound) {
|
|
14
|
+
return { year: bound.year, day: bound.day ?? Infinity };
|
|
15
|
+
}
|
|
16
|
+
/** Negative if a is earlier than b, positive if later, 0 if the same date. */
|
|
17
|
+
export function compareDates(a, b) {
|
|
18
|
+
return a.year !== b.year ? a.year - b.year : a.day - b.day;
|
|
19
|
+
}
|
|
20
|
+
/** True if a dated value falls within the range's bounds (inclusive). Either bound is optional. */
|
|
21
|
+
export function isDateWithinRange(date, range) {
|
|
22
|
+
if (range.from && compareDates(date, lowerTuple(range.from)) < 0)
|
|
23
|
+
return false;
|
|
24
|
+
if (range.to && compareDates(date, upperTuple(range.to)) > 0)
|
|
25
|
+
return false;
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* True if a dated value is at or before the range's upper bound. Used for the
|
|
30
|
+
* entity/version inclusion gate — the lower bound never excludes entities
|
|
31
|
+
* (REQUIREMENTS.md: an entity introduced long ago can still be relevant now).
|
|
32
|
+
*/
|
|
33
|
+
export function isAtOrBeforeUpperBound(date, range) {
|
|
34
|
+
if (!range.to)
|
|
35
|
+
return true;
|
|
36
|
+
return compareDates(date, upperTuple(range.to)) <= 0;
|
|
37
|
+
}
|
|
38
|
+
/** True if a dated value is at or after the range's lower bound (or the range has none). */
|
|
39
|
+
export function isAtOrAfterLowerBound(date, range) {
|
|
40
|
+
if (!range.from)
|
|
41
|
+
return true;
|
|
42
|
+
return compareDates(date, lowerTuple(range.from)) >= 0;
|
|
43
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* A named build variant's filter query (PROJECT_CONFIG.md #3): tags a file must
|
|
4
|
+
* carry, and/or an event date range. This is both the runtime shape the render
|
|
5
|
+
* pipeline filters against and the shape a vault's world.config.json validates
|
|
6
|
+
* its `buildVariants` entries against — one definition, no separate DSL to parse.
|
|
7
|
+
*/
|
|
8
|
+
export declare const buildVariantQuerySchema: z.ZodObject<{
|
|
9
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
10
|
+
dateRange: z.ZodOptional<z.ZodObject<{
|
|
11
|
+
from: z.ZodOptional<z.ZodObject<{
|
|
12
|
+
year: z.ZodNumber;
|
|
13
|
+
day: z.ZodOptional<z.ZodNumber>;
|
|
14
|
+
}, z.core.$strip>>;
|
|
15
|
+
to: z.ZodOptional<z.ZodObject<{
|
|
16
|
+
year: z.ZodNumber;
|
|
17
|
+
day: z.ZodOptional<z.ZodNumber>;
|
|
18
|
+
}, z.core.$strip>>;
|
|
19
|
+
}, z.core.$strip>>;
|
|
20
|
+
}, z.core.$strip>;
|
|
21
|
+
export type BuildVariantQuery = z.infer<typeof buildVariantQuerySchema>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { dateRangeQuerySchema } from "./dates.js";
|
|
3
|
+
/**
|
|
4
|
+
* A named build variant's filter query (PROJECT_CONFIG.md #3): tags a file must
|
|
5
|
+
* carry, and/or an event date range. This is both the runtime shape the render
|
|
6
|
+
* pipeline filters against and the shape a vault's world.config.json validates
|
|
7
|
+
* its `buildVariants` entries against — one definition, no separate DSL to parse.
|
|
8
|
+
*/
|
|
9
|
+
export const buildVariantQuerySchema = z.object({
|
|
10
|
+
tags: z.array(z.string()).optional(),
|
|
11
|
+
dateRange: dateRangeQuerySchema.optional(),
|
|
12
|
+
});
|
|
@@ -0,0 +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
|
+
};
|
|
27
|
+
/**
|
|
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
|
|
30
|
+
* validateFrontmatter actually enforces. This is what the CLI's `schema`
|
|
31
|
+
* command exposes so an agent always sees the real, current rules — see
|
|
32
|
+
* claude-dev/PROJECT_STRUCTURE.md.
|
|
33
|
+
*/
|
|
34
|
+
export declare function describeSchema(entityTypeVocabulary?: string[]): string;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { commonFieldsSchema, entityFrontmatterSchema, eventFrontmatterSchema, wikilinkSchema, } from "./fields.js";
|
|
3
|
+
const FIELD_MEANINGS = {
|
|
4
|
+
type: "Top-level kind.",
|
|
5
|
+
title: "Human-readable display name (filenames are UUIDs, this is the real name).",
|
|
6
|
+
tags: "Project-defined tags used for build-time filtering.",
|
|
7
|
+
aliases: "Alternate names/spellings, for search recall and Obsidian autocomplete only — not used for link resolution.",
|
|
8
|
+
entityType: "Category (e.g. character, location). Lint-checked against this vault's declared vocabulary, if any.",
|
|
9
|
+
supersedes: "Marks this file as a new version of an existing entity — points at the file it replaces.",
|
|
10
|
+
causedBy: "The event responsible for this version existing. Required alongside supersedes, and vice versa.",
|
|
11
|
+
after: "Causal predecessor event(s) — the required ordering/branching backbone.",
|
|
12
|
+
date: "In-world date anchor. Give both year and day.",
|
|
13
|
+
entities: "The entities this event is tied to.",
|
|
14
|
+
};
|
|
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";
|
|
57
|
+
}
|
|
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
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
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
|
|
68
|
+
* validateFrontmatter actually enforces. This is what the CLI's `schema`
|
|
69
|
+
* command exposes so an agent always sees the real, current rules — see
|
|
70
|
+
* claude-dev/PROJECT_STRUCTURE.md.
|
|
71
|
+
*/
|
|
72
|
+
export function describeSchema(entityTypeVocabulary) {
|
|
73
|
+
const commonKeys = Object.keys(commonFieldsSchema.shape);
|
|
74
|
+
const excluded = new Set([...commonKeys, "type"]);
|
|
75
|
+
const entityKeys = Object.keys(entityFrontmatterSchema.shape).filter((key) => !excluded.has(key));
|
|
76
|
+
const eventKeys = Object.keys(eventFrontmatterSchema.shape).filter((key) => !excluded.has(key));
|
|
77
|
+
const lines = [
|
|
78
|
+
"Common fields (every file):",
|
|
79
|
+
` - type ("entity" or "event", required): ${FIELD_MEANINGS.type}`,
|
|
80
|
+
...describeFields(commonFieldsSchema.shape, commonKeys),
|
|
81
|
+
"",
|
|
82
|
+
"Entity-only fields:",
|
|
83
|
+
...describeFields(entityFrontmatterSchema.shape, entityKeys),
|
|
84
|
+
"",
|
|
85
|
+
"Event-only fields:",
|
|
86
|
+
...describeFields(eventFrontmatterSchema.shape, eventKeys),
|
|
87
|
+
"",
|
|
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.",
|
|
90
|
+
];
|
|
91
|
+
if (entityTypeVocabulary && entityTypeVocabulary.length > 0) {
|
|
92
|
+
lines.push("", `This vault's declared entityType vocabulary: ${entityTypeVocabulary.join(", ")}`);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
lines.push("", "This vault has not declared an entityType vocabulary — any entityType value is accepted.");
|
|
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)}`);
|
|
98
|
+
return lines.join("\n");
|
|
99
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
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.ZodString & z.ZodType<string, string, z.core.$ZodTypeInternals<string, string>>;
|
|
4
|
+
export declare const inWorldDateSchema: z.ZodObject<{
|
|
5
|
+
year: z.ZodNumber;
|
|
6
|
+
day: z.ZodNumber;
|
|
7
|
+
}, z.core.$strip>;
|
|
8
|
+
/** Fields every knowledge file carries (SCHEMA.md "Common frontmatter"). */
|
|
9
|
+
export declare const commonFieldsSchema: z.ZodObject<{
|
|
10
|
+
title: z.ZodString;
|
|
11
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
12
|
+
aliases: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
13
|
+
}, z.core.$strip>;
|
|
14
|
+
export declare const entityFrontmatterSchema: z.ZodObject<{
|
|
15
|
+
title: z.ZodString;
|
|
16
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
17
|
+
aliases: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
18
|
+
type: z.ZodLiteral<"entity">;
|
|
19
|
+
entityType: z.ZodOptional<z.ZodString>;
|
|
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>;
|
|
23
|
+
export declare const eventFrontmatterSchema: z.ZodObject<{
|
|
24
|
+
title: z.ZodString;
|
|
25
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
26
|
+
aliases: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
27
|
+
type: z.ZodLiteral<"event">;
|
|
28
|
+
after: z.ZodOptional<z.ZodArray<z.ZodString & z.ZodType<string, string, z.core.$ZodTypeInternals<string, string>>>>;
|
|
29
|
+
date: z.ZodOptional<z.ZodObject<{
|
|
30
|
+
year: z.ZodNumber;
|
|
31
|
+
day: z.ZodNumber;
|
|
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<{
|
|
36
|
+
title: z.ZodString;
|
|
37
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
38
|
+
aliases: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
39
|
+
type: z.ZodLiteral<"entity">;
|
|
40
|
+
entityType: z.ZodOptional<z.ZodString>;
|
|
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<{
|
|
44
|
+
title: z.ZodString;
|
|
45
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
46
|
+
aliases: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
47
|
+
type: z.ZodLiteral<"event">;
|
|
48
|
+
after: z.ZodOptional<z.ZodArray<z.ZodString & z.ZodType<string, string, z.core.$ZodTypeInternals<string, string>>>>;
|
|
49
|
+
date: z.ZodOptional<z.ZodObject<{
|
|
50
|
+
year: z.ZodNumber;
|
|
51
|
+
day: z.ZodNumber;
|
|
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">;
|
|
55
|
+
export type EntityFrontmatter = z.infer<typeof entityFrontmatterSchema>;
|
|
56
|
+
export type EventFrontmatter = z.infer<typeof eventFrontmatterSchema>;
|
|
57
|
+
export type KnowledgeFileFrontmatter = z.infer<typeof knowledgeFileFrontmatterSchema>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { isWikilink } from "./wikilink.js";
|
|
3
|
+
/** 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. */
|
|
4
|
+
export const wikilinkSchema = z.string().refine(isWikilink, {
|
|
5
|
+
message: 'Expected a wikilink in the form "[[uuid|Display Name]]"',
|
|
6
|
+
});
|
|
7
|
+
export const inWorldDateSchema = z.object({
|
|
8
|
+
year: z.number().int(),
|
|
9
|
+
day: z.number().int(),
|
|
10
|
+
});
|
|
11
|
+
/** Fields every knowledge file carries (SCHEMA.md "Common frontmatter"). */
|
|
12
|
+
export const commonFieldsSchema = z.object({
|
|
13
|
+
title: z.string().min(1, "title is required"),
|
|
14
|
+
tags: z.array(z.string()).optional(),
|
|
15
|
+
aliases: z.array(z.string()).optional(),
|
|
16
|
+
});
|
|
17
|
+
export const entityFrontmatterSchema = commonFieldsSchema
|
|
18
|
+
.extend({
|
|
19
|
+
type: z.literal("entity"),
|
|
20
|
+
entityType: z.string().optional(),
|
|
21
|
+
supersedes: wikilinkSchema.optional(),
|
|
22
|
+
causedBy: wikilinkSchema.optional(),
|
|
23
|
+
})
|
|
24
|
+
.loose();
|
|
25
|
+
export const eventFrontmatterSchema = commonFieldsSchema
|
|
26
|
+
.extend({
|
|
27
|
+
type: z.literal("event"),
|
|
28
|
+
after: z.array(wikilinkSchema).optional(),
|
|
29
|
+
date: inWorldDateSchema.optional(),
|
|
30
|
+
entities: z.array(wikilinkSchema).optional(),
|
|
31
|
+
})
|
|
32
|
+
.loose();
|
|
33
|
+
export const knowledgeFileFrontmatterSchema = z
|
|
34
|
+
.discriminatedUnion("type", [entityFrontmatterSchema, eventFrontmatterSchema])
|
|
35
|
+
.superRefine((data, ctx) => {
|
|
36
|
+
if (data.type === "entity" && Boolean(data.supersedes) !== Boolean(data.causedBy)) {
|
|
37
|
+
ctx.addIssue({
|
|
38
|
+
code: "custom",
|
|
39
|
+
message: '"supersedes" and "causedBy" must both be present, or both absent',
|
|
40
|
+
path: ["causedBy"],
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type KnowledgeFileFrontmatter } from "./fields.js";
|
|
2
|
+
export interface ValidationIssue {
|
|
3
|
+
path: string;
|
|
4
|
+
message: string;
|
|
5
|
+
/** "error" blocks a write and makes a file invalid; "warning" is a suggestion only. */
|
|
6
|
+
severity: "error" | "warning";
|
|
7
|
+
}
|
|
8
|
+
export declare function hasErrors(issues: ValidationIssue[]): boolean;
|
|
9
|
+
export interface ValidationResult {
|
|
10
|
+
valid: boolean;
|
|
11
|
+
frontmatter?: KnowledgeFileFrontmatter;
|
|
12
|
+
issues: ValidationIssue[];
|
|
13
|
+
}
|
|
14
|
+
export interface ValidateOptions {
|
|
15
|
+
/** This project's declared entityType vocabulary (PROJECT_CONFIG.md). Omit to skip the check. */
|
|
16
|
+
entityTypeVocabulary?: string[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The one place a knowledge file's frontmatter gets checked (PROJECT_STRUCTURE.md:
|
|
20
|
+
* used by vault parsing, CLI `validate`/`create`, and the CLI's `schema` introspection).
|
|
21
|
+
*/
|
|
22
|
+
export declare function validateFrontmatter(raw: unknown, options?: ValidateOptions): ValidationResult;
|