@graview/core 0.1.27 → 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/document/compiled.js +2 -2
- package/dist/document/compiled.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 +244 -17
- package/dist/document/edit.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/template.d.ts.map +1 -1
- package/dist/document/template.js +3 -6
- package/dist/document/template.js.map +1 -1
- package/dist/document.schema.json +2 -2
- package/dist/permissions/policy.d.ts.map +1 -1
- package/dist/permissions/policy.js +2 -2
- package/dist/permissions/policy.js.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/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";
|
|
@@ -18,7 +18,9 @@ import { isRecord as isObject, respellDocument } from "./respell.js";
|
|
|
18
18
|
import { refName } from "./compiled.js";
|
|
19
19
|
import { accentProblem } from "../theme/accent.js";
|
|
20
20
|
import { pageFieldFindings, pageNames, renamedOnPage, withoutOnPage } from "./page-fields.js";
|
|
21
|
-
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";
|
|
22
24
|
/** The ops, for tool schemas and the guide. */
|
|
23
25
|
export const EDIT_OPS = [
|
|
24
26
|
"add-kind",
|
|
@@ -52,6 +54,11 @@ export const EDIT_OPS = [
|
|
|
52
54
|
"set-home",
|
|
53
55
|
"arrange-pages",
|
|
54
56
|
"set-computed",
|
|
57
|
+
"set-people",
|
|
58
|
+
"add-grant",
|
|
59
|
+
"remove-grant",
|
|
60
|
+
"add-sight",
|
|
61
|
+
"remove-sight",
|
|
55
62
|
];
|
|
56
63
|
/** Words the rule language keeps: a field or relation renamed to one could not be named in a rule. */
|
|
57
64
|
const KEYWORDS = new Set(["and", "or", "not", "in", "where", "true", "false", "null"]);
|
|
@@ -150,6 +157,12 @@ const SHAPES = {
|
|
|
150
157
|
description: z.union([z.string().min(1).max(500), z.null()]).optional(),
|
|
151
158
|
})
|
|
152
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(),
|
|
153
166
|
};
|
|
154
167
|
const NONE = new Set();
|
|
155
168
|
const clone = (v) => structuredClone(v);
|
|
@@ -785,6 +798,13 @@ class Editor {
|
|
|
785
798
|
return this.arrangePages(i, e);
|
|
786
799
|
case "set-computed":
|
|
787
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);
|
|
788
808
|
}
|
|
789
809
|
}
|
|
790
810
|
/** How a record's page orders and groups its facts (FR-148): judged as the compile judges `kinds.<kind>.page`. */
|
|
@@ -1136,6 +1156,120 @@ class Editor {
|
|
|
1136
1156
|
else
|
|
1137
1157
|
this.said.push(`${plural}' "${name}" is ${next.label ? `shown as "${next.label}"` : "shown by its name"}${next.description ? `, described as "${next.description}"` : ""}.`);
|
|
1138
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
|
+
}
|
|
1139
1273
|
// ── kinds ──
|
|
1140
1274
|
addKind(i, e) {
|
|
1141
1275
|
const { op: _op, kind, act, ...rest } = e;
|
|
@@ -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) {
|
|
@@ -1679,7 +1830,7 @@ class Editor {
|
|
|
1679
1830
|
continue;
|
|
1680
1831
|
const kept = rule.repairs.filter((r) => r.act !== act);
|
|
1681
1832
|
if (kept.length !== rule.repairs.length)
|
|
1682
|
-
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)}`);
|
|
1683
1834
|
if (kept.length > 0)
|
|
1684
1835
|
rule.repairs = kept;
|
|
1685
1836
|
else
|
|
@@ -2035,14 +2186,14 @@ class Editor {
|
|
|
2035
2186
|
}
|
|
2036
2187
|
}
|
|
2037
2188
|
if (uses) {
|
|
2038
|
-
gone.push(
|
|
2189
|
+
gone.push(titled("act", name, act), ...this.removeActQuietly(name));
|
|
2039
2190
|
}
|
|
2040
2191
|
}
|
|
2041
2192
|
for (const [name, rule] of Object.entries(doc.rules ?? {})) {
|
|
2042
2193
|
const ctx = rule.over === "graph" ? NONE : new Set([rule.over]);
|
|
2043
2194
|
if (exprMentions(doc, rule.when, ctx, r) || exprMentions(doc, rule.require, ctx, r) || templateMentions(doc, rule.says, ctx, r)) {
|
|
2044
2195
|
delete doc.rules[name];
|
|
2045
|
-
gone.push(
|
|
2196
|
+
gone.push(titled("rule", name, rule));
|
|
2046
2197
|
}
|
|
2047
2198
|
}
|
|
2048
2199
|
if (doc.rules && Object.keys(doc.rules).length === 0)
|
|
@@ -2242,6 +2393,70 @@ class Editor {
|
|
|
2242
2393
|
return said;
|
|
2243
2394
|
}
|
|
2244
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
|
+
}
|
|
2245
2460
|
/** A `$name` reference or bare argument name in a guard, renamed (for an argument that followed its field). */
|
|
2246
2461
|
function rewriteBinding(source, from, to) {
|
|
2247
2462
|
try {
|
|
@@ -2263,7 +2478,6 @@ function swapToken(name, from, to) {
|
|
|
2263
2478
|
}
|
|
2264
2479
|
return name;
|
|
2265
2480
|
}
|
|
2266
|
-
const listOf = (xs) => (xs.length <= 1 ? xs.join("") : `${xs.slice(0, -1).join(", ")} and ${xs.at(-1)}`);
|
|
2267
2481
|
/** Map every block of a view, nested ones included; a block mapped to null is dropped. */
|
|
2268
2482
|
function mapBlocks(blocks, f) {
|
|
2269
2483
|
const out = [];
|
|
@@ -2284,13 +2498,12 @@ function mapBlocks(blocks, f) {
|
|
|
2284
2498
|
return out;
|
|
2285
2499
|
}
|
|
2286
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}"`;
|
|
2287
2503
|
const withA = (noun) => `${/^[aeiou]/i.test(noun) ? "an" : "a"} ${noun}`;
|
|
2288
|
-
const nounOf = (doc, kind) => doc.kinds
|
|
2504
|
+
const nounOf = (doc, kind) => nounWords(doc.kinds, kind);
|
|
2289
2505
|
/** A kind's plural as a person reads it: "packages", "people and teams". */
|
|
2290
|
-
|
|
2291
|
-
const plural = doc.kinds[kind]?.plural;
|
|
2292
|
-
return plural ? plural.charAt(0).toLowerCase() + plural.slice(1) : `${words(kind)}s`;
|
|
2293
|
-
}
|
|
2506
|
+
const pluralWords = (doc, kind) => pluralOf(doc.kinds, kind);
|
|
2294
2507
|
/** The kinds a lens binds, for saying where it stands. */
|
|
2295
2508
|
function kindsBound(lens) {
|
|
2296
2509
|
const bindings = lens["bindings"];
|
|
@@ -2545,6 +2758,20 @@ export function editDocument(document, edits, options = {}) {
|
|
|
2545
2758
|
});
|
|
2546
2759
|
if (editor.findings.length > 0)
|
|
2547
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
|
+
}
|
|
2548
2775
|
/*
|
|
2549
2776
|
* THE TOOL INTERFACE MOVED, AND SAYS SO (FR-34). A derived act is named for
|
|
2550
2777
|
* its kind and asks for its fields by name, so a rename changes what an
|