@graview/core 0.1.0 → 0.1.3
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 +7 -0
- package/dist/app.d.ts +17 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js.map +1 -1
- package/dist/arrange.d.ts +3 -2
- package/dist/arrange.d.ts.map +1 -1
- package/dist/arrange.js +19 -2
- package/dist/arrange.js.map +1 -1
- package/dist/beginning.d.ts +5 -2
- package/dist/beginning.d.ts.map +1 -1
- package/dist/beginning.js +28 -0
- package/dist/beginning.js.map +1 -1
- package/dist/capabilities.d.ts +32 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +31 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/cli/check/index.d.ts +1 -0
- package/dist/cli/check/index.d.ts.map +1 -1
- package/dist/cli/check/index.js +1 -0
- package/dist/cli/check/index.js.map +1 -1
- package/dist/cli/check/lenses.d.ts.map +1 -1
- package/dist/cli/check/lenses.js +25 -0
- package/dist/cli/check/lenses.js.map +1 -1
- package/dist/cli/check/policy.d.ts.map +1 -1
- package/dist/cli/check/policy.js +35 -0
- package/dist/cli/check/policy.js.map +1 -1
- package/dist/cli/check/relations.d.ts.map +1 -1
- package/dist/cli/check/relations.js +31 -0
- package/dist/cli/check/relations.js.map +1 -1
- package/dist/cli/check/views.d.ts +14 -0
- package/dist/cli/check/views.d.ts.map +1 -0
- package/dist/cli/check/views.js +27 -0
- package/dist/cli/check/views.js.map +1 -0
- package/dist/cli/check.d.ts.map +1 -1
- package/dist/cli/check.js +2 -1
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/create.d.ts +1 -1
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +63 -1
- package/dist/cli/create.js.map +1 -1
- package/dist/cli/index.d.ts +18 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +66 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/conformance/fixtures.d.ts +41 -0
- package/dist/conformance/fixtures.d.ts.map +1 -0
- package/dist/conformance/fixtures.js +950 -0
- package/dist/conformance/fixtures.js.map +1 -0
- package/dist/conformance/index.d.ts +54 -0
- package/dist/conformance/index.d.ts.map +1 -0
- package/dist/conformance/index.js +81 -0
- package/dist/conformance/index.js.map +1 -0
- package/dist/document/canonical.d.ts +4 -0
- package/dist/document/canonical.d.ts.map +1 -0
- package/dist/document/canonical.js +29 -0
- package/dist/document/canonical.js.map +1 -0
- package/dist/document/compile.d.ts +43 -0
- package/dist/document/compile.d.ts.map +1 -0
- package/dist/document/compile.js +807 -0
- package/dist/document/compile.js.map +1 -0
- package/dist/document/diff.d.ts +32 -0
- package/dist/document/diff.d.ts.map +1 -0
- package/dist/document/diff.js +148 -0
- package/dist/document/diff.js.map +1 -0
- package/dist/document/edit.d.ts +63 -0
- package/dist/document/edit.d.ts.map +1 -0
- package/dist/document/edit.js +1432 -0
- package/dist/document/edit.js.map +1 -0
- package/dist/document/expr/analyze.d.ts +14 -0
- package/dist/document/expr/analyze.d.ts.map +1 -0
- package/dist/document/expr/analyze.js +52 -0
- package/dist/document/expr/analyze.js.map +1 -0
- package/dist/document/expr/evaluate.d.ts +36 -0
- package/dist/document/expr/evaluate.d.ts.map +1 -0
- package/dist/document/expr/evaluate.js +344 -0
- package/dist/document/expr/evaluate.js.map +1 -0
- package/dist/document/expr/index.d.ts +8 -0
- package/dist/document/expr/index.d.ts.map +1 -0
- package/dist/document/expr/index.js +5 -0
- package/dist/document/expr/index.js.map +1 -0
- package/dist/document/expr/parse.d.ts +50 -0
- package/dist/document/expr/parse.d.ts.map +1 -0
- package/dist/document/expr/parse.js +240 -0
- package/dist/document/expr/parse.js.map +1 -0
- package/dist/document/expr/print.d.ts +4 -0
- package/dist/document/expr/print.d.ts.map +1 -0
- package/dist/document/expr/print.js +81 -0
- package/dist/document/expr/print.js.map +1 -0
- package/dist/document/findings.d.ts +15 -0
- package/dist/document/findings.d.ts.map +1 -0
- package/dist/document/findings.js +30 -0
- package/dist/document/findings.js.map +1 -0
- package/dist/document/graview-template.d.ts +148 -0
- package/dist/document/graview-template.d.ts.map +1 -0
- package/dist/document/graview-template.js +450 -0
- package/dist/document/graview-template.js.map +1 -0
- package/dist/document/index.d.ts +27 -0
- package/dist/document/index.d.ts.map +1 -0
- package/dist/document/index.js +16 -0
- package/dist/document/index.js.map +1 -0
- package/dist/document/migrate.d.ts +41 -0
- package/dist/document/migrate.d.ts.map +1 -0
- package/dist/document/migrate.js +252 -0
- package/dist/document/migrate.js.map +1 -0
- package/dist/document/patch.d.ts +16 -0
- package/dist/document/patch.d.ts.map +1 -0
- package/dist/document/patch.js +79 -0
- package/dist/document/patch.js.map +1 -0
- package/dist/document/rules.d.ts +40 -0
- package/dist/document/rules.d.ts.map +1 -0
- package/dist/document/rules.js +88 -0
- package/dist/document/rules.js.map +1 -0
- package/dist/document/schema.d.ts +461 -0
- package/dist/document/schema.d.ts.map +1 -0
- package/dist/document/schema.js +199 -0
- package/dist/document/schema.js.map +1 -0
- package/dist/document/template.d.ts +28 -0
- package/dist/document/template.d.ts.map +1 -0
- package/dist/document/template.js +117 -0
- package/dist/document/template.js.map +1 -0
- package/dist/document/to-document.d.ts +34 -0
- package/dist/document/to-document.d.ts.map +1 -0
- package/dist/document/to-document.js +186 -0
- package/dist/document/to-document.js.map +1 -0
- package/dist/document/upgrade.d.ts +9 -0
- package/dist/document/upgrade.d.ts.map +1 -0
- package/dist/document/upgrade.js +19 -0
- package/dist/document/upgrade.js.map +1 -0
- package/dist/document/views.d.ts +65 -0
- package/dist/document/views.d.ts.map +1 -0
- package/dist/document/views.js +264 -0
- package/dist/document/views.js.map +1 -0
- package/dist/formats.d.ts +50 -0
- package/dist/formats.d.ts.map +1 -0
- package/dist/formats.js +75 -0
- package/dist/formats.js.map +1 -0
- package/dist/graph/graph.d.ts +61 -3
- package/dist/graph/graph.d.ts.map +1 -1
- package/dist/graph/graph.js +187 -20
- package/dist/graph/graph.js.map +1 -1
- package/dist/index.d.ts +43 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +31 -11
- package/dist/index.js.map +1 -1
- package/dist/integrity.d.ts +46 -0
- package/dist/integrity.d.ts.map +1 -0
- package/dist/integrity.js +179 -0
- package/dist/integrity.js.map +1 -0
- package/dist/invariants/engine.d.ts +8 -0
- package/dist/invariants/engine.d.ts.map +1 -1
- package/dist/invariants/engine.js +40 -2
- package/dist/invariants/engine.js.map +1 -1
- package/dist/invariants/types.d.ts +19 -0
- package/dist/invariants/types.d.ts.map +1 -1
- package/dist/labels.d.ts +65 -0
- package/dist/labels.d.ts.map +1 -0
- package/dist/labels.js +123 -0
- package/dist/labels.js.map +1 -0
- package/dist/mutations/define-mutation.d.ts.map +1 -1
- package/dist/mutations/define-mutation.js +5 -6
- package/dist/mutations/define-mutation.js.map +1 -1
- package/dist/mutations/derive-edits.d.ts.map +1 -1
- package/dist/mutations/derive-edits.js +20 -1
- package/dist/mutations/derive-edits.js.map +1 -1
- package/dist/mutations/form.d.ts +9 -0
- package/dist/mutations/form.d.ts.map +1 -1
- package/dist/mutations/form.js +20 -0
- package/dist/mutations/form.js.map +1 -1
- package/dist/mutations/node-ref.d.ts +6 -0
- package/dist/mutations/node-ref.d.ts.map +1 -1
- package/dist/mutations/node-ref.js +22 -8
- package/dist/mutations/node-ref.js.map +1 -1
- package/dist/mutations/types.d.ts +8 -0
- package/dist/mutations/types.d.ts.map +1 -1
- package/dist/mutations/words.d.ts +49 -0
- package/dist/mutations/words.d.ts.map +1 -0
- package/dist/mutations/words.js +61 -0
- package/dist/mutations/words.js.map +1 -0
- package/dist/ops/log.d.ts +156 -4
- package/dist/ops/log.d.ts.map +1 -1
- package/dist/ops/log.js +234 -51
- package/dist/ops/log.js.map +1 -1
- package/dist/ops/revisions.d.ts +66 -0
- package/dist/ops/revisions.d.ts.map +1 -0
- package/dist/ops/revisions.js +97 -0
- package/dist/ops/revisions.js.map +1 -0
- package/dist/ops/types.d.ts +49 -1
- package/dist/ops/types.d.ts.map +1 -1
- package/dist/ops/undo.d.ts +29 -2
- package/dist/ops/undo.d.ts.map +1 -1
- package/dist/ops/undo.js +110 -1
- package/dist/ops/undo.js.map +1 -1
- package/dist/ops/withheld.d.ts +37 -0
- package/dist/ops/withheld.d.ts.map +1 -0
- package/dist/ops/withheld.js +84 -0
- package/dist/ops/withheld.js.map +1 -0
- package/dist/permissions/policy.d.ts +15 -0
- package/dist/permissions/policy.d.ts.map +1 -1
- package/dist/permissions/policy.js +28 -1
- package/dist/permissions/policy.js.map +1 -1
- package/dist/permissions/sight.d.ts +66 -0
- package/dist/permissions/sight.d.ts.map +1 -0
- package/dist/permissions/sight.js +131 -0
- package/dist/permissions/sight.js.map +1 -0
- package/dist/permissions/types.d.ts +26 -0
- package/dist/permissions/types.d.ts.map +1 -1
- package/dist/permissions/types.js.map +1 -1
- package/dist/persistence/memory.d.ts.map +1 -1
- package/dist/persistence/memory.js +25 -0
- package/dist/persistence/memory.js.map +1 -1
- package/dist/persistence/sql.d.ts +43 -0
- package/dist/persistence/sql.d.ts.map +1 -0
- package/dist/persistence/sql.js +154 -0
- package/dist/persistence/sql.js.map +1 -0
- package/dist/persistence/sqlite.d.ts +17 -3
- package/dist/persistence/sqlite.d.ts.map +1 -1
- package/dist/persistence/sqlite.js +32 -96
- package/dist/persistence/sqlite.js.map +1 -1
- package/dist/persistence/types.d.ts +19 -0
- package/dist/persistence/types.d.ts.map +1 -1
- package/dist/presence.d.ts +56 -0
- package/dist/presence.d.ts.map +1 -1
- package/dist/presence.js +62 -2
- package/dist/presence.js.map +1 -1
- package/dist/refusal.d.ts +34 -0
- package/dist/refusal.d.ts.map +1 -0
- package/dist/refusal.js +40 -0
- package/dist/refusal.js.map +1 -0
- package/dist/scaffold/from-template.d.ts +16 -0
- package/dist/scaffold/from-template.d.ts.map +1 -0
- package/dist/scaffold/from-template.js +157 -0
- package/dist/scaffold/from-template.js.map +1 -0
- package/dist/scaffold/index.d.ts +10 -1
- package/dist/scaffold/index.d.ts.map +1 -1
- package/dist/scaffold/index.js +35 -7
- package/dist/scaffold/index.js.map +1 -1
- package/dist/scaffold/names.d.ts +4 -0
- package/dist/scaffold/names.d.ts.map +1 -1
- package/dist/scaffold/names.js.map +1 -1
- package/dist/scaffold/project.d.ts +1 -1
- package/dist/scaffold/project.d.ts.map +1 -1
- package/dist/scaffold/project.js +44 -13
- package/dist/scaffold/project.js.map +1 -1
- package/dist/scaffold/ui.d.ts.map +1 -1
- package/dist/scaffold/ui.js +19 -10
- package/dist/scaffold/ui.js.map +1 -1
- package/dist/schema/define-node.d.ts +20 -0
- package/dist/schema/define-node.d.ts.map +1 -1
- package/dist/schema/define-node.js +27 -2
- package/dist/schema/define-node.js.map +1 -1
- package/dist/schema/path.d.ts +19 -0
- package/dist/schema/path.d.ts.map +1 -0
- package/dist/schema/path.js +34 -0
- package/dist/schema/path.js.map +1 -0
- package/dist/schema/types.d.ts +17 -0
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/seen.d.ts +64 -0
- package/dist/seen.d.ts.map +1 -0
- package/dist/seen.js +200 -0
- package/dist/seen.js.map +1 -0
- package/dist/store.d.ts +378 -8
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +801 -41
- package/dist/store.js.map +1 -1
- package/dist/testing.d.ts +6 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +3 -1
- package/dist/testing.js.map +1 -1
- package/dist/theme/palettes.d.ts.map +1 -1
- package/dist/theme/palettes.js +4 -0
- package/dist/theme/palettes.js.map +1 -1
- package/dist/theme/types.d.ts +9 -0
- package/dist/theme/types.d.ts.map +1 -1
- package/dist/theme/types.js +4 -0
- package/dist/theme/types.js.map +1 -1
- package/dist/validate-graph.d.ts +84 -0
- package/dist/validate-graph.d.ts.map +1 -0
- package/dist/validate-graph.js +295 -0
- package/dist/validate-graph.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/dist/views/types.d.ts +8 -0
- package/dist/views/types.d.ts.map +1 -1
- package/dist/views/types.js +5 -0
- package/dist/views/types.js.map +1 -1
- package/dist/watched.d.ts +39 -0
- package/dist/watched.d.ts.map +1 -1
- package/dist/watched.js +94 -1
- package/dist/watched.js.map +1 -1
- package/dist/who.d.ts +24 -1
- package/dist/who.d.ts.map +1 -1
- package/dist/who.js +49 -2
- package/dist/who.js.map +1 -1
- package/package.json +9 -1
|
@@ -0,0 +1,807 @@
|
|
|
1
|
+
import { RuleBudgetError, withArticle, brandFromAccent, checkApp, createSchema, defineInvariant, defineMutation, defineNode, nodeRef, SCHEMES, } from "../index.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { analyzeExpr } from "./expr/analyze.js";
|
|
4
|
+
import { rememberDocument } from "./to-document.js";
|
|
5
|
+
import { evaluateExpr, ExprBudgetError, ExprEvalError } from "./expr/evaluate.js";
|
|
6
|
+
import { ExprSyntaxError, parseExpr } from "./expr/parse.js";
|
|
7
|
+
import { error, hasErrors, warning } from "./findings.js";
|
|
8
|
+
import { DocumentSpec, MAX_DOCUMENT_BYTES, } from "./schema.js";
|
|
9
|
+
import { parseTemplate, renderTemplate, TemplateError } from "./template.js";
|
|
10
|
+
import { upgradeDocument } from "./upgrade.js";
|
|
11
|
+
import { validateViews } from "./views.js";
|
|
12
|
+
const BUILTIN_REFS = new Set(["subject", "now", "today"]);
|
|
13
|
+
const SUBJECT_ARG = "id";
|
|
14
|
+
/** Parse and validate without compiling — the document's own sentences, before the framework's. */
|
|
15
|
+
export function readDocument(raw) {
|
|
16
|
+
if (typeof raw === "string") {
|
|
17
|
+
if (raw.length > MAX_DOCUMENT_BYTES)
|
|
18
|
+
return { findings: [error("too-large", "", `a document may be at most ${MAX_DOCUMENT_BYTES / 1024} KB`)] };
|
|
19
|
+
try {
|
|
20
|
+
raw = JSON.parse(raw);
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
return { findings: [error("not-json", "", `this is not JSON: ${e.message}`)] };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else if (JSON.stringify(raw ?? null).length > MAX_DOCUMENT_BYTES) {
|
|
27
|
+
return { findings: [error("too-large", "", `a document may be at most ${MAX_DOCUMENT_BYTES / 1024} KB`)] };
|
|
28
|
+
}
|
|
29
|
+
// An older format is upgraded before it is read (upgrade.ts); the host records that as a new version.
|
|
30
|
+
try {
|
|
31
|
+
raw = upgradeDocument(raw).document;
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
return { findings: [error("format", "formatVersion", e.message)] };
|
|
35
|
+
}
|
|
36
|
+
const parsed = DocumentSpec.safeParse(raw);
|
|
37
|
+
if (!parsed.success) {
|
|
38
|
+
return {
|
|
39
|
+
findings: parsed.error.issues.map((issue) => {
|
|
40
|
+
const path = issue.path.map(String).join(".");
|
|
41
|
+
if (issue.code === "unrecognized_keys")
|
|
42
|
+
return error("unknown-key", path, `${issue.keys.map((k) => `"${k}"`).join(", ")} ${issue.keys.length === 1 ? "is" : "are"} not part of a Graview document here`, "check the spelling against docs/declaration-document.md");
|
|
43
|
+
if (issue.code === "invalid_union" && /\.effects\.\d+$/.test(path)) {
|
|
44
|
+
return error("effect-shape", path, "this effect is not one Graview knows", 'an effect is one of {"create": kind, "as"?: name, "set"?: {field: value}}, {"set": {field: value, …}, "target"?: "$ref"}, {"connect"|"sever": relation, "from": "$ref", "to": "$ref"}, {"remove": "$ref"}');
|
|
45
|
+
}
|
|
46
|
+
if (issue.code === "invalid_key") {
|
|
47
|
+
const inner = issue.issues?.[0]?.message;
|
|
48
|
+
return error("shape", path, `the name "${String(issue.path.at(-1))}" is not allowed here${inner ? `: ${inner}` : ""}`);
|
|
49
|
+
}
|
|
50
|
+
return error("shape", path, issue.message);
|
|
51
|
+
}),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const document = parsed.data;
|
|
55
|
+
return { document, findings: validate(document) };
|
|
56
|
+
}
|
|
57
|
+
const asArray = (v) => (v === undefined ? [] : Array.isArray(v) ? v : [v]);
|
|
58
|
+
function edgeIndex(document) {
|
|
59
|
+
const edges = new Map();
|
|
60
|
+
for (const [kind, spec] of Object.entries(document.kinds)) {
|
|
61
|
+
for (const [name, edge] of Object.entries(spec.edges ?? {})) {
|
|
62
|
+
const prior = edges.get(name);
|
|
63
|
+
edges.set(name, {
|
|
64
|
+
name,
|
|
65
|
+
from: [...(prior?.from ?? []), kind],
|
|
66
|
+
to: edge.to === "*" || prior?.to === "*" ? "*" : [...new Set([...(prior?.to ?? []), ...edge.to])],
|
|
67
|
+
cardinality: edge.cardinality ?? "many",
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return edges;
|
|
72
|
+
}
|
|
73
|
+
export function kindShapes(document) {
|
|
74
|
+
const shapes = new Map();
|
|
75
|
+
for (const [kind, spec] of Object.entries(document.kinds)) {
|
|
76
|
+
shapes.set(kind, {
|
|
77
|
+
fields: new Set(Object.keys(spec.fields)),
|
|
78
|
+
edges: new Map(Object.entries(spec.edges ?? {}).map(([name, e]) => [name, e.cardinality ?? "many"])),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return shapes;
|
|
82
|
+
}
|
|
83
|
+
/** The effects an act's shorthands mean, in order. */
|
|
84
|
+
export function effectsOf(act, document) {
|
|
85
|
+
// Shorthands first, then any listed effects: an act may say "writes quote" and also set a status.
|
|
86
|
+
const effects = [];
|
|
87
|
+
if (act.creates) {
|
|
88
|
+
const kind = document.kinds[act.creates];
|
|
89
|
+
const set = {};
|
|
90
|
+
for (const field of Object.keys(kind?.fields ?? {}))
|
|
91
|
+
set[field] = `$${field}`;
|
|
92
|
+
// Fields a later effect fills in on the new record are part of making it, not questions to ask.
|
|
93
|
+
for (const e of act.effects ?? [])
|
|
94
|
+
if ("set" in e && !("create" in e) && refName(e.target) === "new")
|
|
95
|
+
Object.assign(set, e.set);
|
|
96
|
+
effects.push({ create: act.creates, as: "new", set });
|
|
97
|
+
}
|
|
98
|
+
if (act.sets)
|
|
99
|
+
effects.push({ set: act.sets });
|
|
100
|
+
// An act that makes a record already asks for each of its fields: `writes` beside `creates` (and no `on`) names some of them again, not a subject.
|
|
101
|
+
if (act.writes && !(act.creates && act.on === undefined))
|
|
102
|
+
effects.push({ set: Object.fromEntries(act.writes.map((f) => [f, `$${f}`])) });
|
|
103
|
+
if (act.connects)
|
|
104
|
+
effects.push({ connect: act.connects, from: "$subject", to: "$to" });
|
|
105
|
+
if (act.severs)
|
|
106
|
+
effects.push({ sever: act.severs, from: "$subject", to: "$to" });
|
|
107
|
+
if (act.removes)
|
|
108
|
+
effects.push({ remove: "$subject" });
|
|
109
|
+
if (act.effects)
|
|
110
|
+
effects.push(...act.effects.filter((e) => !(act.creates && "set" in e && !("create" in e) && refName(e.target) === "new")));
|
|
111
|
+
return effects;
|
|
112
|
+
}
|
|
113
|
+
const refName = (v) => (typeof v === "string" && /^\$[A-Za-z][A-Za-z0-9]*$/.test(v) ? v.slice(1) : undefined);
|
|
114
|
+
function tryExpr(source, path, findings) {
|
|
115
|
+
try {
|
|
116
|
+
const expr = parseExpr(source);
|
|
117
|
+
const shape = analyzeExpr(expr);
|
|
118
|
+
for (const fn of shape.unknownFunctions)
|
|
119
|
+
findings.push(error("unknown-function", path, `"${fn}" is not a function the rule language knows`, "see docs/declaration-document.md#expression-language"));
|
|
120
|
+
return expr;
|
|
121
|
+
}
|
|
122
|
+
catch (e) {
|
|
123
|
+
if (e instanceof ExprSyntaxError) {
|
|
124
|
+
findings.push(error("expression", path, e.sentence + ` (at character ${e.at + 1})`));
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
throw e;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function tryTemplate(source, path, findings) {
|
|
131
|
+
try {
|
|
132
|
+
return parseTemplate(source);
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
if (e instanceof TemplateError || e instanceof ExprSyntaxError) {
|
|
136
|
+
findings.push(error("template", path, e instanceof ExprSyntaxError ? e.sentence : e.sentence));
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
throw e;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** The document's own sentences: references that do not resolve, expressions that do not parse. */
|
|
143
|
+
function validate(document) {
|
|
144
|
+
const findings = [];
|
|
145
|
+
const kinds = new Set(Object.keys(document.kinds));
|
|
146
|
+
const edges = edgeIndex(document);
|
|
147
|
+
const acts = document.acts ?? {};
|
|
148
|
+
const roles = new Set(document.roles ?? []);
|
|
149
|
+
for (const [kind, spec] of Object.entries(document.kinds)) {
|
|
150
|
+
const at = `kinds.${kind}`;
|
|
151
|
+
for (const [name, edge] of Object.entries(spec.edges ?? {})) {
|
|
152
|
+
if (edge.to !== "*")
|
|
153
|
+
for (const target of edge.to)
|
|
154
|
+
if (!kinds.has(target))
|
|
155
|
+
findings.push(error("edge-target", `${at}.edges.${name}.to`, `"${target}" is not a kind this document declares`));
|
|
156
|
+
if (spec.fields[name])
|
|
157
|
+
findings.push(error("edge-field-clash", `${at}.edges.${name}`, `"${name}" is both a field and a relation of ${kind}`, "rename one of them"));
|
|
158
|
+
}
|
|
159
|
+
if (spec.lifecycle && !spec.fields[spec.lifecycle.field])
|
|
160
|
+
findings.push(error("lifecycle-field", `${at}.lifecycle.field`, `${kind} has no field "${spec.lifecycle.field}"`));
|
|
161
|
+
for (const field of spec.glance ?? []) {
|
|
162
|
+
if (!spec.fields[field])
|
|
163
|
+
findings.push(error("glance-field", `${at}.glance`, `a glance at ${kind} is to say "${field}", and ${kind} has no field called that`, `use one of: ${Object.keys(spec.fields).join(", ")}`));
|
|
164
|
+
}
|
|
165
|
+
for (const key of ["label", "describe"]) {
|
|
166
|
+
const source = spec[key];
|
|
167
|
+
if (!source)
|
|
168
|
+
continue;
|
|
169
|
+
const parts = tryTemplate(source, `${at}.${key}`, findings);
|
|
170
|
+
for (const part of parts ?? []) {
|
|
171
|
+
if (!part.expr)
|
|
172
|
+
continue;
|
|
173
|
+
for (const name of analyzeExpr(part.expr).names) {
|
|
174
|
+
if (!spec.fields[name])
|
|
175
|
+
findings.push(error("template-field", `${at}.${key}`, `${kind} has no field "${name}" for its ${key} to show`, key === "label" ? "a label can only show the record's own fields" : undefined));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
for (const [field, f] of Object.entries(spec.fields)) {
|
|
180
|
+
if (f.default !== undefined && f.type === "enum" && !f.options?.includes(String(f.default))) {
|
|
181
|
+
findings.push(error("default-option", `${at}.fields.${field}.default`, `"${String(f.default)}" is not one of ${field}'s options`));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
for (const [name, act] of Object.entries(acts)) {
|
|
186
|
+
const at = `acts.${name}`;
|
|
187
|
+
for (const kind of asArray(act.on))
|
|
188
|
+
if (!kinds.has(kind))
|
|
189
|
+
findings.push(error("act-kind", `${at}.on`, `"${kind}" is not a kind this document declares`));
|
|
190
|
+
const subjectKinds = asArray(act.on);
|
|
191
|
+
const effects = effectsOf(act, document);
|
|
192
|
+
if (act.creates && act.on === undefined && act.writes && kinds.has(act.creates)) {
|
|
193
|
+
for (const field of act.writes)
|
|
194
|
+
if (!document.kinds[act.creates].fields[field])
|
|
195
|
+
findings.push(error("act-field", `${at}.writes`, `${act.creates} has no field "${field}"`));
|
|
196
|
+
}
|
|
197
|
+
if (effects.length === 0)
|
|
198
|
+
findings.push(error("act-empty", at, `"${name}" does nothing`, "give it effects or a shorthand such as sets or creates"));
|
|
199
|
+
const created = new Map();
|
|
200
|
+
effects.forEach((effect, i) => {
|
|
201
|
+
const shorthands = effects.length - (act.effects?.length ?? 0);
|
|
202
|
+
const where = act.effects && i >= shorthands ? `${at}.effects.${i - shorthands}` : at;
|
|
203
|
+
if ("create" in effect) {
|
|
204
|
+
if (!kinds.has(effect.create))
|
|
205
|
+
findings.push(error("act-kind", `${where}.create`, `"${effect.create}" is not a kind this document declares`));
|
|
206
|
+
else
|
|
207
|
+
for (const field of Object.keys(effect.set ?? {}))
|
|
208
|
+
if (!document.kinds[effect.create].fields[field])
|
|
209
|
+
findings.push(error("act-field", `${where}.set.${field}`, `${effect.create} has no field "${field}"`));
|
|
210
|
+
if (effect.as)
|
|
211
|
+
created.set(effect.as, effect.create);
|
|
212
|
+
}
|
|
213
|
+
else if ("connect" in effect || "sever" in effect) {
|
|
214
|
+
const edgeName = "connect" in effect ? effect.connect : effect.sever;
|
|
215
|
+
if (!edges.has(edgeName))
|
|
216
|
+
findings.push(error("act-edge", `${where}.${"connect" in effect ? "connect" : "sever"}`, `"${edgeName}" is not a relation any kind declares`));
|
|
217
|
+
if (refName(effect.from) === "subject" && subjectKinds.length === 0)
|
|
218
|
+
findings.push(error("act-subject", `${at}.on`, `"${name}" acts on "$subject" but does not say which kinds it is "on"`));
|
|
219
|
+
}
|
|
220
|
+
else if ("set" in effect) {
|
|
221
|
+
const target = refName(effect.target ?? "$subject");
|
|
222
|
+
const targetKinds = target === "subject" ? subjectKinds : created.has(target ?? "") ? [created.get(target)] : [];
|
|
223
|
+
if (target === "subject" && subjectKinds.length === 0)
|
|
224
|
+
findings.push(error("act-subject", `${at}.on`, `"${name}" changes "$subject" but does not say which kinds it is "on"`));
|
|
225
|
+
for (const field of Object.keys(effect.set)) {
|
|
226
|
+
for (const kind of targetKinds)
|
|
227
|
+
if (kinds.has(kind) && !document.kinds[kind].fields[field])
|
|
228
|
+
findings.push(error("act-field", `${where}.set.${field}`, `${kind} has no field "${field}"`));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
else if ("remove" in effect) {
|
|
232
|
+
if (refName(effect.remove) === "subject" && subjectKinds.length === 0)
|
|
233
|
+
findings.push(error("act-subject", `${at}.on`, `"${name}" removes "$subject" but does not say which kinds it is "on"`));
|
|
234
|
+
}
|
|
235
|
+
const values = "set" in effect && effect.set ? Object.values(effect.set) : [];
|
|
236
|
+
values.forEach((v) => {
|
|
237
|
+
if (v && typeof v === "object" && !Array.isArray(v) && "expr" in v)
|
|
238
|
+
tryExpr(v.expr, `${where}.set`, findings);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
if (act.allowedWhen)
|
|
242
|
+
tryExpr(act.allowedWhen, `${at}.allowedWhen`, findings);
|
|
243
|
+
if (act.refusal)
|
|
244
|
+
tryTemplate(act.refusal, `${at}.refusal`, findings);
|
|
245
|
+
}
|
|
246
|
+
for (const [name, rule] of Object.entries(document.rules ?? {})) {
|
|
247
|
+
const at = `rules.${name}`;
|
|
248
|
+
if (rule.over !== "graph" && !kinds.has(rule.over))
|
|
249
|
+
findings.push(error("rule-kind", `${at}.over`, `"${rule.over}" is not a kind this document declares`));
|
|
250
|
+
const spec = rule.over === "graph" ? undefined : document.kinds[rule.over];
|
|
251
|
+
for (const key of ["when", "require"]) {
|
|
252
|
+
const source = rule[key];
|
|
253
|
+
if (!source)
|
|
254
|
+
continue;
|
|
255
|
+
const expr = tryExpr(source, `${at}.${key}`, findings);
|
|
256
|
+
if (!expr)
|
|
257
|
+
continue;
|
|
258
|
+
const shape = analyzeExpr(expr);
|
|
259
|
+
for (const n of shape.names) {
|
|
260
|
+
if (rule.over === "graph")
|
|
261
|
+
findings.push(error("rule-name", `${at}.${key}`, `"${n}" means nothing in a rule over the whole graph`, "name a kind in \"over\", or use all('kind')"));
|
|
262
|
+
else if (spec && !spec.fields[n] && !spec.edges?.[n]) {
|
|
263
|
+
const hyphenated = Object.keys(spec.edges ?? {}).find((e) => e.includes("-") && e.split("-").includes(n));
|
|
264
|
+
findings.push(error("rule-name", `${at}.${key}`, `${rule.over} has no field or relation called "${n}"`, hyphenated ? `"${hyphenated}" reads as a subtraction in a rule; rename the relation to one camelCase word, like "${hyphenated.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase())}"` : undefined));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
for (const e of shape.edges)
|
|
268
|
+
if (!edges.has(e))
|
|
269
|
+
findings.push(error("rule-edge", `${at}.${key}`, `"${e}" is not a relation any kind declares`));
|
|
270
|
+
for (const k of shape.sweeps) {
|
|
271
|
+
if (!kinds.has(k))
|
|
272
|
+
findings.push(error("rule-kind", `${at}.${key}`, `"${k}" is not a kind this document declares`));
|
|
273
|
+
else if (rule.over !== "graph")
|
|
274
|
+
findings.push(warning("rule-cost", `${at}.${key}`, `this rule looks at every ${k} for every ${rule.over}, which grows slowly as the app grows`, "walk a relation with out()/in() instead of all() where you can"));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (rule.says)
|
|
278
|
+
tryTemplate(rule.says, `${at}.says`, findings);
|
|
279
|
+
(rule.repairs ?? []).forEach((repair, i) => {
|
|
280
|
+
if (!acts[repair.act] && !/^(edit|remove)-/.test(repair.act))
|
|
281
|
+
findings.push(error("repair-act", `${at}.repairs.${i}.act`, `"${repair.act}" is not an act this document declares`));
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
const policy = document.policy;
|
|
285
|
+
if (policy) {
|
|
286
|
+
policy.grants.forEach((grant, i) => {
|
|
287
|
+
if (grant.roles !== "*" && roles.size > 0)
|
|
288
|
+
for (const r of grant.roles)
|
|
289
|
+
if (!roles.has(r))
|
|
290
|
+
findings.push(error("grant-role", `policy.grants.${i}.roles`, `"${r}" is not one of the document's roles`));
|
|
291
|
+
if (grant.mutations !== "*")
|
|
292
|
+
for (const m of grant.mutations)
|
|
293
|
+
if (!acts[m] && !/^(edit|remove)-/.test(m))
|
|
294
|
+
findings.push(error("grant-act", `policy.grants.${i}.mutations`, `"${m}" is not an act this document declares`));
|
|
295
|
+
});
|
|
296
|
+
(policy.sees ?? []).forEach((sight, i) => {
|
|
297
|
+
for (const k of sight.kinds)
|
|
298
|
+
if (!kinds.has(k))
|
|
299
|
+
findings.push(error("sight-kind", `policy.sees.${i}.kinds`, `"${k}" is not a kind this document declares`));
|
|
300
|
+
if (sight.roles !== "*" && roles.size > 0)
|
|
301
|
+
for (const r of sight.roles)
|
|
302
|
+
if (!roles.has(r))
|
|
303
|
+
findings.push(error("sight-role", `policy.sees.${i}.roles`, `"${r}" is not one of the document's roles`));
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
findings.push(...validateViews(document));
|
|
307
|
+
return findings;
|
|
308
|
+
}
|
|
309
|
+
function fieldSchema(spec, optional) {
|
|
310
|
+
let schema;
|
|
311
|
+
switch (spec.type) {
|
|
312
|
+
case "string":
|
|
313
|
+
schema = z.string().max(500);
|
|
314
|
+
break;
|
|
315
|
+
case "text":
|
|
316
|
+
schema = z.string().max(20_000);
|
|
317
|
+
break;
|
|
318
|
+
case "number":
|
|
319
|
+
schema = z.number();
|
|
320
|
+
break;
|
|
321
|
+
case "integer":
|
|
322
|
+
schema = z.number().int();
|
|
323
|
+
break;
|
|
324
|
+
case "boolean":
|
|
325
|
+
schema = z.boolean();
|
|
326
|
+
break;
|
|
327
|
+
case "date":
|
|
328
|
+
schema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "a date like 2026-10-02");
|
|
329
|
+
break;
|
|
330
|
+
case "datetime":
|
|
331
|
+
schema = z.string().refine((s) => !Number.isNaN(Date.parse(s)), "a date and time like 2026-10-02T14:30:00Z");
|
|
332
|
+
break;
|
|
333
|
+
case "enum":
|
|
334
|
+
schema = z.enum(spec.options);
|
|
335
|
+
break;
|
|
336
|
+
case "list":
|
|
337
|
+
schema = z.array(spec.of === "number" ? z.number() : spec.of === "date" ? z.string().regex(/^\d{4}-\d{2}-\d{2}$/) : z.string().max(500)).max(200);
|
|
338
|
+
break;
|
|
339
|
+
case "url":
|
|
340
|
+
schema = z.url();
|
|
341
|
+
break;
|
|
342
|
+
case "email":
|
|
343
|
+
schema = z.email();
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
if (spec.description)
|
|
347
|
+
schema = schema.describe(spec.description);
|
|
348
|
+
return optional ? schema.optional() : schema;
|
|
349
|
+
}
|
|
350
|
+
/** What one of these is called, when the kind says nothing: the first required word-ish field. */
|
|
351
|
+
function defaultLabelField(spec) {
|
|
352
|
+
const entries = Object.entries(spec.fields);
|
|
353
|
+
return (entries.find(([, f]) => f.required && (f.type === "string" || f.type === "text"))?.[0] ??
|
|
354
|
+
entries.find(([, f]) => f.type === "string")?.[0]);
|
|
355
|
+
}
|
|
356
|
+
/** The arguments an act asks for: declared ones, then whatever its effects reference. */
|
|
357
|
+
function argsOf(name, act, effects, document, edges) {
|
|
358
|
+
const args = new Map();
|
|
359
|
+
const subjectKinds = asArray(act.on);
|
|
360
|
+
const created = new Map();
|
|
361
|
+
const usesSubject = subjectKinds.length > 0 ||
|
|
362
|
+
effects.some((e) => ("remove" in e && refName(e.remove) === "subject") || ("from" in e && (refName(e.from) === "subject" || refName(e.to) === "subject")) || ("set" in e && !("create" in e) && refName(e.target ?? "$subject") === "subject"));
|
|
363
|
+
if (usesSubject)
|
|
364
|
+
args.set(SUBJECT_ARG, { schema: nodeRef(subjectKinds.length > 0 ? subjectKinds : "*"), required: true });
|
|
365
|
+
const declared = act.args ?? {};
|
|
366
|
+
const writesOnlyOne = act.writes?.length === 1;
|
|
367
|
+
const note = (arg, schema) => {
|
|
368
|
+
if (arg === SUBJECT_ARG || BUILTIN_REFS.has(arg) || created.has(arg) || args.has(arg))
|
|
369
|
+
return;
|
|
370
|
+
const d = declared[arg];
|
|
371
|
+
args.set(arg, d ? { schema: fieldSchema(d, !d.required), required: Boolean(d.required) } : schema());
|
|
372
|
+
};
|
|
373
|
+
for (const effect of effects) {
|
|
374
|
+
if ("create" in effect) {
|
|
375
|
+
const kind = document.kinds[effect.create];
|
|
376
|
+
for (const [field, value] of Object.entries(effect.set ?? {})) {
|
|
377
|
+
const arg = refName(value);
|
|
378
|
+
const spec = kind?.fields[field];
|
|
379
|
+
if (arg && spec) {
|
|
380
|
+
const required = Boolean(spec.required) && spec.default === undefined;
|
|
381
|
+
note(arg, () => ({ schema: fieldSchema(spec, !required), required }));
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (effect.as)
|
|
385
|
+
created.set(effect.as, effect.create);
|
|
386
|
+
}
|
|
387
|
+
else if ("connect" in effect || "sever" in effect) {
|
|
388
|
+
const edge = edges.get("connect" in effect ? effect.connect : effect.sever);
|
|
389
|
+
for (const [end, value] of [["from", effect.from], ["to", effect.to]]) {
|
|
390
|
+
const arg = refName(value);
|
|
391
|
+
if (!arg)
|
|
392
|
+
continue;
|
|
393
|
+
const kinds = end === "to" ? edge?.to ?? "*" : edge?.from ?? "*";
|
|
394
|
+
note(arg, () => ({ schema: nodeRef(kinds === "*" ? "*" : kinds), required: true }));
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
else if ("set" in effect) {
|
|
398
|
+
const target = refName(effect.target ?? "$subject");
|
|
399
|
+
const targetKind = target === "subject" ? subjectKinds[0] : created.get(target ?? "");
|
|
400
|
+
if (target && target !== "subject" && !created.has(target))
|
|
401
|
+
note(target, () => ({ schema: nodeRef("*"), required: true }));
|
|
402
|
+
for (const [field, value] of Object.entries(effect.set)) {
|
|
403
|
+
const arg = refName(value);
|
|
404
|
+
if (!arg)
|
|
405
|
+
continue;
|
|
406
|
+
const spec = targetKind ? document.kinds[targetKind]?.fields[field] : undefined;
|
|
407
|
+
note(arg, () => (spec ? { schema: fieldSchema(spec, !writesOnlyOne), required: writesOnlyOne } : { schema: z.unknown().optional(), required: false }));
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
else if ("remove" in effect) {
|
|
411
|
+
const arg = refName(effect.remove);
|
|
412
|
+
if (arg)
|
|
413
|
+
note(arg, () => ({ schema: nodeRef("*"), required: true }));
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
for (const [arg, d] of Object.entries(declared))
|
|
417
|
+
if (!args.has(arg))
|
|
418
|
+
args.set(arg, { schema: fieldSchema(d, !d.required), required: Boolean(d.required) });
|
|
419
|
+
void name;
|
|
420
|
+
return args;
|
|
421
|
+
}
|
|
422
|
+
class Refused extends Error {
|
|
423
|
+
}
|
|
424
|
+
export function compileDocument(raw, options = {}) {
|
|
425
|
+
const read = readDocument(raw);
|
|
426
|
+
if (!read.document || hasErrors(read.findings))
|
|
427
|
+
return { ok: false, findings: read.findings };
|
|
428
|
+
const document = read.document;
|
|
429
|
+
const findings = [...read.findings];
|
|
430
|
+
if (options.previous)
|
|
431
|
+
findings.push(...renamesFromNothing(document, options.previous));
|
|
432
|
+
const today = options.today ?? (() => new Date().toISOString().slice(0, 10));
|
|
433
|
+
const shapes = kindShapes(document);
|
|
434
|
+
const edges = edgeIndex(document);
|
|
435
|
+
// ── kinds ────────────────────────────────────────────────────────────────
|
|
436
|
+
const definitions = Object.entries(document.kinds).map(([kind, spec]) => {
|
|
437
|
+
const shape = {};
|
|
438
|
+
for (const [field, f] of Object.entries(spec.fields))
|
|
439
|
+
shape[field] = fieldSchema(f, !f.required);
|
|
440
|
+
const labelSource = spec.label ?? (defaultLabelField(spec) ? `{${defaultLabelField(spec)}}` : undefined);
|
|
441
|
+
const labelParts = labelSource ? parseTemplate(labelSource) : undefined;
|
|
442
|
+
const describeParts = spec.describe ? parseTemplate(spec.describe) : undefined;
|
|
443
|
+
const labels = Object.fromEntries(Object.entries(spec.fields).filter(([, f]) => f.label).map(([n, f]) => [n, f.label]));
|
|
444
|
+
// Defaults stay out of the zod schema (hydration must not invent values) and ride beside it, for a repair to read.
|
|
445
|
+
const defaults = Object.fromEntries(Object.entries(spec.fields).filter(([, f]) => f.default !== undefined).map(([n, f]) => [n, f.default]));
|
|
446
|
+
const edgeDecls = Object.fromEntries(Object.entries(spec.edges ?? {}).map(([n, e]) => [
|
|
447
|
+
n,
|
|
448
|
+
{
|
|
449
|
+
to: e.to,
|
|
450
|
+
...(e.cardinality ? { cardinality: e.cardinality } : {}),
|
|
451
|
+
...(e.description ? { description: e.description } : {}),
|
|
452
|
+
...(e.inverse ? { inverse: e.inverse } : {}),
|
|
453
|
+
...(e.appendOnly ? { appendOnly: e.appendOnly } : {}),
|
|
454
|
+
},
|
|
455
|
+
]));
|
|
456
|
+
return defineNode(kind, {
|
|
457
|
+
fields: z.object(shape),
|
|
458
|
+
edges: edgeDecls,
|
|
459
|
+
...(spec.plural ? { plural: spec.plural } : {}),
|
|
460
|
+
...(spec.noun ? { noun: spec.noun } : {}),
|
|
461
|
+
...(spec.description ? { description: spec.description } : {}),
|
|
462
|
+
...(labelParts ? { label: (node) => renderTemplate(labelParts, { node: node, kinds: shapes, today: today() }) || node.id } : {}),
|
|
463
|
+
...(describeParts ? { describe: (node) => renderTemplate(describeParts, { node: node, kinds: shapes, today: today() }) } : {}),
|
|
464
|
+
...(spec.lifecycle ? { lifecycle: { field: spec.lifecycle.field, retired: spec.lifecycle.retired } } : {}),
|
|
465
|
+
...(spec.figure ? { figure: spec.figure } : {}),
|
|
466
|
+
...(Object.keys(labels).length > 0 || spec.glance ? { display: { ...(Object.keys(labels).length > 0 ? { labels } : {}), ...(spec.glance ? { glance: [...spec.glance] } : {}) } } : {}),
|
|
467
|
+
...(Object.keys(defaults).length > 0 ? { defaults } : {}),
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
const schema = createSchema(definitions);
|
|
471
|
+
// ── acts ─────────────────────────────────────────────────────────────────
|
|
472
|
+
const mutations = [];
|
|
473
|
+
for (const [name, act] of Object.entries(document.acts ?? {})) {
|
|
474
|
+
const effects = effectsOf(act, document);
|
|
475
|
+
const args = argsOf(name, act, effects, document, edges);
|
|
476
|
+
const shape = {};
|
|
477
|
+
for (const [arg, a] of args)
|
|
478
|
+
shape[arg] = a.schema;
|
|
479
|
+
const subjectKinds = asArray(act.on);
|
|
480
|
+
const title = act.title ?? name.replace(/-/g, " ");
|
|
481
|
+
const guard = act.allowedWhen ? parseExpr(act.allowedWhen) : undefined;
|
|
482
|
+
const refusal = act.refusal ? parseTemplate(act.refusal) : undefined;
|
|
483
|
+
const destructive = act.destructive ?? effects.some((e) => "remove" in e || "sever" in e);
|
|
484
|
+
const creates = [...new Set(effects.flatMap((e) => ("create" in e ? [e.create] : [])))];
|
|
485
|
+
const connects = [...new Set(effects.flatMap((e) => ("connect" in e ? [e.connect] : [])))];
|
|
486
|
+
const severs = [...new Set(effects.flatMap((e) => ("sever" in e ? [e.sever] : [])))];
|
|
487
|
+
// An act WRITES only what it sets on its subject; setting fields on a record it just made is part of making it.
|
|
488
|
+
const writes = [...new Set(effects.flatMap((e) => ("set" in e && !("create" in e) && (refName(e.target ?? "$subject") === "subject") ? Object.keys(e.set) : [])))];
|
|
489
|
+
/*
|
|
490
|
+
* Twice is once when nothing is made and every value set is given
|
|
491
|
+
* (a literal or an argument), never computed from what is there.
|
|
492
|
+
*/
|
|
493
|
+
const idempotent = creates.length === 0 &&
|
|
494
|
+
effects.every((e) => !("set" in e) || !e.set || Object.values(e.set).every((v) => !(v && typeof v === "object" && !Array.isArray(v) && "expr" in v)));
|
|
495
|
+
const exprs = new Map();
|
|
496
|
+
for (const effect of effects) {
|
|
497
|
+
if (!("set" in effect) || !effect.set)
|
|
498
|
+
continue;
|
|
499
|
+
for (const v of Object.values(effect.set))
|
|
500
|
+
if (v && typeof v === "object" && !Array.isArray(v) && "expr" in v)
|
|
501
|
+
exprs.set(v.expr, parseExpr(v.expr));
|
|
502
|
+
}
|
|
503
|
+
mutations.push(defineMutation(name, {
|
|
504
|
+
input: z.object(shape),
|
|
505
|
+
title,
|
|
506
|
+
...(act.description ? { description: act.description } : {}),
|
|
507
|
+
...(act.fromTheOtherEnd ? { fromTheOtherEnd: act.fromTheOtherEnd } : {}),
|
|
508
|
+
...(args.has(SUBJECT_ARG) ? { subject: { kinds: subjectKinds.length > 0 ? subjectKinds : "*", arg: SUBJECT_ARG } } : {}),
|
|
509
|
+
...(destructive ? { destructive: true } : {}),
|
|
510
|
+
...(idempotent ? { idempotent: true } : {}),
|
|
511
|
+
...(creates.length > 0 ? { creates } : {}),
|
|
512
|
+
...(connects.length > 0 ? { connects } : {}),
|
|
513
|
+
...(severs.length > 0 ? { severs } : {}),
|
|
514
|
+
...(writes.length > 0 ? { writes } : {}),
|
|
515
|
+
describe: (input, graph) => {
|
|
516
|
+
const subject = typeof input[SUBJECT_ARG] === "string" ? graph.getNode(input[SUBJECT_ARG]) : undefined;
|
|
517
|
+
const what = subject ? labelFor(subject) : Object.values(input).find((v) => typeof v === "string");
|
|
518
|
+
return what ? `${title}: ${String(what)}` : title;
|
|
519
|
+
},
|
|
520
|
+
apply(context, input) {
|
|
521
|
+
const graph = context.graph;
|
|
522
|
+
const subjectId = typeof input[SUBJECT_ARG] === "string" ? input[SUBJECT_ARG] : undefined;
|
|
523
|
+
const subject = subjectId ? graph.getNode(subjectId) : undefined;
|
|
524
|
+
if (subjectId && !subject)
|
|
525
|
+
throw new Refused(`there is no record "${subjectId}"`);
|
|
526
|
+
if (subject && subjectKinds.length > 0 && !subjectKinds.includes(subject.kind))
|
|
527
|
+
throw new Refused(`"${title}" acts on ${subjectKinds.join(" or ")}, not on ${withArticle(subject.kind)}`);
|
|
528
|
+
const bindings = {};
|
|
529
|
+
for (const [k, v] of Object.entries(input))
|
|
530
|
+
if (v !== undefined)
|
|
531
|
+
bindings[k] = v;
|
|
532
|
+
if (guard && subject) {
|
|
533
|
+
let allowed;
|
|
534
|
+
try {
|
|
535
|
+
allowed = evaluateExpr(guard, { graph, subject, kinds: shapes, bindings, today: today() });
|
|
536
|
+
}
|
|
537
|
+
catch (e) {
|
|
538
|
+
if (e instanceof ExprEvalError)
|
|
539
|
+
throw new Refused(`"${title}" could not be judged: ${e.sentence}`);
|
|
540
|
+
throw e;
|
|
541
|
+
}
|
|
542
|
+
if (allowed !== true) {
|
|
543
|
+
throw new Refused(refusal ? renderTemplate(refusal, { node: subject, kinds: shapes, graph, bindings, today: today() }) : `"${title}" is not allowed for ${labelFor(subject)} right now`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
const made = new Map();
|
|
547
|
+
const resolve = (value, current) => {
|
|
548
|
+
if (value && typeof value === "object" && !Array.isArray(value) && "expr" in value) {
|
|
549
|
+
try {
|
|
550
|
+
return evaluateExpr(exprs.get(value.expr), { graph, subject: current ?? subject ?? null, kinds: shapes, bindings, today: today() });
|
|
551
|
+
}
|
|
552
|
+
catch (e) {
|
|
553
|
+
if (e instanceof ExprEvalError)
|
|
554
|
+
throw new Refused(`"${title}" could not work out a value: ${e.sentence}`);
|
|
555
|
+
throw e;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
const ref = refName(value);
|
|
559
|
+
if (ref === undefined)
|
|
560
|
+
return typeof value === "string" && value.startsWith("$$") ? value.slice(1) : value;
|
|
561
|
+
if (ref === "subject")
|
|
562
|
+
return subjectId;
|
|
563
|
+
if (ref === "now")
|
|
564
|
+
return new Date().toISOString();
|
|
565
|
+
if (ref === "today")
|
|
566
|
+
return today();
|
|
567
|
+
if (made.has(ref))
|
|
568
|
+
return made.get(ref);
|
|
569
|
+
return input[ref];
|
|
570
|
+
};
|
|
571
|
+
for (const effect of effects) {
|
|
572
|
+
if ("create" in effect) {
|
|
573
|
+
const kind = document.kinds[effect.create];
|
|
574
|
+
const node = { kind: effect.create };
|
|
575
|
+
for (const [field, f] of Object.entries(kind.fields))
|
|
576
|
+
if (f.default !== undefined)
|
|
577
|
+
node[field] = f.default;
|
|
578
|
+
for (const [field, value] of Object.entries(effect.set ?? {})) {
|
|
579
|
+
const v = resolve(value, undefined);
|
|
580
|
+
if (v !== undefined)
|
|
581
|
+
node[field] = v;
|
|
582
|
+
}
|
|
583
|
+
const labelField = defaultLabelField(kind);
|
|
584
|
+
const id = context.freshId(String((labelField && node[labelField]) ?? effect.create), effect.create);
|
|
585
|
+
context.addNode({ id, ...node });
|
|
586
|
+
if (effect.as)
|
|
587
|
+
made.set(effect.as, id);
|
|
588
|
+
}
|
|
589
|
+
else if ("connect" in effect || "sever" in effect) {
|
|
590
|
+
const kind = "connect" in effect ? effect.connect : effect.sever;
|
|
591
|
+
const from = String(resolve(effect.from, undefined) ?? "");
|
|
592
|
+
const to = String(resolve(effect.to, undefined) ?? "");
|
|
593
|
+
if (!graph.has(from) && ![...made.values()].includes(from))
|
|
594
|
+
throw new Refused(`there is no record "${from}"`);
|
|
595
|
+
if (!graph.has(to) && ![...made.values()].includes(to))
|
|
596
|
+
throw new Refused(`there is no record "${to}"`);
|
|
597
|
+
if ("connect" in effect) {
|
|
598
|
+
if (edges.get(kind)?.cardinality === "one")
|
|
599
|
+
for (const existing of graph.out(from, kind))
|
|
600
|
+
if (existing.id !== to)
|
|
601
|
+
context.removeEdge({ kind, from, to: existing.id });
|
|
602
|
+
if (!graph.out(from, kind).some((n) => n.id === to))
|
|
603
|
+
context.addEdge({ kind, from, to });
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
context.removeEdge({ kind, from, to });
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
else if ("set" in effect) {
|
|
610
|
+
const targetId = String(resolve(effect.target ?? "$subject", undefined) ?? "");
|
|
611
|
+
const target = graph.getNode(targetId);
|
|
612
|
+
if (!target && ![...made.values()].includes(targetId))
|
|
613
|
+
throw new Refused(`there is no record "${targetId}"`);
|
|
614
|
+
const patch = {};
|
|
615
|
+
for (const [field, value] of Object.entries(effect.set)) {
|
|
616
|
+
const v = resolve(value, target);
|
|
617
|
+
if (v !== undefined)
|
|
618
|
+
patch[field] = v;
|
|
619
|
+
}
|
|
620
|
+
if (Object.keys(patch).length > 0)
|
|
621
|
+
context.patchNode(targetId, patch);
|
|
622
|
+
}
|
|
623
|
+
else if ("remove" in effect) {
|
|
624
|
+
const id = String(resolve(effect.remove, undefined) ?? "");
|
|
625
|
+
if (!graph.has(id))
|
|
626
|
+
throw new Refused(`there is no record "${id}"`);
|
|
627
|
+
context.removeNode(id);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
},
|
|
631
|
+
}));
|
|
632
|
+
}
|
|
633
|
+
function labelFor(node) {
|
|
634
|
+
const definition = schema.tryDefinition(node.kind);
|
|
635
|
+
return definition?.label?.(node) ?? node.id;
|
|
636
|
+
}
|
|
637
|
+
// ── rules ────────────────────────────────────────────────────────────────
|
|
638
|
+
const invariants = [];
|
|
639
|
+
for (const [name, rule] of Object.entries(document.rules ?? {})) {
|
|
640
|
+
const require = parseExpr(rule.require);
|
|
641
|
+
const when = rule.when ? parseExpr(rule.when) : undefined;
|
|
642
|
+
const says = rule.says ? parseTemplate(rule.says) : undefined;
|
|
643
|
+
const title = rule.title ?? name.replace(/-/g, " ");
|
|
644
|
+
const repairs = rule.repairs ?? [];
|
|
645
|
+
const judge = (graph, subject) => {
|
|
646
|
+
const ctx = { graph, subject, kinds: shapes, today: today() };
|
|
647
|
+
let holds;
|
|
648
|
+
/*
|
|
649
|
+
* A RULE THAT CANNOT ANSWER IS THE ENGINE'S TO SAY (FR-29). Out of
|
|
650
|
+
* budget it throws RuleBudgetError, and the violation's status is
|
|
651
|
+
* `over-budget`; any other mistake (a word added to a number) is
|
|
652
|
+
* `could-not-judge`. Both name the rule and its subject, and neither
|
|
653
|
+
* is a hang or a crash that takes the other rules down.
|
|
654
|
+
*/
|
|
655
|
+
try {
|
|
656
|
+
if (when && evaluateExpr(when, ctx) !== true)
|
|
657
|
+
return [];
|
|
658
|
+
holds = evaluateExpr(require, ctx) === true;
|
|
659
|
+
}
|
|
660
|
+
catch (e) {
|
|
661
|
+
if (e instanceof ExprBudgetError)
|
|
662
|
+
throw new RuleBudgetError(e.sentence);
|
|
663
|
+
if (e instanceof ExprEvalError)
|
|
664
|
+
throw new Error(e.sentence);
|
|
665
|
+
throw e;
|
|
666
|
+
}
|
|
667
|
+
if (holds)
|
|
668
|
+
return [];
|
|
669
|
+
const repairList = repairs.flatMap((r) => {
|
|
670
|
+
const act = document.acts?.[r.act];
|
|
671
|
+
const args = {};
|
|
672
|
+
if (subject && (act?.on || /^(edit|remove)-/.test(r.act)))
|
|
673
|
+
args[SUBJECT_ARG] = subject.id;
|
|
674
|
+
for (const [k, v] of Object.entries(r.args ?? {}))
|
|
675
|
+
args[k] = refName(v) === "subject" ? subject?.id : v;
|
|
676
|
+
const declared = act ? argsOf(r.act, act, effectsOf(act, document), document, edges) : new Map();
|
|
677
|
+
const missing = [...declared].filter(([k, a]) => a.required && args[k] === undefined).map(([k]) => k);
|
|
678
|
+
return [{ mutation: r.act, label: r.label ?? act?.title ?? r.act.replace(/-/g, " "), args, ...(missing.length > 0 ? { missing } : {}) }];
|
|
679
|
+
});
|
|
680
|
+
return [
|
|
681
|
+
{
|
|
682
|
+
invariant: name,
|
|
683
|
+
...(subject ? { subjectId: subject.id } : {}),
|
|
684
|
+
label: title,
|
|
685
|
+
message: (says ? renderTemplate(says, { node: subject, kinds: shapes, graph, today: today() }) : subject ? `${labelFor(subject)}: ${title}` : title),
|
|
686
|
+
nodeIds: subject ? [subject.id] : [],
|
|
687
|
+
repairs: repairList,
|
|
688
|
+
},
|
|
689
|
+
];
|
|
690
|
+
};
|
|
691
|
+
invariants.push((rule.over === "graph"
|
|
692
|
+
? defineInvariant(name, {
|
|
693
|
+
scope: "graph",
|
|
694
|
+
label: title,
|
|
695
|
+
judgement: { require: rule.require, ...(rule.when ? { when: rule.when } : {}), ...(rule.says ? { says: rule.says } : {}) },
|
|
696
|
+
...(rule.description ? { description: rule.description } : {}),
|
|
697
|
+
...(repairs.length > 0 ? { repairs: repairs.map((r) => r.act) } : {}),
|
|
698
|
+
evaluate: ({ graph }) => judge(graph, null),
|
|
699
|
+
})
|
|
700
|
+
: defineInvariant(name, {
|
|
701
|
+
scope: { kind: rule.over },
|
|
702
|
+
label: title,
|
|
703
|
+
judgement: { require: rule.require, ...(rule.when ? { when: rule.when } : {}), ...(rule.says ? { says: rule.says } : {}) },
|
|
704
|
+
...(rule.description ? { description: rule.description } : {}),
|
|
705
|
+
...(repairs.length > 0 ? { repairs: repairs.map((r) => r.act) } : {}),
|
|
706
|
+
evaluate: ({ graph, subject }) => judge(graph, subject),
|
|
707
|
+
})));
|
|
708
|
+
}
|
|
709
|
+
// ── policy, brand, the rest ──────────────────────────────────────────────
|
|
710
|
+
/*
|
|
711
|
+
* WHO SEES WHAT is the app's, in the framework's one meaning (FR-02):
|
|
712
|
+
* absent, everybody sees everything; present, a kind no sight names is
|
|
713
|
+
* seen by nobody but the system, and `own` is the principal's own records.
|
|
714
|
+
*/
|
|
715
|
+
const policy = document.policy
|
|
716
|
+
? {
|
|
717
|
+
grants: document.policy.grants,
|
|
718
|
+
...(document.policy.sees ? { sees: document.policy.sees } : {}),
|
|
719
|
+
...(document.roles ? { roles: document.roles } : {}),
|
|
720
|
+
}
|
|
721
|
+
: undefined;
|
|
722
|
+
let brand;
|
|
723
|
+
if (document.brand) {
|
|
724
|
+
const derived = brandFromAccent({ accent: document.brand.accent, base: SCHEMES });
|
|
725
|
+
if (derived.ok)
|
|
726
|
+
brand = { name: document.brand.name ?? document.name, schemes: derived.schemes };
|
|
727
|
+
// A colour that cannot be read is not a reason to refuse an app: it wears the default colours and says why.
|
|
728
|
+
else
|
|
729
|
+
findings.push(warning("brand", "brand.accent", `that accent cannot make a readable brand, so the app keeps Graview's colours: ${derived.why}`, "pick a colour further from orange-red, or a darker one"));
|
|
730
|
+
}
|
|
731
|
+
if (hasErrors(findings))
|
|
732
|
+
return { ok: false, findings };
|
|
733
|
+
const app = {
|
|
734
|
+
name: document.name,
|
|
735
|
+
schema,
|
|
736
|
+
mutations,
|
|
737
|
+
invariants,
|
|
738
|
+
...(policy ? { policy } : {}),
|
|
739
|
+
...(brand ? { brand } : {}),
|
|
740
|
+
...(document.modules ? { modules: document.modules } : {}),
|
|
741
|
+
...(document.lenses ? { lenses: document.lenses } : {}),
|
|
742
|
+
...(document.settings ? { settings: document.settings } : {}),
|
|
743
|
+
// The document's views are the declaration's view specs (FR-03): data the framework draws.
|
|
744
|
+
...(document.views && Object.keys(document.views).length > 0 ? { viewSpecs: document.views } : {}),
|
|
745
|
+
version: document.version ?? 1,
|
|
746
|
+
};
|
|
747
|
+
if (!options.skipFrameworkCheck) {
|
|
748
|
+
const check = checkApp(app);
|
|
749
|
+
for (const f of check.findings) {
|
|
750
|
+
const finding = { severity: f.severity === "error" ? "error" : f.severity === "warning" ? "warning" : "note", code: `check:${f.code}`, path: frameworkPath(f.where, document), message: f.message, ...(f.fix ? { fix: f.fix } : {}) };
|
|
751
|
+
findings.push(f.code === "glance-unchosen" ? inDocumentWords(finding, f.where, document) : finding);
|
|
752
|
+
}
|
|
753
|
+
if (hasErrors(findings))
|
|
754
|
+
return { ok: false, findings };
|
|
755
|
+
}
|
|
756
|
+
// `toDocument(app)` gives this document back, byte for byte.
|
|
757
|
+
rememberDocument(app, document);
|
|
758
|
+
return { ok: true, app, document, findings, sights: document.policy?.sees, kinds: shapes };
|
|
759
|
+
}
|
|
760
|
+
/** Map a framework check's "where" (a kind, an act, a rule by name) back to a document path when we can. */
|
|
761
|
+
function frameworkPath(where, document) {
|
|
762
|
+
const word = where.split(/[\s:·]/).find((w) => w.length > 0) ?? where;
|
|
763
|
+
if (document.kinds[word])
|
|
764
|
+
return `kinds.${word}`;
|
|
765
|
+
if (document.acts?.[word])
|
|
766
|
+
return `acts.${word}`;
|
|
767
|
+
if (document.rules?.[word])
|
|
768
|
+
return `rules.${word}`;
|
|
769
|
+
return where;
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* The check tells a TypeScript author to write `display: { glance }`; a
|
|
773
|
+
* document's author writes `kinds.<kind>.glance` (FR-39) — the same choice
|
|
774
|
+
* and the same first three, in the words of the format they hold.
|
|
775
|
+
*/
|
|
776
|
+
function inDocumentWords(finding, where, document) {
|
|
777
|
+
const kind = /^defineNode\("([^"]+)"\)/.exec(where)?.[1];
|
|
778
|
+
const spec = kind ? document.kinds[kind] : undefined;
|
|
779
|
+
if (!kind || !spec)
|
|
780
|
+
return finding;
|
|
781
|
+
const first = Object.keys(spec.fields).filter((field) => field !== "label").slice(0, 3);
|
|
782
|
+
return { ...finding, path: `kinds.${kind}.glance`, fix: `Say which: "glance": [${first.map((field) => `"${field}"`).join(", ")}] on kinds.${kind} — the facts a person compares one by, at a glance.` };
|
|
783
|
+
}
|
|
784
|
+
export { Refused as ActRefusal };
|
|
785
|
+
/** Every `renamedFrom` that names nothing in the version before: the values it was meant to carry would be lost. */
|
|
786
|
+
function renamesFromNothing(document, previous) {
|
|
787
|
+
const out = [];
|
|
788
|
+
const previousEdges = new Set(Object.values(previous.kinds).flatMap((kind) => Object.keys(kind.edges ?? {})));
|
|
789
|
+
for (const [kind, spec] of Object.entries(document.kinds)) {
|
|
790
|
+
const from = spec.renamedFrom;
|
|
791
|
+
if (from && !previous.kinds[from])
|
|
792
|
+
out.push(error("renamed-from-nothing", `kinds.${kind}.renamedFrom`, `${kind} says it was ${from}, and the previous version has no kind called ${from}`, "name the kind it was, or drop renamedFrom"));
|
|
793
|
+
const before = previous.kinds[from ?? kind];
|
|
794
|
+
for (const [field, fieldSpec] of Object.entries(spec.fields)) {
|
|
795
|
+
const was = fieldSpec.renamedFrom;
|
|
796
|
+
if (was && !before?.fields[was])
|
|
797
|
+
out.push(error("renamed-from-nothing", `kinds.${kind}.fields.${field}.renamedFrom`, `${field} says it was ${was}, and the previous ${from ?? kind} has no field called ${was}`, "name the field it was, or drop renamedFrom"));
|
|
798
|
+
}
|
|
799
|
+
for (const [edge, edgeSpec] of Object.entries(spec.edges ?? {})) {
|
|
800
|
+
const was = edgeSpec.renamedFrom;
|
|
801
|
+
if (was && !previousEdges.has(was))
|
|
802
|
+
out.push(error("renamed-from-nothing", `kinds.${kind}.edges.${edge}.renamedFrom`, `${edge} says it was ${was}, and the previous version has no relation called ${was}`, "name the relation it was, or drop renamedFrom"));
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return out;
|
|
806
|
+
}
|
|
807
|
+
//# sourceMappingURL=compile.js.map
|