@graview/core 0.1.26 → 0.1.28
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/dist/capabilities.js +1 -1
- package/dist/capabilities.js.map +1 -1
- package/dist/capital.d.ts +2 -0
- package/dist/capital.d.ts.map +1 -1
- package/dist/capital.js +2 -0
- package/dist/capital.js.map +1 -1
- package/dist/cli/check/fact-rules.js +1 -1
- package/dist/cli/check/fact-rules.js.map +1 -1
- package/dist/cli/check/hidden-reads.d.ts.map +1 -1
- package/dist/cli/check/hidden-reads.js +2 -1
- package/dist/cli/check/hidden-reads.js.map +1 -1
- 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/people.d.ts +19 -0
- package/dist/cli/check/people.d.ts.map +1 -0
- package/dist/cli/check/people.js +80 -0
- package/dist/cli/check/people.js.map +1 -0
- package/dist/cli/check/policy.d.ts.map +1 -1
- package/dist/cli/check/policy.js +3 -2
- package/dist/cli/check/policy.js.map +1 -1
- 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/describe.d.ts.map +1 -1
- package/dist/cli/describe.js +6 -0
- package/dist/cli/describe.js.map +1 -1
- package/dist/days.d.ts +2 -0
- package/dist/days.d.ts.map +1 -1
- package/dist/days.js +2 -0
- package/dist/days.js.map +1 -1
- package/dist/document/act-findings.d.ts +1 -1
- package/dist/document/act-findings.d.ts.map +1 -1
- package/dist/document/act-findings.js +35 -3
- package/dist/document/act-findings.js.map +1 -1
- package/dist/document/block-context.d.ts.map +1 -1
- package/dist/document/block-context.js +2 -1
- package/dist/document/block-context.js.map +1 -1
- package/dist/document/compile.d.ts.map +1 -1
- package/dist/document/compile.js +6 -7
- package/dist/document/compile.js.map +1 -1
- package/dist/document/compiled.d.ts.map +1 -1
- package/dist/document/compiled.js +83 -41
- package/dist/document/compiled.js.map +1 -1
- package/dist/document/describe-place.d.ts.map +1 -1
- package/dist/document/describe-place.js +2 -1
- package/dist/document/describe-place.js.map +1 -1
- package/dist/document/diff.d.ts.map +1 -1
- package/dist/document/diff.js +45 -2
- package/dist/document/diff.js.map +1 -1
- package/dist/document/edit.d.ts +1 -1
- package/dist/document/edit.d.ts.map +1 -1
- package/dist/document/edit.js +265 -35
- package/dist/document/edit.js.map +1 -1
- package/dist/document/expr/evaluate.d.ts.map +1 -1
- package/dist/document/expr/evaluate.js +2 -1
- package/dist/document/expr/evaluate.js.map +1 -1
- package/dist/document/policy-words.d.ts +31 -0
- package/dist/document/policy-words.d.ts.map +1 -0
- package/dist/document/policy-words.js +54 -0
- package/dist/document/policy-words.js.map +1 -0
- package/dist/document/respell.d.ts +2 -0
- package/dist/document/respell.d.ts.map +1 -1
- package/dist/document/respell.js +2 -1
- package/dist/document/respell.js.map +1 -1
- package/dist/document/rules.d.ts.map +1 -1
- package/dist/document/rules.js +2 -1
- package/dist/document/rules.js.map +1 -1
- package/dist/document/schema.d.ts +8 -0
- package/dist/document/schema.d.ts.map +1 -1
- package/dist/document/schema.js +6 -0
- package/dist/document/schema.js.map +1 -1
- package/dist/document/template.d.ts.map +1 -1
- package/dist/document/template.js +5 -8
- package/dist/document/template.js.map +1 -1
- package/dist/document/to-document.d.ts.map +1 -1
- package/dist/document/to-document.js +1 -0
- package/dist/document/to-document.js.map +1 -1
- package/dist/document.schema.json +12 -2
- package/dist/installation.d.ts.map +1 -1
- package/dist/installation.js +2 -0
- package/dist/installation.js.map +1 -1
- package/dist/lines.d.ts.map +1 -1
- package/dist/lines.js +2 -1
- package/dist/lines.js.map +1 -1
- package/dist/mutations/define-mutation.d.ts +2 -0
- package/dist/mutations/define-mutation.d.ts.map +1 -1
- package/dist/mutations/define-mutation.js +1 -0
- package/dist/mutations/define-mutation.js.map +1 -1
- package/dist/mutations/types.d.ts +6 -0
- package/dist/mutations/types.d.ts.map +1 -1
- package/dist/permissions/policy.d.ts +8 -2
- package/dist/permissions/policy.d.ts.map +1 -1
- package/dist/permissions/policy.js +27 -42
- package/dist/permissions/policy.js.map +1 -1
- package/dist/permissions/sight.d.ts +4 -3
- package/dist/permissions/sight.d.ts.map +1 -1
- package/dist/permissions/sight.js +10 -9
- package/dist/permissions/sight.js.map +1 -1
- package/dist/permissions/types.d.ts +17 -0
- package/dist/permissions/types.d.ts.map +1 -1
- package/dist/permissions/types.js.map +1 -1
- package/dist/schema/define-node.js +2 -2
- package/dist/schema/define-node.js.map +1 -1
- package/dist/schema/json-schema.d.ts.map +1 -1
- package/dist/schema/json-schema.js +5 -1
- package/dist/schema/json-schema.js.map +1 -1
- package/dist/schema/types.d.ts +9 -0
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +2 -6
- package/dist/search.js.map +1 -1
- package/dist/store.d.ts +4 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +79 -75
- package/dist/store.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/document/edit.js
CHANGED
|
@@ -5,7 +5,7 @@ import { error } from "./findings.js";
|
|
|
5
5
|
import { coerce } from "./migrate.js";
|
|
6
6
|
import { outsideRange, rangeWords } from "./range.js";
|
|
7
7
|
import { farEnd } from "./far-end.js";
|
|
8
|
-
import { ActSpec, EDGE_NAME, EdgeSpec, FIELD_NAME, FIELD_TYPES, FieldSpec, KindSpec, NAME, RuleSpec, } from "./schema.js";
|
|
8
|
+
import { ActSpec, EDGE_NAME, EdgeSpec, FIELD_NAME, FIELD_TYPES, FieldSpec, GrantSpec, KindSpec, NAME, RuleSpec, SightSpec, } from "./schema.js";
|
|
9
9
|
import { SHIPPED_LENSES, isShippedLens, SHIPPED_LENS_NAMES } from "../places.js";
|
|
10
10
|
import { placeSlug } from "../views/types.js";
|
|
11
11
|
import { computedOf, validateComputed } from "./computed.js";
|
|
@@ -14,10 +14,13 @@ import { validateViews, VIEW_SLOTS } from "./views.js";
|
|
|
14
14
|
import { brandFindings } from "./brand-check.js";
|
|
15
15
|
import { canonicalize } from "./canonical.js";
|
|
16
16
|
import { quotedAtTheEnd } from "./quote.js";
|
|
17
|
-
import { respellDocument } from "./respell.js";
|
|
17
|
+
import { isRecord as isObject, respellDocument } from "./respell.js";
|
|
18
|
+
import { refName } from "./compiled.js";
|
|
18
19
|
import { accentProblem } from "../theme/accent.js";
|
|
19
20
|
import { pageFieldFindings, pageNames, renamedOnPage, withoutOnPage } from "./page-fields.js";
|
|
20
|
-
import { capitalize } from "../capital.js";
|
|
21
|
+
import { capitalize, listed as listOf } from "../capital.js";
|
|
22
|
+
import { quoted } from "./quote.js";
|
|
23
|
+
import { FIRST_GRANT, FIRST_SIGHT, grantBody, grantGone, grantSentence, NO_GRANT_LEFT, NO_SIGHT_LEFT, peopleKinds, peopleSentence, nounWords, pluralWords as pluralOf, sightGone, sightSentence, sightWords, whoWords } from "./policy-words.js";
|
|
21
24
|
/** The ops, for tool schemas and the guide. */
|
|
22
25
|
export const EDIT_OPS = [
|
|
23
26
|
"add-kind",
|
|
@@ -51,6 +54,11 @@ export const EDIT_OPS = [
|
|
|
51
54
|
"set-home",
|
|
52
55
|
"arrange-pages",
|
|
53
56
|
"set-computed",
|
|
57
|
+
"set-people",
|
|
58
|
+
"add-grant",
|
|
59
|
+
"remove-grant",
|
|
60
|
+
"add-sight",
|
|
61
|
+
"remove-sight",
|
|
54
62
|
];
|
|
55
63
|
/** Words the rule language keeps: a field or relation renamed to one could not be named in a rule. */
|
|
56
64
|
const KEYWORDS = new Set(["and", "or", "not", "in", "where", "true", "false", "null"]);
|
|
@@ -149,12 +157,16 @@ const SHAPES = {
|
|
|
149
157
|
description: z.union([z.string().min(1).max(500), z.null()]).optional(),
|
|
150
158
|
})
|
|
151
159
|
.strict(),
|
|
160
|
+
// Who the people are, and who may do and see what (FR-170): a grant and a sight as the document holds one.
|
|
161
|
+
"set-people": z.object({ op: z.literal("set-people"), kind: kindName, people: z.boolean() }).strict(),
|
|
162
|
+
"add-grant": z.object({ op: z.literal("add-grant") }).passthrough(),
|
|
163
|
+
"remove-grant": z.object({ op: z.literal("remove-grant") }).passthrough(),
|
|
164
|
+
"add-sight": z.object({ op: z.literal("add-sight") }).passthrough(),
|
|
165
|
+
"remove-sight": z.object({ op: z.literal("remove-sight") }).passthrough(),
|
|
152
166
|
};
|
|
153
167
|
const NONE = new Set();
|
|
154
168
|
const clone = (v) => structuredClone(v);
|
|
155
|
-
const asArray = (v) =>
|
|
156
|
-
const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
157
|
-
const refName = (v) => (typeof v === "string" && /^\$[A-Za-z][A-Za-z0-9]*$/.test(v) ? v.slice(1) : undefined);
|
|
169
|
+
const asArray = (v) => [v ?? []].flat();
|
|
158
170
|
const kebab = (camel) => camel.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
|
|
159
171
|
const words = (name) => kebab(name).replace(/-/g, " ");
|
|
160
172
|
function hasKind(kinds, kind) {
|
|
@@ -605,6 +617,11 @@ class Editor {
|
|
|
605
617
|
}
|
|
606
618
|
return true;
|
|
607
619
|
}
|
|
620
|
+
/** A shape's refusals, each at its path: a key it does not take, said by name. */
|
|
621
|
+
refuse(i, issues, what) {
|
|
622
|
+
for (const issue of issues)
|
|
623
|
+
this.fail(i, issue.path.map(String).join("."), issue.keys ? `${issue.keys.map((k) => `"${k}"`).join(", ")} ${issue.keys.length === 1 ? "is" : "are"} not part of ${what}` : issue.message);
|
|
624
|
+
}
|
|
608
625
|
apply(i, edit) {
|
|
609
626
|
const op = edit["op"];
|
|
610
627
|
if (typeof op !== "string" || !EDIT_OPS.includes(op)) {
|
|
@@ -613,10 +630,7 @@ class Editor {
|
|
|
613
630
|
}
|
|
614
631
|
const parsed = SHAPES[op].safeParse(edit);
|
|
615
632
|
if (!parsed.success) {
|
|
616
|
-
|
|
617
|
-
const path = issue.path.map(String).join(".");
|
|
618
|
-
this.fail(i, path, issue.code === "unrecognized_keys" ? `${issue.keys.map((k) => `"${k}"`).join(", ")} ${issue.keys.length === 1 ? "is" : "are"} not part of ${op}` : issue.message);
|
|
619
|
-
}
|
|
633
|
+
this.refuse(i, parsed.error.issues, op);
|
|
620
634
|
return;
|
|
621
635
|
}
|
|
622
636
|
const e = parsed.data;
|
|
@@ -784,6 +798,13 @@ class Editor {
|
|
|
784
798
|
return this.arrangePages(i, e);
|
|
785
799
|
case "set-computed":
|
|
786
800
|
return this.setComputed(i, e);
|
|
801
|
+
case "set-people":
|
|
802
|
+
return this.setPeople(i, e);
|
|
803
|
+
case "add-grant":
|
|
804
|
+
case "remove-grant":
|
|
805
|
+
case "add-sight":
|
|
806
|
+
case "remove-sight":
|
|
807
|
+
return this.policyEdit(i, e);
|
|
787
808
|
}
|
|
788
809
|
}
|
|
789
810
|
/** How a record's page orders and groups its facts (FR-148): judged as the compile judges `kinds.<kind>.page`. */
|
|
@@ -1135,6 +1156,120 @@ class Editor {
|
|
|
1135
1156
|
else
|
|
1136
1157
|
this.said.push(`${plural}' "${name}" is ${next.label ? `shown as "${next.label}"` : "shown by its name"}${next.description ? `, described as "${next.description}"` : ""}.`);
|
|
1137
1158
|
}
|
|
1159
|
+
// ── who the people are, and who may do and see what (FR-170) ──
|
|
1160
|
+
/** The last edit that changed the people or the policy: where what they make together is judged. */
|
|
1161
|
+
lastPolicyEdit = -1;
|
|
1162
|
+
failAs(code, i, path, message, fix) {
|
|
1163
|
+
this.findings.push(error(code, `edits.${i}${path ? `.${path}` : ""}`, message, fix));
|
|
1164
|
+
}
|
|
1165
|
+
setPeople(i, e) {
|
|
1166
|
+
const spec = this.kind(i, e.kind);
|
|
1167
|
+
if (!spec)
|
|
1168
|
+
return;
|
|
1169
|
+
this.lastPolicyEdit = i;
|
|
1170
|
+
const already = (spec.people === true) === e.people;
|
|
1171
|
+
if (e.people && !already) {
|
|
1172
|
+
// Exactly one kind is the people: a principal's id is a record of it, never of one of two.
|
|
1173
|
+
const other = peopleKinds(this.doc.kinds)[0];
|
|
1174
|
+
if (other)
|
|
1175
|
+
return this.failAs("people-ambiguous", i, "kind", `${pluralOf(this.doc.kinds, other)} are already the people who sign in, and an app has one kind for them`, `{"op": "set-people", "kind": "${other}", "people": false} first, in the same change`);
|
|
1176
|
+
spec.people = true;
|
|
1177
|
+
}
|
|
1178
|
+
else if (!e.people)
|
|
1179
|
+
delete spec.people;
|
|
1180
|
+
this.said.push(peopleSentence(this.doc.kinds, e.kind, e.people, already));
|
|
1181
|
+
}
|
|
1182
|
+
/** A grant or a sight as the document holds one, or undefined having said why not. */
|
|
1183
|
+
policyPart(i, e, sight) {
|
|
1184
|
+
const { op: _op, ...rest } = e;
|
|
1185
|
+
const parsed = (sight ? SightSpec : GrantSpec).safeParse(rest);
|
|
1186
|
+
if (!parsed.success)
|
|
1187
|
+
return void this.refuse(i, parsed.error.issues, sight ? "a sight" : "a grant");
|
|
1188
|
+
return parsed.data;
|
|
1189
|
+
}
|
|
1190
|
+
policyEdit(i, e) {
|
|
1191
|
+
const sight = e.op === "add-sight" || e.op === "remove-sight";
|
|
1192
|
+
const part = this.policyPart(i, e, sight);
|
|
1193
|
+
if (!part)
|
|
1194
|
+
return;
|
|
1195
|
+
this.lastPolicyEdit = i;
|
|
1196
|
+
if (e.op === "remove-grant" || e.op === "remove-sight")
|
|
1197
|
+
return this.removePolicyPart(i, part, sight);
|
|
1198
|
+
// What it names is the app's: its kinds, and a grant's acts (a kind's derived edit and remove among them).
|
|
1199
|
+
if (!sight && Array.isArray(part.kinds) && part.kinds.length === 0)
|
|
1200
|
+
return this.fail(i, "kinds", "a grant on no kind gives nothing", "leave kinds out for every kind, or name the kinds it is on");
|
|
1201
|
+
for (const [n, kind] of (Array.isArray(part.kinds) ? part.kinds : []).entries()) {
|
|
1202
|
+
if (!this.doc.kinds[kind])
|
|
1203
|
+
return this.failAs(sight ? "sight-kind" : "edit", i, `kinds.${n}`, `"${kind}" is not a kind this app has; it has ${Object.keys(this.doc.kinds).join(", ")}`);
|
|
1204
|
+
}
|
|
1205
|
+
if (!sight && part.mutations !== "*") {
|
|
1206
|
+
for (const [n, act] of part.mutations.entries()) {
|
|
1207
|
+
const derived = /^(?:edit|remove)-(.+)$/.exec(act);
|
|
1208
|
+
if (this.doc.acts?.[act] || (derived && this.doc.kinds[derived[1]]))
|
|
1209
|
+
continue;
|
|
1210
|
+
return this.failAs("grant-act", i, `mutations.${n}`, `"${act}" is not an act this app has`, `one of ${[...Object.keys(this.doc.acts ?? {}), ...Object.keys(this.doc.kinds).flatMap((kind) => [`edit-${kind}`, `remove-${kind}`])].join(", ")}`);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
const said = [];
|
|
1214
|
+
// A role the app has not declared becomes one of its roles, where it declares them.
|
|
1215
|
+
if (Array.isArray(this.doc.roles) && part.roles !== "*") {
|
|
1216
|
+
for (const role of part.roles) {
|
|
1217
|
+
if (this.doc.roles.includes(role))
|
|
1218
|
+
continue;
|
|
1219
|
+
this.doc.roles.push(role);
|
|
1220
|
+
said.push(`${capitalize(whoWords([role]))} is a new role.`);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
if (!this.doc.policy) {
|
|
1224
|
+
// No policy permits everything; a sight alone would take every act away, so its policy says everyone may still do them.
|
|
1225
|
+
const everything = { roles: "*", mutations: "*" };
|
|
1226
|
+
this.doc.policy = { grants: sight ? [everything] : [] };
|
|
1227
|
+
said.push(FIRST_GRANT, ...(sight ? [grantSentence(this.doc, everything)] : []));
|
|
1228
|
+
}
|
|
1229
|
+
if (sight) {
|
|
1230
|
+
if (!this.doc.policy.sees?.length)
|
|
1231
|
+
said.push(FIRST_SIGHT);
|
|
1232
|
+
(this.doc.policy.sees ??= []).push(part);
|
|
1233
|
+
said.push(sightSentence(this.doc, part));
|
|
1234
|
+
}
|
|
1235
|
+
else {
|
|
1236
|
+
this.doc.policy.grants.push(part);
|
|
1237
|
+
said.push(grantSentence(this.doc, part));
|
|
1238
|
+
}
|
|
1239
|
+
this.said.push(...said);
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* A GRANT OR A SIGHT, NAMED BY WHAT IT SAYS. A chat names one the way it
|
|
1243
|
+
* would say it — "the grant that lets everyone take back a vote" — so a
|
|
1244
|
+
* removal names it by its keys: roles and acts (or kinds) in any order,
|
|
1245
|
+
* and any other key it gives must match too. Never by its place in the
|
|
1246
|
+
* list, which another change can move.
|
|
1247
|
+
*/
|
|
1248
|
+
removePolicyPart(i, part, sight) {
|
|
1249
|
+
const list = (sight ? this.doc.policy?.sees : this.doc.policy?.grants) ?? [];
|
|
1250
|
+
const sameSet = (a, b) => a === b || (Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((x) => b.includes(x)));
|
|
1251
|
+
const keys = sight ? ["own", "via"] : ["kinds", "self", "ownEnd", "describe"];
|
|
1252
|
+
const matches = list.filter((one) => sameSet(one.roles, part.roles) &&
|
|
1253
|
+
(sight ? sameSet(one.kinds, part.kinds) : sameSet(one.mutations, part.mutations)) &&
|
|
1254
|
+
keys.every((key) => part[key] === undefined || (typeof part[key] === "boolean" ? (one[key] ?? false) === part[key] : key === "kinds" ? sameSet(one[key] ?? "*", part[key]) : one[key] === part[key])));
|
|
1255
|
+
const what = sight ? "sight" : "grant";
|
|
1256
|
+
const say = (one) => (sight ? `${whoWords(one.roles)} ${sightWords(this.doc, one)}` : grantBody(this.doc, one));
|
|
1257
|
+
if (matches.length === 0) {
|
|
1258
|
+
const there = list.map(say);
|
|
1259
|
+
return this.fail(i, "", `no ${what} ${sight ? "shows" : "says"} ${say(part)}${there.length ? `; the ${what}s ${sight ? "show" : "say"} ${there.length > 1 ? `${there.slice(0, -1).join("; ")}; and ${there.at(-1)}` : there[0]}` : `; the app has no ${what}s`}`);
|
|
1260
|
+
}
|
|
1261
|
+
if (matches.length > 1)
|
|
1262
|
+
return this.fail(i, "", `${matches.length} ${what}s ${sight ? "show" : "say"} ${say(part)}`, `say ${sight ? "own or via" : "ownEnd, self, kinds or describe"} as the one to remove says them`);
|
|
1263
|
+
const gone = matches[0];
|
|
1264
|
+
list.splice(list.indexOf(gone), 1);
|
|
1265
|
+
this.said.push(sight ? sightGone(this.doc, gone) : grantGone(this.doc, gone));
|
|
1266
|
+
if (sight && list.length === 0) {
|
|
1267
|
+
delete this.doc.policy.sees;
|
|
1268
|
+
this.said.push(NO_SIGHT_LEFT);
|
|
1269
|
+
}
|
|
1270
|
+
else if (!sight && list.length === 0)
|
|
1271
|
+
this.said.push(NO_GRANT_LEFT);
|
|
1272
|
+
}
|
|
1138
1273
|
// ── kinds ──
|
|
1139
1274
|
addKind(i, e) {
|
|
1140
1275
|
const { op: _op, kind, act, ...rest } = e;
|
|
@@ -1142,8 +1277,7 @@ class Editor {
|
|
|
1142
1277
|
return this.fail(i, "kind", `there is already a kind "${kind}"`);
|
|
1143
1278
|
const parsed = KindSpec.safeParse(rest);
|
|
1144
1279
|
if (!parsed.success) {
|
|
1145
|
-
|
|
1146
|
-
this.fail(i, issue.path.map(String).join("."), issue.code === "unrecognized_keys" ? `${issue.keys.map((k) => `"${k}"`).join(", ")} ${issue.keys.length === 1 ? "is" : "are"} not part of a kind` : issue.message);
|
|
1280
|
+
this.refuse(i, parsed.error.issues, "a kind");
|
|
1147
1281
|
return;
|
|
1148
1282
|
}
|
|
1149
1283
|
const spec = { ...parsed.data };
|
|
@@ -1228,6 +1362,11 @@ class Editor {
|
|
|
1228
1362
|
return;
|
|
1229
1363
|
if (Object.keys(this.doc.kinds).length === 1)
|
|
1230
1364
|
return this.fail(i, "kind", "an app keeps at least one kind");
|
|
1365
|
+
// The last sight gone would show everyone everything, every other kind included: the policy's to change first, as a relation a sight reads is.
|
|
1366
|
+
const sees = this.doc.policy?.sees ?? [];
|
|
1367
|
+
if (sees.length > 0 && sees.every((sight) => sight.kinds.every((k) => k === kind))) {
|
|
1368
|
+
return this.fail(i, "kind", `every sight shows only ${pluralWords(this.doc, kind)}, so with them gone everyone would see everything`, `remove those sights first, in the same change ({"op": "remove-sight", …}), which says so`);
|
|
1369
|
+
}
|
|
1231
1370
|
const gone = [];
|
|
1232
1371
|
/*
|
|
1233
1372
|
* The relations it declares go with it — and so does what walks them
|
|
@@ -1262,8 +1401,8 @@ class Editor {
|
|
|
1262
1401
|
for (const [name, act] of Object.entries(this.doc.acts ?? {})) {
|
|
1263
1402
|
const on = asArray(act.on);
|
|
1264
1403
|
if (act.creates === kind || (act.effects ?? []).some((x) => x.create === kind) || (on.includes(kind) && on.length === 1)) {
|
|
1404
|
+
gone.push(titled("act", name, act));
|
|
1265
1405
|
this.removeActQuietly(name);
|
|
1266
|
-
gone.push(`act ${name}`);
|
|
1267
1406
|
}
|
|
1268
1407
|
else if (on.includes(kind))
|
|
1269
1408
|
act.on = on.filter((k) => k !== kind);
|
|
@@ -1271,7 +1410,7 @@ class Editor {
|
|
|
1271
1410
|
for (const [name, rule] of Object.entries(this.doc.rules ?? {})) {
|
|
1272
1411
|
if (rule.over === kind || exprMentions(this.doc, rule.when, rule.over === "graph" ? NONE : new Set([rule.over]), { t: "kind", from: kind, to: "\u0000" }) || exprMentions(this.doc, rule.require, rule.over === "graph" ? NONE : new Set([rule.over]), { t: "kind", from: kind, to: "\u0000" })) {
|
|
1273
1412
|
delete this.doc.rules[name];
|
|
1274
|
-
gone.push(
|
|
1413
|
+
gone.push(titled("rule", name, rule));
|
|
1275
1414
|
}
|
|
1276
1415
|
}
|
|
1277
1416
|
if (this.doc.views?.[kind] && !Array.isArray(this.doc.views[kind])) {
|
|
@@ -1279,9 +1418,21 @@ class Editor {
|
|
|
1279
1418
|
if (Object.keys(this.doc.views).length === 0)
|
|
1280
1419
|
delete this.doc.views;
|
|
1281
1420
|
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1421
|
+
/*
|
|
1422
|
+
* A grant no longer names the kind, nor its derived edit and remove; one
|
|
1423
|
+
* left naming no kind or no act gives nothing, and goes, as a grant does
|
|
1424
|
+
* whose last act is removed — kept, it read as a grant on every kind.
|
|
1425
|
+
*/
|
|
1426
|
+
if (this.doc.policy) {
|
|
1427
|
+
const derived = [`edit-${kind}`, `remove-${kind}`];
|
|
1428
|
+
this.doc.policy.grants = this.doc.policy.grants.flatMap((g) => {
|
|
1429
|
+
const kinds = Array.isArray(g.kinds) ? g.kinds.filter((k) => k !== kind) : g.kinds;
|
|
1430
|
+
const mutations = Array.isArray(g.mutations) ? g.mutations.filter((m) => !derived.includes(m) || this.doc.acts?.[m]) : g.mutations;
|
|
1431
|
+
if ((Array.isArray(g.kinds) && g.kinds.length > 0 && kinds.length === 0) || (Array.isArray(mutations) && mutations.length === 0))
|
|
1432
|
+
return [];
|
|
1433
|
+
return [{ ...g, ...(kinds !== undefined ? { kinds } : {}), mutations }];
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1285
1436
|
if (this.doc.policy?.sees) {
|
|
1286
1437
|
this.doc.policy.sees = this.doc.policy.sees.map((s) => ({ ...s, kinds: s.kinds.filter((k) => k !== kind) })).filter((s) => s.kinds.length > 0);
|
|
1287
1438
|
if (this.doc.policy.sees.length === 0)
|
|
@@ -1319,7 +1470,7 @@ class Editor {
|
|
|
1319
1470
|
for (const f of [...this.fills])
|
|
1320
1471
|
if (f.kind === kind)
|
|
1321
1472
|
this.fills.splice(this.fills.indexOf(f), 1);
|
|
1322
|
-
this.said.push(`The ${kind} kind is removed, and every ${kind} with it${gone.length ? `; so are ${listOf(gone)}` : ""}.`);
|
|
1473
|
+
this.said.push(`The ${kind} kind is removed, and every ${kind} with it${gone.length ? `; so ${gone.length === 1 ? "is" : "are"} ${listOf(gone)}` : ""}.`);
|
|
1323
1474
|
}
|
|
1324
1475
|
// ── fields ──
|
|
1325
1476
|
addField(i, e) {
|
|
@@ -1333,8 +1484,7 @@ class Editor {
|
|
|
1333
1484
|
return this.fail(i, "field", `"${field}" is a word the rule language keeps; pick another name`);
|
|
1334
1485
|
const parsed = FieldSpec.safeParse({ ...(given ?? {}), ...inline });
|
|
1335
1486
|
if (!parsed.success) {
|
|
1336
|
-
|
|
1337
|
-
this.fail(i, issue.path.map(String).join("."), issue.code === "unrecognized_keys" ? `${issue.keys.map((k) => `"${k}"`).join(", ")} ${issue.keys.length === 1 ? "is" : "are"} not part of a field` : issue.message);
|
|
1487
|
+
this.refuse(i, parsed.error.issues, "a field");
|
|
1338
1488
|
return;
|
|
1339
1489
|
}
|
|
1340
1490
|
const f = { ...parsed.data };
|
|
@@ -1570,8 +1720,7 @@ class Editor {
|
|
|
1570
1720
|
return this.fail(i, "relation", `"${relation}" is a word the rule language keeps; pick another name`);
|
|
1571
1721
|
const parsed = EdgeSpec.safeParse({ ...rest, to: typeof rest.to === "string" && rest.to !== "*" ? [rest.to] : rest.to });
|
|
1572
1722
|
if (!parsed.success) {
|
|
1573
|
-
|
|
1574
|
-
this.fail(i, issue.path.map(String).join("."), issue.code === "unrecognized_keys" ? `${issue.keys.map((k) => `"${k}"`).join(", ")} ${issue.keys.length === 1 ? "is" : "are"} not part of a relation` : issue.message);
|
|
1723
|
+
this.refuse(i, parsed.error.issues, "a relation");
|
|
1575
1724
|
return;
|
|
1576
1725
|
}
|
|
1577
1726
|
const edge = { ...parsed.data };
|
|
@@ -1598,6 +1747,10 @@ class Editor {
|
|
|
1598
1747
|
if (Object.values(this.doc.kinds).some((s) => s.edges?.[to]))
|
|
1599
1748
|
return this.fail(i, "to", `another kind already has a relation called "${to}"`);
|
|
1600
1749
|
const touched = this.rewriteAll({ t: "edge", from: relation, to });
|
|
1750
|
+
// A sight via it names it too.
|
|
1751
|
+
for (const sight of this.doc.policy?.sees ?? [])
|
|
1752
|
+
if (sight.via === relation)
|
|
1753
|
+
sight.via = to;
|
|
1601
1754
|
for (const k of holders) {
|
|
1602
1755
|
const s = this.doc.kinds[k];
|
|
1603
1756
|
const edges = {};
|
|
@@ -1623,6 +1776,9 @@ class Editor {
|
|
|
1623
1776
|
const others = [...sources(this.doc, relation)].filter((k) => k !== kind);
|
|
1624
1777
|
if (others.length === 0 && this.lensesDrawnBy(i, { t: "edge", from: relation, to: "\u0000" }))
|
|
1625
1778
|
return;
|
|
1779
|
+
// A sight via it is the policy's to change first, as a lens drawn by it is: dropped, it could show everybody what it kept to each seat's own.
|
|
1780
|
+
if (others.length === 0 && this.doc.policy?.sees?.some((sight) => sight.via === relation))
|
|
1781
|
+
return this.fail(i, "relation", `a sight shows what "${relation}" joins to each seat's own record`, "change the policy's sight first");
|
|
1626
1782
|
const gone = [];
|
|
1627
1783
|
this.dropRelation(kind, relation, gone);
|
|
1628
1784
|
this.said.push(`The "${relation}" relation from ${kind} is removed, with every link of that kind${gone.length ? `; ${listOf(gone)} ${gone.length === 1 ? "goes" : "go"} with it` : ""}.`);
|
|
@@ -1644,8 +1800,7 @@ class Editor {
|
|
|
1644
1800
|
return this.fail(i, "act", `there is already an act "${act}"`, "pass replace: true to change it, or pick another name");
|
|
1645
1801
|
const parsed = ActSpec.safeParse(rest);
|
|
1646
1802
|
if (!parsed.success) {
|
|
1647
|
-
|
|
1648
|
-
this.fail(i, issue.path.map(String).join("."), issue.code === "unrecognized_keys" ? `${issue.keys.map((k) => `"${k}"`).join(", ")} ${issue.keys.length === 1 ? "is" : "are"} not part of an act` : issue.message);
|
|
1803
|
+
this.refuse(i, parsed.error.issues, "an act");
|
|
1649
1804
|
return;
|
|
1650
1805
|
}
|
|
1651
1806
|
const existed = Boolean(this.doc.acts?.[act]);
|
|
@@ -1675,7 +1830,7 @@ class Editor {
|
|
|
1675
1830
|
continue;
|
|
1676
1831
|
const kept = rule.repairs.filter((r) => r.act !== act);
|
|
1677
1832
|
if (kept.length !== rule.repairs.length)
|
|
1678
|
-
gone.push(`the "${rule.repairs.find((r) => r.act === act)?.label ?? act}" repair of rule
|
|
1833
|
+
gone.push(`the "${rule.repairs.find((r) => r.act === act)?.label ?? act}" repair of ${titled("rule", name, rule)}`);
|
|
1679
1834
|
if (kept.length > 0)
|
|
1680
1835
|
rule.repairs = kept;
|
|
1681
1836
|
else
|
|
@@ -1689,8 +1844,7 @@ class Editor {
|
|
|
1689
1844
|
return this.fail(i, "rule", `there is already a rule "${rule}"`, "pass replace: true to change it, or pick another name");
|
|
1690
1845
|
const parsed = RuleSpec.safeParse(rest);
|
|
1691
1846
|
if (!parsed.success) {
|
|
1692
|
-
|
|
1693
|
-
this.fail(i, issue.path.map(String).join("."), issue.code === "unrecognized_keys" ? `${issue.keys.map((k) => `"${k}"`).join(", ")} ${issue.keys.length === 1 ? "is" : "are"} not part of a rule` : issue.message);
|
|
1847
|
+
this.refuse(i, parsed.error.issues, "a rule");
|
|
1694
1848
|
return;
|
|
1695
1849
|
}
|
|
1696
1850
|
const existed = Boolean(this.doc.rules?.[rule]);
|
|
@@ -2032,14 +2186,14 @@ class Editor {
|
|
|
2032
2186
|
}
|
|
2033
2187
|
}
|
|
2034
2188
|
if (uses) {
|
|
2035
|
-
gone.push(
|
|
2189
|
+
gone.push(titled("act", name, act), ...this.removeActQuietly(name));
|
|
2036
2190
|
}
|
|
2037
2191
|
}
|
|
2038
2192
|
for (const [name, rule] of Object.entries(doc.rules ?? {})) {
|
|
2039
2193
|
const ctx = rule.over === "graph" ? NONE : new Set([rule.over]);
|
|
2040
2194
|
if (exprMentions(doc, rule.when, ctx, r) || exprMentions(doc, rule.require, ctx, r) || templateMentions(doc, rule.says, ctx, r)) {
|
|
2041
2195
|
delete doc.rules[name];
|
|
2042
|
-
gone.push(
|
|
2196
|
+
gone.push(titled("rule", name, rule));
|
|
2043
2197
|
}
|
|
2044
2198
|
}
|
|
2045
2199
|
if (doc.rules && Object.keys(doc.rules).length === 0)
|
|
@@ -2239,6 +2393,70 @@ class Editor {
|
|
|
2239
2393
|
return said;
|
|
2240
2394
|
}
|
|
2241
2395
|
}
|
|
2396
|
+
/** What the people and the policy make together, as `graview check` judges it (`checkPeople`), said over the document. */
|
|
2397
|
+
function peopleProblems(doc, at) {
|
|
2398
|
+
const out = [];
|
|
2399
|
+
const kinds = doc.kinds;
|
|
2400
|
+
const people = peopleKinds(kinds);
|
|
2401
|
+
const policy = doc.policy;
|
|
2402
|
+
const reads = policy?.sees?.some((sight) => sight.via !== undefined) ? "a sight via a relation" : policy?.grants.some((grant) => grant.ownEnd) ? "a grant held to a person's own end of a link" : undefined;
|
|
2403
|
+
if (people.length === 0 && reads)
|
|
2404
|
+
out.push(error("people-unsaid", at, `${reads} reads each person's own record, and no kind is the people who sign in`, '{"op": "set-people", "kind": "<the kind whose records are the people>", "people": true}'));
|
|
2405
|
+
for (const sight of policy?.sees ?? []) {
|
|
2406
|
+
const via = sight.via;
|
|
2407
|
+
if (via === undefined)
|
|
2408
|
+
continue;
|
|
2409
|
+
const from = Object.keys(kinds).filter((kind) => kinds[kind].edges?.[via]);
|
|
2410
|
+
if (from.length === 0) {
|
|
2411
|
+
out.push(error("sight-via-unknown-edge", at, `no kind has a relation ${quoted(via)}, so a sight via it shows nothing`, `name a relation that joins ${listOf(sight.kinds.map((kind) => pluralOf(kinds, kind)))} to the people`));
|
|
2412
|
+
continue;
|
|
2413
|
+
}
|
|
2414
|
+
if (people.length === 0)
|
|
2415
|
+
continue;
|
|
2416
|
+
const reaches = (target) => from.some((kind) => {
|
|
2417
|
+
const to = kinds[kind].edges[via].to;
|
|
2418
|
+
return to === "*" || to.includes(target);
|
|
2419
|
+
});
|
|
2420
|
+
for (const kind of sight.kinds) {
|
|
2421
|
+
if ((from.includes(kind) && people.some(reaches)) || (people.some((one) => from.includes(one)) && reaches(kind)))
|
|
2422
|
+
continue;
|
|
2423
|
+
out.push(error("sight-via-misses-people", at, `${quoted(via)} never joins ${pluralOf(kinds, kind)} to ${pluralOf(kinds, people[0])}, so the sight shows no ${nounOf(doc, kind)} to anyone`, `name a relation between ${nounOf(doc, kind)} and ${nounOf(doc, people[0])}, or say own: true`));
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
for (const grant of policy?.grants ?? []) {
|
|
2427
|
+
if (!grant.ownEnd)
|
|
2428
|
+
continue;
|
|
2429
|
+
if (grant.mutations === "*") {
|
|
2430
|
+
out.push(error("grant-own-end-without-link", at, "a grant held to a person's own end of a link names every act, and most link nothing", "name the acts it holds: the ones that connect or sever"));
|
|
2431
|
+
continue;
|
|
2432
|
+
}
|
|
2433
|
+
for (const name of grant.mutations) {
|
|
2434
|
+
const ends = linkEnds(doc.acts?.[name]);
|
|
2435
|
+
if (ends === "far")
|
|
2436
|
+
continue;
|
|
2437
|
+
const title = quoted(doc.acts?.[name]?.title ?? name);
|
|
2438
|
+
out.push(ends === "near"
|
|
2439
|
+
? error("grant-own-end-without-link", at, `${title} takes no record at the other end of what it links, so a grant held to a person's own end of it is never met`, 'take the record at its far end as an argument, or say "$me" there and drop ownEnd')
|
|
2440
|
+
: error("grant-own-end-without-link", at, `${title} connects and severs nothing, so a grant held to a person's own end of a link has no end to hold`, "hold it with self: true for the subject, or name an act that connects or severs"));
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
return out;
|
|
2444
|
+
}
|
|
2445
|
+
/** Whether an act links, and whether it takes the record at the far end of a link as an argument: "far", "near" (only its subject, "$me" or what it made) or "none". */
|
|
2446
|
+
function linkEnds(act) {
|
|
2447
|
+
if (!act)
|
|
2448
|
+
return "none";
|
|
2449
|
+
if (act.connects || act.severs)
|
|
2450
|
+
return "far";
|
|
2451
|
+
const own = new Set(["subject", "me", "new", ...(act.effects ?? []).flatMap((e) => (e.create && e.as ? [e.as] : []))]);
|
|
2452
|
+
const links = (act.effects ?? []).filter((e) => e.connect || e.sever);
|
|
2453
|
+
if (links.length === 0)
|
|
2454
|
+
return "none";
|
|
2455
|
+
return links.some((e) => [e.from, e.to].some((end) => {
|
|
2456
|
+
const ref = refName(end);
|
|
2457
|
+
return ref !== undefined && !own.has(ref);
|
|
2458
|
+
})) ? "far" : "near";
|
|
2459
|
+
}
|
|
2242
2460
|
/** A `$name` reference or bare argument name in a guard, renamed (for an argument that followed its field). */
|
|
2243
2461
|
function rewriteBinding(source, from, to) {
|
|
2244
2462
|
try {
|
|
@@ -2260,7 +2478,6 @@ function swapToken(name, from, to) {
|
|
|
2260
2478
|
}
|
|
2261
2479
|
return name;
|
|
2262
2480
|
}
|
|
2263
|
-
const listOf = (xs) => (xs.length <= 1 ? xs.join("") : `${xs.slice(0, -1).join(", ")} and ${xs.at(-1)}`);
|
|
2264
2481
|
/** Map every block of a view, nested ones included; a block mapped to null is dropped. */
|
|
2265
2482
|
function mapBlocks(blocks, f) {
|
|
2266
2483
|
const out = [];
|
|
@@ -2281,13 +2498,12 @@ function mapBlocks(blocks, f) {
|
|
|
2281
2498
|
return out;
|
|
2282
2499
|
}
|
|
2283
2500
|
// ── words for what changed ───────────────────────────────────────────────────
|
|
2501
|
+
/** An act or a rule that goes with something, as a person reads it: 'the act "Draw a match"', by its id only when it has no title. */
|
|
2502
|
+
const titled = (what, name, spec) => `the ${what} "${spec.title ?? name}"`;
|
|
2284
2503
|
const withA = (noun) => `${/^[aeiou]/i.test(noun) ? "an" : "a"} ${noun}`;
|
|
2285
|
-
const nounOf = (doc, kind) => doc.kinds
|
|
2504
|
+
const nounOf = (doc, kind) => nounWords(doc.kinds, kind);
|
|
2286
2505
|
/** A kind's plural as a person reads it: "packages", "people and teams". */
|
|
2287
|
-
|
|
2288
|
-
const plural = doc.kinds[kind]?.plural;
|
|
2289
|
-
return plural ? plural.charAt(0).toLowerCase() + plural.slice(1) : `${words(kind)}s`;
|
|
2290
|
-
}
|
|
2506
|
+
const pluralWords = (doc, kind) => pluralOf(doc.kinds, kind);
|
|
2291
2507
|
/** The kinds a lens binds, for saying where it stands. */
|
|
2292
2508
|
function kindsBound(lens) {
|
|
2293
2509
|
const bindings = lens["bindings"];
|
|
@@ -2542,6 +2758,20 @@ export function editDocument(document, edits, options = {}) {
|
|
|
2542
2758
|
});
|
|
2543
2759
|
if (editor.findings.length > 0)
|
|
2544
2760
|
return { ok: false, findings: editor.findings };
|
|
2761
|
+
/*
|
|
2762
|
+
* WHAT THE PEOPLE AND THE POLICY MAKE TOGETHER, judged once the change is
|
|
2763
|
+
* whole (FR-170): moving the people from one kind to another is two edits,
|
|
2764
|
+
* and the first alone leaves a sight reading nobody. What the change made
|
|
2765
|
+
* wrong is refused at its last such edit; what was wrong before is not
|
|
2766
|
+
* this change's to answer for.
|
|
2767
|
+
*/
|
|
2768
|
+
if (editor.lastPolicyEdit >= 0) {
|
|
2769
|
+
const at = `edits.${editor.lastPolicyEdit}`;
|
|
2770
|
+
const before = new Set(peopleProblems(current, at).map((finding) => finding.message));
|
|
2771
|
+
const made = peopleProblems(editor.doc, at).filter((finding) => !before.has(finding.message));
|
|
2772
|
+
if (made.length > 0)
|
|
2773
|
+
return { ok: false, findings: made };
|
|
2774
|
+
}
|
|
2545
2775
|
/*
|
|
2546
2776
|
* THE TOOL INTERFACE MOVED, AND SAYS SO (FR-34). A derived act is named for
|
|
2547
2777
|
* its kind and asks for its fields by name, so a rename changes what an
|