@graview/core 0.1.0
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 +96 -0
- package/README.md +35 -0
- package/dist/app.d.ts +294 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +49 -0
- package/dist/app.js.map +1 -0
- package/dist/arrange.d.ts +185 -0
- package/dist/arrange.d.ts.map +1 -0
- package/dist/arrange.js +456 -0
- package/dist/arrange.js.map +1 -0
- package/dist/beginning.d.ts +65 -0
- package/dist/beginning.d.ts.map +1 -0
- package/dist/beginning.js +94 -0
- package/dist/beginning.js.map +1 -0
- package/dist/bind.d.ts +42 -0
- package/dist/bind.d.ts.map +1 -0
- package/dist/bind.js +26 -0
- package/dist/bind.js.map +1 -0
- package/dist/city.d.ts +93 -0
- package/dist/city.d.ts.map +1 -0
- package/dist/city.js +160 -0
- package/dist/city.js.map +1 -0
- package/dist/cli/check/arrangement.d.ts +13 -0
- package/dist/cli/check/arrangement.d.ts.map +1 -0
- package/dist/cli/check/arrangement.js +63 -0
- package/dist/cli/check/arrangement.js.map +1 -0
- package/dist/cli/check/beginnings.d.ts +16 -0
- package/dist/cli/check/beginnings.d.ts.map +1 -0
- package/dist/cli/check/beginnings.js +53 -0
- package/dist/cli/check/beginnings.js.map +1 -0
- package/dist/cli/check/brand.d.ts +17 -0
- package/dist/cli/check/brand.d.ts.map +1 -0
- package/dist/cli/check/brand.js +153 -0
- package/dist/cli/check/brand.js.map +1 -0
- package/dist/cli/check/city.d.ts +17 -0
- package/dist/cli/check/city.d.ts.map +1 -0
- package/dist/cli/check/city.js +58 -0
- package/dist/cli/check/city.js.map +1 -0
- package/dist/cli/check/context.d.ts +50 -0
- package/dist/cli/check/context.d.ts.map +1 -0
- package/dist/cli/check/context.js +37 -0
- package/dist/cli/check/context.js.map +1 -0
- package/dist/cli/check/fields.d.ts +16 -0
- package/dist/cli/check/fields.d.ts.map +1 -0
- package/dist/cli/check/fields.js +237 -0
- package/dist/cli/check/fields.js.map +1 -0
- package/dist/cli/check/index.d.ts +14 -0
- package/dist/cli/check/index.d.ts.map +1 -0
- package/dist/cli/check/index.js +14 -0
- package/dist/cli/check/index.js.map +1 -0
- package/dist/cli/check/intelligence.d.ts +17 -0
- package/dist/cli/check/intelligence.d.ts.map +1 -0
- package/dist/cli/check/intelligence.js +136 -0
- package/dist/cli/check/intelligence.js.map +1 -0
- package/dist/cli/check/lenses.d.ts +17 -0
- package/dist/cli/check/lenses.d.ts.map +1 -0
- package/dist/cli/check/lenses.js +239 -0
- package/dist/cli/check/lenses.js.map +1 -0
- package/dist/cli/check/migrations.d.ts +16 -0
- package/dist/cli/check/migrations.d.ts.map +1 -0
- package/dist/cli/check/migrations.js +43 -0
- package/dist/cli/check/migrations.js.map +1 -0
- package/dist/cli/check/modules.d.ts +16 -0
- package/dist/cli/check/modules.d.ts.map +1 -0
- package/dist/cli/check/modules.js +123 -0
- package/dist/cli/check/modules.js.map +1 -0
- package/dist/cli/check/policy.d.ts +16 -0
- package/dist/cli/check/policy.d.ts.map +1 -0
- package/dist/cli/check/policy.js +90 -0
- package/dist/cli/check/policy.js.map +1 -0
- package/dist/cli/check/relations.d.ts +29 -0
- package/dist/cli/check/relations.d.ts.map +1 -0
- package/dist/cli/check/relations.js +279 -0
- package/dist/cli/check/relations.js.map +1 -0
- package/dist/cli/check/routes.d.ts +16 -0
- package/dist/cli/check/routes.d.ts.map +1 -0
- package/dist/cli/check/routes.js +31 -0
- package/dist/cli/check/routes.js.map +1 -0
- package/dist/cli/check/settings.d.ts +16 -0
- package/dist/cli/check/settings.d.ts.map +1 -0
- package/dist/cli/check/settings.js +81 -0
- package/dist/cli/check/settings.js.map +1 -0
- package/dist/cli/check.d.ts +36 -0
- package/dist/cli/check.d.ts.map +1 -0
- package/dist/cli/check.js +76 -0
- package/dist/cli/check.js.map +1 -0
- package/dist/cli/create.d.ts +20 -0
- package/dist/cli/create.d.ts.map +1 -0
- package/dist/cli/create.js +251 -0
- package/dist/cli/create.js.map +1 -0
- package/dist/cli/describe.d.ts +35 -0
- package/dist/cli/describe.d.ts.map +1 -0
- package/dist/cli/describe.js +230 -0
- package/dist/cli/describe.js.map +1 -0
- package/dist/cli/docs.d.ts +11 -0
- package/dist/cli/docs.d.ts.map +1 -0
- package/dist/cli/docs.js +152 -0
- package/dist/cli/docs.js.map +1 -0
- package/dist/cli/index.d.ts +18 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +281 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/extend.d.ts +23 -0
- package/dist/extend.d.ts.map +1 -0
- package/dist/extend.js +53 -0
- package/dist/extend.js.map +1 -0
- package/dist/graph/diff.d.ts +31 -0
- package/dist/graph/diff.d.ts.map +1 -0
- package/dist/graph/diff.js +71 -0
- package/dist/graph/diff.js.map +1 -0
- package/dist/graph/graph.d.ts +69 -0
- package/dist/graph/graph.d.ts.map +1 -0
- package/dist/graph/graph.js +284 -0
- package/dist/graph/graph.js.map +1 -0
- package/dist/graph/primitives.d.ts +52 -0
- package/dist/graph/primitives.d.ts.map +1 -0
- package/dist/graph/primitives.js +62 -0
- package/dist/graph/primitives.js.map +1 -0
- package/dist/graph/tracked.d.ts +31 -0
- package/dist/graph/tracked.d.ts.map +1 -0
- package/dist/graph/tracked.js +69 -0
- package/dist/graph/tracked.js.map +1 -0
- package/dist/graph/types.d.ts +34 -0
- package/dist/graph/types.d.ts.map +1 -0
- package/dist/graph/types.js +0 -0
- package/dist/graph/types.js.map +1 -0
- package/dist/index.d.ts +100 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +90 -0
- package/dist/index.js.map +1 -0
- package/dist/installation.d.ts +112 -0
- package/dist/installation.d.ts.map +1 -0
- package/dist/installation.js +193 -0
- package/dist/installation.js.map +1 -0
- package/dist/intelligence-bridge.d.ts +73 -0
- package/dist/intelligence-bridge.d.ts.map +1 -0
- package/dist/intelligence-bridge.js +31 -0
- package/dist/intelligence-bridge.js.map +1 -0
- package/dist/invariants/engine.d.ts +36 -0
- package/dist/invariants/engine.d.ts.map +1 -0
- package/dist/invariants/engine.js +88 -0
- package/dist/invariants/engine.js.map +1 -0
- package/dist/invariants/types.d.ts +75 -0
- package/dist/invariants/types.d.ts.map +1 -0
- package/dist/invariants/types.js +2 -0
- package/dist/invariants/types.js.map +1 -0
- package/dist/modules.d.ts +75 -0
- package/dist/modules.d.ts.map +1 -0
- package/dist/modules.js +87 -0
- package/dist/modules.js.map +1 -0
- package/dist/mutations/decidable.d.ts +26 -0
- package/dist/mutations/decidable.d.ts.map +1 -0
- package/dist/mutations/decidable.js +39 -0
- package/dist/mutations/decidable.js.map +1 -0
- package/dist/mutations/define-mutation.d.ts +32 -0
- package/dist/mutations/define-mutation.d.ts.map +1 -0
- package/dist/mutations/define-mutation.js +181 -0
- package/dist/mutations/define-mutation.js.map +1 -0
- package/dist/mutations/derive-edits.d.ts +75 -0
- package/dist/mutations/derive-edits.d.ts.map +1 -0
- package/dist/mutations/derive-edits.js +240 -0
- package/dist/mutations/derive-edits.js.map +1 -0
- package/dist/mutations/form.d.ts +63 -0
- package/dist/mutations/form.d.ts.map +1 -0
- package/dist/mutations/form.js +113 -0
- package/dist/mutations/form.js.map +1 -0
- package/dist/mutations/node-ref.d.ts +83 -0
- package/dist/mutations/node-ref.d.ts.map +1 -0
- package/dist/mutations/node-ref.js +113 -0
- package/dist/mutations/node-ref.js.map +1 -0
- package/dist/mutations/types.d.ts +136 -0
- package/dist/mutations/types.d.ts.map +1 -0
- package/dist/mutations/types.js +2 -0
- package/dist/mutations/types.js.map +1 -0
- package/dist/ops/log.d.ts +35 -0
- package/dist/ops/log.d.ts.map +1 -0
- package/dist/ops/log.js +103 -0
- package/dist/ops/log.js.map +1 -0
- package/dist/ops/types.d.ts +52 -0
- package/dist/ops/types.d.ts.map +1 -0
- package/dist/ops/types.js +2 -0
- package/dist/ops/types.js.map +1 -0
- package/dist/ops/undo.d.ts +33 -0
- package/dist/ops/undo.d.ts.map +1 -0
- package/dist/ops/undo.js +65 -0
- package/dist/ops/undo.js.map +1 -0
- package/dist/permissions/policy.d.ts +68 -0
- package/dist/permissions/policy.d.ts.map +1 -0
- package/dist/permissions/policy.js +191 -0
- package/dist/permissions/policy.js.map +1 -0
- package/dist/permissions/types.d.ts +78 -0
- package/dist/permissions/types.d.ts.map +1 -0
- package/dist/permissions/types.js +17 -0
- package/dist/permissions/types.js.map +1 -0
- package/dist/persistence/memory.d.ts +6 -0
- package/dist/persistence/memory.d.ts.map +1 -0
- package/dist/persistence/memory.js +31 -0
- package/dist/persistence/memory.js.map +1 -0
- package/dist/persistence/sqlite.d.ts +59 -0
- package/dist/persistence/sqlite.d.ts.map +1 -0
- package/dist/persistence/sqlite.js +131 -0
- package/dist/persistence/sqlite.js.map +1 -0
- package/dist/persistence/types.d.ts +22 -0
- package/dist/persistence/types.d.ts.map +1 -0
- package/dist/persistence/types.js +2 -0
- package/dist/persistence/types.js.map +1 -0
- package/dist/presence.d.ts +38 -0
- package/dist/presence.d.ts.map +1 -0
- package/dist/presence.js +50 -0
- package/dist/presence.js.map +1 -0
- package/dist/scaffold/domain.d.ts +8 -0
- package/dist/scaffold/domain.d.ts.map +1 -0
- package/dist/scaffold/domain.js +331 -0
- package/dist/scaffold/domain.js.map +1 -0
- package/dist/scaffold/index.d.ts +66 -0
- package/dist/scaffold/index.d.ts.map +1 -0
- package/dist/scaffold/index.js +142 -0
- package/dist/scaffold/index.js.map +1 -0
- package/dist/scaffold/lens.d.ts +31 -0
- package/dist/scaffold/lens.d.ts.map +1 -0
- package/dist/scaffold/lens.js +176 -0
- package/dist/scaffold/lens.js.map +1 -0
- package/dist/scaffold/names.d.ts +44 -0
- package/dist/scaffold/names.d.ts.map +1 -0
- package/dist/scaffold/names.js +69 -0
- package/dist/scaffold/names.js.map +1 -0
- package/dist/scaffold/project.d.ts +41 -0
- package/dist/scaffold/project.d.ts.map +1 -0
- package/dist/scaffold/project.js +604 -0
- package/dist/scaffold/project.js.map +1 -0
- package/dist/scaffold/ui.d.ts +15 -0
- package/dist/scaffold/ui.d.ts.map +1 -0
- package/dist/scaffold/ui.js +536 -0
- package/dist/scaffold/ui.js.map +1 -0
- package/dist/schema/define-node.d.ts +132 -0
- package/dist/schema/define-node.d.ts.map +1 -0
- package/dist/schema/define-node.js +254 -0
- package/dist/schema/define-node.js.map +1 -0
- package/dist/schema/figures.d.ts +42 -0
- package/dist/schema/figures.d.ts.map +1 -0
- package/dist/schema/figures.js +220 -0
- package/dist/schema/figures.js.map +1 -0
- package/dist/schema/json-schema.d.ts +39 -0
- package/dist/schema/json-schema.d.ts.map +1 -0
- package/dist/schema/json-schema.js +74 -0
- package/dist/schema/json-schema.js.map +1 -0
- package/dist/schema/schema.d.ts +43 -0
- package/dist/schema/schema.d.ts.map +1 -0
- package/dist/schema/schema.js +102 -0
- package/dist/schema/schema.js.map +1 -0
- package/dist/schema/types.d.ts +201 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/schema/types.js +2 -0
- package/dist/schema/types.js.map +1 -0
- package/dist/search.d.ts +215 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +449 -0
- package/dist/search.js.map +1 -0
- package/dist/settings.d.ts +30 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +74 -0
- package/dist/settings.js.map +1 -0
- package/dist/store.d.ts +284 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +660 -0
- package/dist/store.js.map +1 -0
- package/dist/studio-door.d.ts +187 -0
- package/dist/studio-door.d.ts.map +1 -0
- package/dist/studio-door.js +20 -0
- package/dist/studio-door.js.map +1 -0
- package/dist/sync/conflict.d.ts +37 -0
- package/dist/sync/conflict.d.ts.map +1 -0
- package/dist/sync/conflict.js +56 -0
- package/dist/sync/conflict.js.map +1 -0
- package/dist/sync/engine.d.ts +118 -0
- package/dist/sync/engine.d.ts.map +1 -0
- package/dist/sync/engine.js +341 -0
- package/dist/sync/engine.js.map +1 -0
- package/dist/sync/google-calendar.d.ts +54 -0
- package/dist/sync/google-calendar.d.ts.map +1 -0
- package/dist/sync/google-calendar.js +167 -0
- package/dist/sync/google-calendar.js.map +1 -0
- package/dist/sync/types.d.ts +125 -0
- package/dist/sync/types.d.ts.map +1 -0
- package/dist/sync/types.js +6 -0
- package/dist/sync/types.js.map +1 -0
- package/dist/temporal/effectivity.d.ts +75 -0
- package/dist/temporal/effectivity.d.ts.map +1 -0
- package/dist/temporal/effectivity.js +89 -0
- package/dist/temporal/effectivity.js.map +1 -0
- package/dist/testing.d.ts +63 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +194 -0
- package/dist/testing.js.map +1 -0
- package/dist/theme/contrast.d.ts +47 -0
- package/dist/theme/contrast.d.ts.map +1 -0
- package/dist/theme/contrast.js +172 -0
- package/dist/theme/contrast.js.map +1 -0
- package/dist/theme/derive.d.ts +81 -0
- package/dist/theme/derive.d.ts.map +1 -0
- package/dist/theme/derive.js +171 -0
- package/dist/theme/derive.js.map +1 -0
- package/dist/theme/kit.d.ts +106 -0
- package/dist/theme/kit.d.ts.map +1 -0
- package/dist/theme/kit.js +84 -0
- package/dist/theme/kit.js.map +1 -0
- package/dist/theme/palettes.d.ts +32 -0
- package/dist/theme/palettes.d.ts.map +1 -0
- package/dist/theme/palettes.js +96 -0
- package/dist/theme/palettes.js.map +1 -0
- package/dist/theme/types.d.ts +131 -0
- package/dist/theme/types.d.ts.map +1 -0
- package/dist/theme/types.js +23 -0
- package/dist/theme/types.js.map +1 -0
- package/dist/views/types.d.ts +89 -0
- package/dist/views/types.d.ts.map +1 -0
- package/dist/views/types.js +88 -0
- package/dist/views/types.js.map +1 -0
- package/dist/watched.d.ts +33 -0
- package/dist/watched.d.ts.map +1 -0
- package/dist/watched.js +50 -0
- package/dist/watched.js.map +1 -0
- package/dist/who.d.ts +26 -0
- package/dist/who.d.ts.map +1 -0
- package/dist/who.js +20 -0
- package/dist/who.js.map +1 -0
- package/package.json +77 -0
package/dist/store.js
ADDED
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
import { Graph, GraphError } from "./graph/graph.js";
|
|
2
|
+
import { resolveModules } from "./modules.js";
|
|
3
|
+
import { diffSnapshots } from "./graph/diff.js";
|
|
4
|
+
import { invert, normalise } from "./graph/primitives.js";
|
|
5
|
+
import { evaluate } from "./invariants/engine.js";
|
|
6
|
+
import { compileMutation } from "./mutations/define-mutation.js";
|
|
7
|
+
import { deriveMutations, derivedVia } from "./mutations/derive-edits.js";
|
|
8
|
+
import { OperationLog } from "./ops/log.js";
|
|
9
|
+
import { permits, permittedMutations } from "./permissions/policy.js";
|
|
10
|
+
import { nounOf } from "./schema/define-node.js";
|
|
11
|
+
import { PermissionDeniedError } from "./permissions/types.js";
|
|
12
|
+
import { checkUndo, undoPrimitives } from "./ops/undo.js";
|
|
13
|
+
import { tellTheWatchItsNames, tellTheWatchOfAStore, tellTheWatchOfAnAuthor, tellTheWatchOfARefusal } from "./watched.js";
|
|
14
|
+
const HUMAN = { kind: "human" };
|
|
15
|
+
/** A violation's identity across judgements: the rule, what it is about, and what it says. */
|
|
16
|
+
export function violationKey(v) {
|
|
17
|
+
return `${v.invariant}|${v.subjectId ?? ""}|${v.message}`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The unit of the framework an app holds: a graph, the log it folds from,
|
|
21
|
+
* the mutations that may touch it and the invariants that judge it.
|
|
22
|
+
*
|
|
23
|
+
* Human and agent edits go through exactly the same path, which is why an
|
|
24
|
+
* agent's work needs no bespoke observability layer — it produces the same
|
|
25
|
+
* diff a human edit does, carrying the same attribution.
|
|
26
|
+
*/
|
|
27
|
+
export class Store {
|
|
28
|
+
schema;
|
|
29
|
+
graph;
|
|
30
|
+
log;
|
|
31
|
+
mutations = new Map();
|
|
32
|
+
invariants;
|
|
33
|
+
invariantOptions;
|
|
34
|
+
policy;
|
|
35
|
+
/**
|
|
36
|
+
* The intelligence the app declared, so a surface can read the DOORS.
|
|
37
|
+
*
|
|
38
|
+
* The store already held the allowlist to enforce it; a seat that wants to
|
|
39
|
+
* offer "copy the prompt" or "send from here" according to what was
|
|
40
|
+
* declared needs the declaration itself, and the store is what every
|
|
41
|
+
* surface already has.
|
|
42
|
+
*/
|
|
43
|
+
intelligence;
|
|
44
|
+
/** What each declared agent may do, by provider name. */
|
|
45
|
+
may = new Map();
|
|
46
|
+
/** What the enabled modules work out to; every surface reads this one answer. */
|
|
47
|
+
modules;
|
|
48
|
+
nextId;
|
|
49
|
+
now;
|
|
50
|
+
counter = 0;
|
|
51
|
+
listeners = new Set();
|
|
52
|
+
constructor(options) {
|
|
53
|
+
this.schema = options.schema;
|
|
54
|
+
this.invariants = options.invariants ?? [];
|
|
55
|
+
this.invariantOptions = options.invariantOptions ?? {};
|
|
56
|
+
this.policy = options.policy;
|
|
57
|
+
this.intelligence = options.intelligence ?? [];
|
|
58
|
+
for (const provider of options.intelligence ?? []) {
|
|
59
|
+
if (provider.may)
|
|
60
|
+
this.may.set(provider.name, new Set(provider.may));
|
|
61
|
+
}
|
|
62
|
+
this.modules = resolveModules(options.modules, options.enabledModules);
|
|
63
|
+
let n = 0;
|
|
64
|
+
this.nextId = options.ids ?? (() => `op${++n}`);
|
|
65
|
+
this.now = options.now ?? (() => new Date(0).toISOString());
|
|
66
|
+
for (const mutation of options.mutations ?? []) {
|
|
67
|
+
if (this.mutations.has(mutation.name)) {
|
|
68
|
+
throw new GraphError(`Duplicate mutation "${mutation.name}"`);
|
|
69
|
+
}
|
|
70
|
+
this.mutations.set(mutation.name, mutation);
|
|
71
|
+
}
|
|
72
|
+
/*
|
|
73
|
+
* THE DERIVED ACTS, registered like any other. A field you could set at
|
|
74
|
+
* creation, you can change: each kind with settable fields nobody writes
|
|
75
|
+
* gets `edit-<kind>`. What was made can be unmade: each kind gets
|
|
76
|
+
* `remove-<kind>`. Both titled, logged, undoable, judged by the
|
|
77
|
+
* invariants — and by the policy, through the acts that already write or
|
|
78
|
+
* create the kind (see `permits`). Nothing an app declares is replaced.
|
|
79
|
+
*/
|
|
80
|
+
for (const mutation of deriveMutations(options.schema, options.mutations ?? [])) {
|
|
81
|
+
this.mutations.set(mutation.name, mutation);
|
|
82
|
+
}
|
|
83
|
+
tellTheWatchItsNames(options.schema, this.mutations.values(), options.policy);
|
|
84
|
+
if (options.log && options.snapshot) {
|
|
85
|
+
// Hydrate: the graph as stored, the history as recorded.
|
|
86
|
+
this.log = OperationLog.from(options.log);
|
|
87
|
+
this.graph = Graph.from(options.schema, options.snapshot, {
|
|
88
|
+
validate: options.validate ?? true,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
else if (options.log) {
|
|
92
|
+
this.log = OperationLog.from(options.log);
|
|
93
|
+
this.graph = this.log.fold(options.schema, { validate: options.validate ?? true });
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
this.log = new OperationLog();
|
|
97
|
+
this.graph = Graph.from(options.schema, options.snapshot ?? { nodes: [], edges: [] }, {
|
|
98
|
+
validate: options.validate ?? true,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/*
|
|
102
|
+
* A HYDRATED STORE COUNTS ON FROM WHERE THE LOG LEFT OFF.
|
|
103
|
+
*
|
|
104
|
+
* Both counters started at zero whatever history was handed in, so the
|
|
105
|
+
* first change after a reload was minted `op1` in `batch:1` — ids the
|
|
106
|
+
* log already held. `batches()` groups by batch id, so the new work was
|
|
107
|
+
* filed under the FIRST turn ever taken: the activity rail went on
|
|
108
|
+
* saying "Add a person" and never grew, undoing that turn would have
|
|
109
|
+
* taken the new change with it, and `log.get(id)` answered with the
|
|
110
|
+
* older op of the two. Every app that remembers anything did this on
|
|
111
|
+
* its second visit.
|
|
112
|
+
*
|
|
113
|
+
* Only the DEFAULT generators are wound forward; an app that supplies
|
|
114
|
+
* its own `ids` owns their uniqueness.
|
|
115
|
+
*/
|
|
116
|
+
const trailing = (value) => {
|
|
117
|
+
const digits = /(\d+)$/.exec(value);
|
|
118
|
+
return digits ? Number(digits[1]) : 0;
|
|
119
|
+
};
|
|
120
|
+
for (const op of this.log.all()) {
|
|
121
|
+
this.counter = Math.max(this.counter, trailing(op.batch));
|
|
122
|
+
n = Math.max(n, trailing(op.id));
|
|
123
|
+
}
|
|
124
|
+
tellTheWatchOfAStore(this);
|
|
125
|
+
}
|
|
126
|
+
mutation(name) {
|
|
127
|
+
const found = this.mutations.get(name);
|
|
128
|
+
if (!found) {
|
|
129
|
+
throw new GraphError(`Unknown mutation "${name}"`, `Registered: ${[...this.mutations.keys()].join(", ") || "(none)"}`);
|
|
130
|
+
}
|
|
131
|
+
// A refusal is a result: the mutation exists, and this workspace has its
|
|
132
|
+
// module off — which is a different sentence from "unknown".
|
|
133
|
+
if (this.mutationDisabled(found)) {
|
|
134
|
+
throw new GraphError(`Mutation "${name}" belongs to a module this workspace has turned off`, `Enabled modules: ${[...this.modules.enabled].join(", ") || "(none)"}`);
|
|
135
|
+
}
|
|
136
|
+
return found;
|
|
137
|
+
}
|
|
138
|
+
/** Off by module, or a derived act of a kind the workspace has off. */
|
|
139
|
+
mutationDisabled(mutation) {
|
|
140
|
+
if (this.modules.disabledMutations.has(mutation.name))
|
|
141
|
+
return true;
|
|
142
|
+
return mutation.derived !== undefined && this.modules.disabledKinds.has(mutation.derived.kind);
|
|
143
|
+
}
|
|
144
|
+
allMutations() {
|
|
145
|
+
return [...this.mutations.values()].filter((mutation) => !this.mutationDisabled(mutation));
|
|
146
|
+
}
|
|
147
|
+
/** The declared acts a derived act resolves its permission through. */
|
|
148
|
+
viaOf(mutation) {
|
|
149
|
+
if (!mutation?.derived)
|
|
150
|
+
return undefined;
|
|
151
|
+
return derivedVia(this.schema, [...this.mutations.values()], mutation);
|
|
152
|
+
}
|
|
153
|
+
allInvariants() {
|
|
154
|
+
return this.invariants.filter((invariant) => this.invariantEnabled(invariant));
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* An invariant is off when its module is, and also when it is scoped to a
|
|
158
|
+
* kind whose module is — a rule about vehicles has nothing to judge in a
|
|
159
|
+
* workspace without them, and evaluating it anyway would resurrect the
|
|
160
|
+
* kind through its violations.
|
|
161
|
+
*/
|
|
162
|
+
invariantEnabled(invariant) {
|
|
163
|
+
if (this.modules.disabledInvariants.has(invariant.name))
|
|
164
|
+
return false;
|
|
165
|
+
if (invariant.scope !== "graph") {
|
|
166
|
+
const kind = invariant.scope.kind;
|
|
167
|
+
if (this.modules.disabledKinds.has(kind))
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Whether a principal may run a call, and — when not — who could.
|
|
174
|
+
*
|
|
175
|
+
* Public because an interface has to be able to ASK rather than guess: an
|
|
176
|
+
* action withheld by permission is stated rather than hidden, and stating
|
|
177
|
+
* it needs the same answer the enforcement uses. One source, two readings.
|
|
178
|
+
*/
|
|
179
|
+
permits(call, principal = HUMAN) {
|
|
180
|
+
/*
|
|
181
|
+
* The same answer `apply` would give. A declared agent's `may` narrows
|
|
182
|
+
* it before the policy does; asked without it, the chat offered a
|
|
183
|
+
* repair as the starter seat, which may only add, and the press met
|
|
184
|
+
* "starter may not take-off here" (W-110).
|
|
185
|
+
*/
|
|
186
|
+
const narrowed = this.refusesAgent(call, principal);
|
|
187
|
+
if (narrowed)
|
|
188
|
+
return { ok: false, refusal: narrowed };
|
|
189
|
+
return permits(this.policy, principal, call.name, this.subjectKindOf(call), this.viaOf(this.mutations.get(call.name)), this.subjectIdOf(call), this.words);
|
|
190
|
+
}
|
|
191
|
+
/** How the policy's refusals name an act and a kind: by title and noun, never by id. */
|
|
192
|
+
get words() {
|
|
193
|
+
return {
|
|
194
|
+
act: (name) => this.mutations.get(name)?.title,
|
|
195
|
+
noun: (kind) => nounOf(this.schema.tryDefinition(kind), kind),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Why a declared agent may not run this call, if it may not.
|
|
200
|
+
*
|
|
201
|
+
* Only ever narrows, and only for an agent: a human author has no
|
|
202
|
+
* allowlist, and a provider that declared no `may` may do whatever its
|
|
203
|
+
* roles allow, which is what "absent means all" has always meant.
|
|
204
|
+
*/
|
|
205
|
+
refusesAgent(call, author) {
|
|
206
|
+
if (author.kind !== "agent" || author.id === undefined)
|
|
207
|
+
return undefined;
|
|
208
|
+
const allowed = this.may.get(author.id);
|
|
209
|
+
if (!allowed || allowed.has(call.name))
|
|
210
|
+
return undefined;
|
|
211
|
+
const titled = (name) => `“${this.mutations.get(name)?.title ?? name}”`;
|
|
212
|
+
const named = [...allowed].map(titled);
|
|
213
|
+
return {
|
|
214
|
+
mutation: call.name,
|
|
215
|
+
message: `${author.id} may not ${titled(call.name)} here: it was declared able to ${named.length === 0 ? "do nothing else" : named.join(", ")}.`,
|
|
216
|
+
wouldNeed: [],
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Whether a principal may run ANY act of a module drawn only for those
|
|
221
|
+
* who administer it. This is the one question the interface asks before
|
|
222
|
+
* offering to show the installation's own districts; the answer comes
|
|
223
|
+
* from the same policy that refuses the acts.
|
|
224
|
+
*/
|
|
225
|
+
mayAdminister(module, principal = HUMAN) {
|
|
226
|
+
const declared = this.modules.administered.get(module);
|
|
227
|
+
if (!declared)
|
|
228
|
+
return false;
|
|
229
|
+
return (declared.mutations ?? []).some((name) => {
|
|
230
|
+
const mutation = this.mutations.get(name);
|
|
231
|
+
if (!mutation)
|
|
232
|
+
return false;
|
|
233
|
+
const kinds = mutation.subject && mutation.subject.kinds !== "*"
|
|
234
|
+
? mutation.subject.kinds
|
|
235
|
+
: [undefined];
|
|
236
|
+
return kinds.some((kind) => permits(this.policy, principal, name, kind, this.viaOf(mutation)).ok);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
/** The kinds of administered modules this principal may not see at all. */
|
|
240
|
+
kindsKeptFrom(principal = HUMAN) {
|
|
241
|
+
const kept = new Set();
|
|
242
|
+
for (const [name, module] of this.modules.administered) {
|
|
243
|
+
if (this.mayAdminister(name, principal))
|
|
244
|
+
continue;
|
|
245
|
+
for (const kind of module.kinds ?? [])
|
|
246
|
+
kept.add(kind);
|
|
247
|
+
}
|
|
248
|
+
return kept;
|
|
249
|
+
}
|
|
250
|
+
subjectIdOf(call) {
|
|
251
|
+
const subject = this.mutations.get(call.name)?.subject;
|
|
252
|
+
if (!subject)
|
|
253
|
+
return undefined;
|
|
254
|
+
const id = call.args[subject.arg];
|
|
255
|
+
return typeof id === "string" ? id : undefined;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* The mutations a principal may run at all.
|
|
259
|
+
*
|
|
260
|
+
* This is what narrows an agent seat's generated tool schema. The seat
|
|
261
|
+
* holds a principal; its tools are what that principal can do; there is no
|
|
262
|
+
* second list to keep in step.
|
|
263
|
+
*/
|
|
264
|
+
permittedMutations(principal = HUMAN) {
|
|
265
|
+
const all = this.allMutations();
|
|
266
|
+
const declared = permittedMutations(this.policy, principal, all.filter((mutation) => !mutation.derived));
|
|
267
|
+
// A derived act is offered when the kind's own acts it rides are.
|
|
268
|
+
const derived = all.filter((mutation) => mutation.derived !== undefined &&
|
|
269
|
+
permits(this.policy, principal, mutation.name, mutation.derived.kind, this.viaOf(mutation)).ok);
|
|
270
|
+
return [...declared, ...derived];
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* The kind a call acts on, read off the mutation's own subject binding.
|
|
274
|
+
*
|
|
275
|
+
* Undefined when the mutation declares no subject, or when the argument
|
|
276
|
+
* names a node that is not there — an add, say. A grant restricted by kind
|
|
277
|
+
* refuses that case rather than allowing it: the safe reading of "I could
|
|
278
|
+
* not tell what this acts on" is no.
|
|
279
|
+
*/
|
|
280
|
+
subjectKindOf(call) {
|
|
281
|
+
const definition = this.mutations.get(call.name);
|
|
282
|
+
const subject = definition?.subject;
|
|
283
|
+
if (!subject)
|
|
284
|
+
return undefined;
|
|
285
|
+
const id = call.args[subject.arg];
|
|
286
|
+
if (typeof id !== "string")
|
|
287
|
+
return undefined;
|
|
288
|
+
return this.graph.getNode(id)?.kind;
|
|
289
|
+
}
|
|
290
|
+
/** Current violations, evaluated fresh — nothing is cached or stale. */
|
|
291
|
+
violations(context) {
|
|
292
|
+
return evaluate(this.graph, this.allInvariants(), {
|
|
293
|
+
...this.invariantOptions,
|
|
294
|
+
...(context === undefined ? {} : { context }),
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* What a mutation would do, without doing it. Every action in the interface
|
|
299
|
+
* previews through here first — a typed mutation is checkable before it
|
|
300
|
+
* applies, whether a human or an agent proposed it.
|
|
301
|
+
*/
|
|
302
|
+
/**
|
|
303
|
+
* Would this act change anything at all?
|
|
304
|
+
*
|
|
305
|
+
* Cheaper than `preview` — it compiles the mutation and counts what came
|
|
306
|
+
* out, without snapshotting the graph or re-evaluating the rules — because
|
|
307
|
+
* it is asked for every act on every selection. An act that compiles to no
|
|
308
|
+
* primitives leaves the graph exactly as it was: "Close it" on something
|
|
309
|
+
* already closed, an edge to a node that already has it, a guard inside
|
|
310
|
+
* the act that decided to do nothing. That is not an act, and offering it
|
|
311
|
+
* is a button that appears broken.
|
|
312
|
+
*/
|
|
313
|
+
wouldChange(call) {
|
|
314
|
+
try {
|
|
315
|
+
return compileMutation(this.graph, this.mutation(call.name), call.args).primitives.length > 0;
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
// An act that cannot even compile is not one that changes nothing; let
|
|
319
|
+
// whoever asked find out the honest way.
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
preview(call, context) {
|
|
324
|
+
const definition = this.mutation(call.name);
|
|
325
|
+
const compiled = compileMutation(this.graph, definition, call.args);
|
|
326
|
+
return this.previewPrimitives(compiled.primitives, {
|
|
327
|
+
reads: compiled.reads,
|
|
328
|
+
writes: compiled.writes,
|
|
329
|
+
intent: compiled.intent,
|
|
330
|
+
context,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
previewPrimitives(primitives, meta) {
|
|
334
|
+
const before = this.violations(meta.context);
|
|
335
|
+
const trial = Graph.from(this.schema, this.graph.snapshot());
|
|
336
|
+
const diff = trial.applyPrimitives(primitives);
|
|
337
|
+
const after = evaluate(trial, this.allInvariants(), {
|
|
338
|
+
...this.invariantOptions,
|
|
339
|
+
...(meta.context === undefined ? {} : { context: meta.context }),
|
|
340
|
+
});
|
|
341
|
+
const beforeKeys = new Set(before.map(violationKey));
|
|
342
|
+
const afterKeys = new Set(after.map(violationKey));
|
|
343
|
+
return {
|
|
344
|
+
diff,
|
|
345
|
+
primitives,
|
|
346
|
+
reads: meta.reads,
|
|
347
|
+
writes: meta.writes,
|
|
348
|
+
intent: meta.intent,
|
|
349
|
+
introduces: after.filter((v) => !beforeKeys.has(violationKey(v))),
|
|
350
|
+
resolves: before.filter((v) => !afterKeys.has(violationKey(v))),
|
|
351
|
+
violationsAfter: after,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
/** Applies one mutation as its own batch, or joins an open batch. */
|
|
355
|
+
apply(call, options = {}) {
|
|
356
|
+
return this.applyAll([call], options);
|
|
357
|
+
}
|
|
358
|
+
/** Applies several mutations as one gesture — one batch, one undo unit. */
|
|
359
|
+
applyAll(calls, options = {}) {
|
|
360
|
+
const batch = options.batch ?? `batch:${++this.counter}`;
|
|
361
|
+
const author = options.author ?? HUMAN;
|
|
362
|
+
const ops = [];
|
|
363
|
+
const allPrimitives = [];
|
|
364
|
+
const reads = new Set();
|
|
365
|
+
const writes = new Set();
|
|
366
|
+
const intents = [];
|
|
367
|
+
const before = this.violations();
|
|
368
|
+
const rollback = this.graph.snapshot();
|
|
369
|
+
/*
|
|
370
|
+
* Permission is checked for the WHOLE gesture before any of it runs.
|
|
371
|
+
*
|
|
372
|
+
* A batch that applied three of five mutations and then refused the
|
|
373
|
+
* fourth would leave the graph in a state nobody asked for, and the
|
|
374
|
+
* rollback below is for errors rather than for policy. Refusing first is
|
|
375
|
+
* also the honest answer to "may I do this": the answer must not depend
|
|
376
|
+
* on how far through the list you got.
|
|
377
|
+
*/
|
|
378
|
+
if (this.policy) {
|
|
379
|
+
for (const call of calls) {
|
|
380
|
+
const verdict = this.permits(call, author);
|
|
381
|
+
if (!verdict.ok) {
|
|
382
|
+
tellTheWatchOfARefusal(verdict.refusal, author.id);
|
|
383
|
+
throw new PermissionDeniedError(verdict.refusal);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
/*
|
|
388
|
+
* AND AN AGENT IS HELD TO WHAT IT WAS DECLARED ABLE TO DO.
|
|
389
|
+
*
|
|
390
|
+
* Checked here rather than only in the tool runtime, because the runtime
|
|
391
|
+
* is one path and `store.apply` is the one everything else uses — an app
|
|
392
|
+
* applying a model's plan itself went straight past the allowlist, and
|
|
393
|
+
* every app would have had to remember to re-implement it. The roles a
|
|
394
|
+
* seat holds say what it may do at all; `may` narrows what THIS provider
|
|
395
|
+
* was brought in for, and the narrower of the two wins.
|
|
396
|
+
*/
|
|
397
|
+
for (const call of calls) {
|
|
398
|
+
const refusal = this.refusesAgent(call, author);
|
|
399
|
+
if (refusal) {
|
|
400
|
+
tellTheWatchOfARefusal(refusal, author.id);
|
|
401
|
+
throw new PermissionDeniedError(refusal);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
tellTheWatchOfAnAuthor(author.id);
|
|
405
|
+
try {
|
|
406
|
+
for (const call of calls) {
|
|
407
|
+
const definition = this.mutation(call.name);
|
|
408
|
+
const compiled = compileMutation(this.graph, definition, call.args);
|
|
409
|
+
/*
|
|
410
|
+
* AN ACT THAT DID NOTHING DOES NOT GO IN THE HISTORY.
|
|
411
|
+
*
|
|
412
|
+
* A mutation whose body decided to do nothing — the guard against a
|
|
413
|
+
* self-referential edge is the one every scaffolded app carries —
|
|
414
|
+
* compiles to no primitives. Logged anyway, it put a line in the
|
|
415
|
+
* activity rail saying a thing had happened, with an undo beside it
|
|
416
|
+
* that undid nothing, and wrote a record that replays to nothing on
|
|
417
|
+
* every future load. The call is still legal and still returns; it
|
|
418
|
+
* simply leaves no trace, because it left no trace.
|
|
419
|
+
*/
|
|
420
|
+
if (compiled.primitives.length === 0)
|
|
421
|
+
continue;
|
|
422
|
+
const op = {
|
|
423
|
+
id: this.nextId(),
|
|
424
|
+
seq: this.log.length,
|
|
425
|
+
batch,
|
|
426
|
+
author,
|
|
427
|
+
intent: options.intent ?? compiled.intent,
|
|
428
|
+
mutation: call,
|
|
429
|
+
/*
|
|
430
|
+
* NORMALISED on the way into the record: a patch that clears a
|
|
431
|
+
* field says so with a value rather than with an absence, or the
|
|
432
|
+
* instruction is lost the moment the op is written to JSON and the
|
|
433
|
+
* inverse quietly does nothing. See `normalise` in primitives.ts.
|
|
434
|
+
*/
|
|
435
|
+
primitives: compiled.primitives.map(normalise),
|
|
436
|
+
inverse: [...compiled.primitives].reverse().map(normalise).map(invert),
|
|
437
|
+
reads: compiled.reads,
|
|
438
|
+
writes: compiled.writes,
|
|
439
|
+
at: this.now(),
|
|
440
|
+
};
|
|
441
|
+
this.graph.applyPrimitives(op.primitives);
|
|
442
|
+
this.log.append(op);
|
|
443
|
+
ops.push(op);
|
|
444
|
+
allPrimitives.push(...op.primitives);
|
|
445
|
+
for (const id of op.reads)
|
|
446
|
+
reads.add(id);
|
|
447
|
+
for (const id of op.writes)
|
|
448
|
+
writes.add(id);
|
|
449
|
+
intents.push(op.intent);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
catch (error) {
|
|
453
|
+
this.graph.load(rollback);
|
|
454
|
+
throw error;
|
|
455
|
+
}
|
|
456
|
+
const after = this.violations();
|
|
457
|
+
const beforeKeys = new Set(before.map(violationKey));
|
|
458
|
+
const afterKeys = new Set(after.map(violationKey));
|
|
459
|
+
const diff = diffSnapshots(rollback, this.graph.snapshot());
|
|
460
|
+
this.notify(diff, ops);
|
|
461
|
+
return {
|
|
462
|
+
batch,
|
|
463
|
+
ops,
|
|
464
|
+
diff,
|
|
465
|
+
primitives: allPrimitives,
|
|
466
|
+
reads: [...reads],
|
|
467
|
+
writes: [...writes],
|
|
468
|
+
intent: options.intent ?? intents.join("; "),
|
|
469
|
+
introduces: after.filter((v) => !beforeKeys.has(violationKey(v))),
|
|
470
|
+
resolves: before.filter((v) => !afterKeys.has(violationKey(v))),
|
|
471
|
+
violationsAfter: after,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* OPERATIONS SOMEBODY ELSE ALREADY MADE, taken into this store.
|
|
476
|
+
*
|
|
477
|
+
* The one thing a store could not do, and the reason "where is my data"
|
|
478
|
+
* could only ever be answered with "in your browser": an op that a server
|
|
479
|
+
* has already judged and compiled has no business being judged and
|
|
480
|
+
* compiled again. Re-running it through `apply` would ask the policy
|
|
481
|
+
* about the wrong principal (whoever is at THIS keyboard, not whoever
|
|
482
|
+
* made the change), recompile against a graph that has moved, and mint a
|
|
483
|
+
* second op with a different id for the same event — so two browsers
|
|
484
|
+
* would diverge rather than converge.
|
|
485
|
+
*
|
|
486
|
+
* So: the primitives land, the op is appended with its own id, author and
|
|
487
|
+
* sequence intact, and the subscribers hear about it exactly as they hear
|
|
488
|
+
* about a local change. Undo still works on it, because an op carries its
|
|
489
|
+
* own inverse.
|
|
490
|
+
*
|
|
491
|
+
* Ops already in the log are skipped by id, so receiving the same batch
|
|
492
|
+
* twice — a poll that overlaps, a reconnect — is not a second change.
|
|
493
|
+
*
|
|
494
|
+
* `seq` is the position in THIS log and is renumbered on the way in; the
|
|
495
|
+
* op keeps everything that identifies it (its id, its author, its intent,
|
|
496
|
+
* its primitives and its inverse). A log is contiguous by construction —
|
|
497
|
+
* that is what makes undo and replay work — and an incoming op carries
|
|
498
|
+
* the sender's numbering, which cannot be this one's the moment this
|
|
499
|
+
* store has applied anything of its own. Whoever is tracking the sender's
|
|
500
|
+
* sequence must keep it themselves; `openRemote` does.
|
|
501
|
+
*/
|
|
502
|
+
receive(ops, options = {}) {
|
|
503
|
+
const known = new Set(this.log.all().map((op) => op.id));
|
|
504
|
+
const fresh = ops.filter((op) => !known.has(op.id));
|
|
505
|
+
if (fresh.length === 0)
|
|
506
|
+
return [];
|
|
507
|
+
const before = this.graph.snapshot();
|
|
508
|
+
const landed = [];
|
|
509
|
+
for (const op of fresh) {
|
|
510
|
+
const here = { ...op, seq: this.log.all().length };
|
|
511
|
+
/*
|
|
512
|
+
* ALREADY IN EFFECT HERE. A client that applied a call provisionally
|
|
513
|
+
* and is now handed the server's op for it has the graph the op
|
|
514
|
+
* describes; applying the primitives again would add the node twice.
|
|
515
|
+
* The op still joins the log — it is the one everybody else has — so
|
|
516
|
+
* undo names it and the activity shows it, and the graph stays put.
|
|
517
|
+
*/
|
|
518
|
+
if (!options.applied)
|
|
519
|
+
this.graph.applyPrimitives(here.primitives);
|
|
520
|
+
this.log.append(here);
|
|
521
|
+
landed.push(here);
|
|
522
|
+
}
|
|
523
|
+
this.notify(diffSnapshots(before, this.graph.snapshot()), landed);
|
|
524
|
+
return landed;
|
|
525
|
+
}
|
|
526
|
+
batches() {
|
|
527
|
+
return this.log.batches();
|
|
528
|
+
}
|
|
529
|
+
/** Whether a batch can come out on its own, and what blocks it if not. */
|
|
530
|
+
canUndo(batchIds) {
|
|
531
|
+
return checkUndo(this.log, typeof batchIds === "string" ? [batchIds] : batchIds);
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* What an undo would look like as a diff, including any invariant it would
|
|
535
|
+
* re-violate. Nothing applies until `undo` is called.
|
|
536
|
+
*/
|
|
537
|
+
previewUndo(batchIds, context) {
|
|
538
|
+
const check = this.canUndo(batchIds);
|
|
539
|
+
if (!check.ok)
|
|
540
|
+
return { ok: false, check };
|
|
541
|
+
const primitives = undoPrimitives(check.ops);
|
|
542
|
+
const writes = new Set();
|
|
543
|
+
for (const op of check.ops)
|
|
544
|
+
for (const id of op.writes)
|
|
545
|
+
writes.add(id);
|
|
546
|
+
return {
|
|
547
|
+
ok: true,
|
|
548
|
+
check,
|
|
549
|
+
...this.previewPrimitives(primitives, {
|
|
550
|
+
reads: [...new Set(check.ops.flatMap((op) => [...op.reads]))],
|
|
551
|
+
writes: [...writes],
|
|
552
|
+
intent: `Undo: ${check.ops.map((op) => op.intent).join("; ")}`,
|
|
553
|
+
...(context === undefined ? {} : { context }),
|
|
554
|
+
}),
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Undo APPENDS inverse ops rather than rewinding a pointer. History is
|
|
559
|
+
* never destroyed, redo is simply the undo of an undo, and dropping an
|
|
560
|
+
* agent's turn while keeping your own edits is a dependency check rather
|
|
561
|
+
* than a stack pop.
|
|
562
|
+
*/
|
|
563
|
+
undo(batchIds, options = {}) {
|
|
564
|
+
const ids = typeof batchIds === "string" ? [batchIds] : batchIds;
|
|
565
|
+
const check = this.canUndo(ids);
|
|
566
|
+
if (!check.ok)
|
|
567
|
+
throw new GraphError(check.message);
|
|
568
|
+
const batch = options.batch ?? `undo:${++this.counter}`;
|
|
569
|
+
const author = options.author ?? HUMAN;
|
|
570
|
+
/*
|
|
571
|
+
* Undo is a CHANGE, and it is judged like one.
|
|
572
|
+
*
|
|
573
|
+
* Without this, undo is the way around the policy: a principal who may
|
|
574
|
+
* not reassign a run could take back somebody else's reassignment and
|
|
575
|
+
* arrive at exactly the state they were refused. What you may undo is
|
|
576
|
+
* what you may have done — the mutation each operation ran, judged
|
|
577
|
+
* again now.
|
|
578
|
+
*/
|
|
579
|
+
if (this.policy) {
|
|
580
|
+
for (const target of check.ops) {
|
|
581
|
+
if (!target.mutation)
|
|
582
|
+
continue;
|
|
583
|
+
const verdict = this.permits(target.mutation, author);
|
|
584
|
+
if (!verdict.ok) {
|
|
585
|
+
const refusal = {
|
|
586
|
+
...verdict.refusal,
|
|
587
|
+
message: `Not permitted to undo "${target.intent}": ${verdict.refusal.message}`,
|
|
588
|
+
};
|
|
589
|
+
tellTheWatchOfARefusal(refusal, author.id);
|
|
590
|
+
throw new PermissionDeniedError(refusal);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
const rollback = this.graph.snapshot();
|
|
595
|
+
const before = this.violations();
|
|
596
|
+
const ops = [];
|
|
597
|
+
for (const target of [...check.ops].sort((a, b) => b.seq - a.seq)) {
|
|
598
|
+
const op = {
|
|
599
|
+
id: this.nextId(),
|
|
600
|
+
seq: this.log.length,
|
|
601
|
+
batch,
|
|
602
|
+
author,
|
|
603
|
+
intent: options.intent ?? `Undo: ${target.intent}`,
|
|
604
|
+
mutation: null,
|
|
605
|
+
primitives: target.inverse,
|
|
606
|
+
inverse: [...target.inverse].reverse().map(invert),
|
|
607
|
+
reads: target.reads,
|
|
608
|
+
writes: target.writes,
|
|
609
|
+
at: this.now(),
|
|
610
|
+
undoes: target.id,
|
|
611
|
+
};
|
|
612
|
+
this.graph.applyPrimitives(op.primitives);
|
|
613
|
+
this.log.append(op);
|
|
614
|
+
ops.push(op);
|
|
615
|
+
}
|
|
616
|
+
const after = this.violations();
|
|
617
|
+
const beforeKeys = new Set(before.map(violationKey));
|
|
618
|
+
const afterKeys = new Set(after.map(violationKey));
|
|
619
|
+
const diff = diffSnapshots(rollback, this.graph.snapshot());
|
|
620
|
+
this.notify(diff, ops);
|
|
621
|
+
return {
|
|
622
|
+
batch,
|
|
623
|
+
ops,
|
|
624
|
+
diff,
|
|
625
|
+
primitives: ops.flatMap((op) => [...op.primitives]),
|
|
626
|
+
reads: [...new Set(ops.flatMap((op) => [...op.reads]))],
|
|
627
|
+
writes: [...new Set(ops.flatMap((op) => [...op.writes]))],
|
|
628
|
+
intent: options.intent ?? ops.map((op) => op.intent).join("; "),
|
|
629
|
+
introduces: after.filter((v) => !beforeKeys.has(violationKey(v))),
|
|
630
|
+
resolves: before.filter((v) => !afterKeys.has(violationKey(v))),
|
|
631
|
+
violationsAfter: after,
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
/** Redo is the undo of an undo — no separate stack exists. */
|
|
635
|
+
redo(undoBatchId, options = {}) {
|
|
636
|
+
return this.undo(undoBatchId, options);
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Every applied change, with the operations that caused it.
|
|
640
|
+
*
|
|
641
|
+
* Notified AFTER the log is appended, not during the graph write: a
|
|
642
|
+
* subscriber that reads the log to find out who made a change must not see
|
|
643
|
+
* a log that has not caught up yet. Subscribing to the graph directly
|
|
644
|
+
* gives the diff without that guarantee.
|
|
645
|
+
*/
|
|
646
|
+
subscribe(listener) {
|
|
647
|
+
this.listeners.add(listener);
|
|
648
|
+
return () => this.listeners.delete(listener);
|
|
649
|
+
}
|
|
650
|
+
notify(diff, ops) {
|
|
651
|
+
if (ops.length === 0)
|
|
652
|
+
return;
|
|
653
|
+
for (const listener of this.listeners)
|
|
654
|
+
listener(diff, ops);
|
|
655
|
+
}
|
|
656
|
+
snapshot() {
|
|
657
|
+
return this.graph.snapshot();
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
//# sourceMappingURL=store.js.map
|