@mark1russell7/coeval 0.3.3 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/static.ts CHANGED
@@ -4,7 +4,7 @@ import { ask } from "./ladder.ts";
4
4
  import { type PartSlot, pathMatches, splitParts } from "./parts.ts";
5
5
  import { type Cap, type Root, ROOTS, isRef } from "./deref.ts";
6
6
  import type { ErrorCode } from "./errors.ts";
7
- import { type Machine, permits } from "./evaluate.ts";
7
+ import { type Machine, partCaps, permits } from "./evaluate.ts";
8
8
 
9
9
  /** One document that a walk visits. */
10
10
  export interface DocVisit {
@@ -12,25 +12,38 @@ export interface DocVisit {
12
12
  readonly place: readonly Key[];
13
13
  /** The keys from the root JSON value to this document. A change at this path changes this document. */
14
14
  readonly at: readonly Key[];
15
+ /** The visit of the document that holds this document as a part. The root has none. */
16
+ readonly parent?: DocVisit | undefined;
17
+ /** The keys from the value of the parent to this document: the path of the part. The root has none. */
18
+ readonly slot: readonly Key[];
15
19
  /** The document. A bare value has the type that its position declares. */
16
20
  readonly doc: Envelope;
17
21
  /** `true` when the type came from the position and not from an envelope. */
18
22
  readonly declared: boolean;
19
23
  /** The kind of the document, when the world has it. */
20
- readonly kind?: Kind;
24
+ readonly kind?: Kind | undefined;
21
25
  /** The keys of the kind after each alias. */
22
- readonly typePath?: readonly Key[];
26
+ readonly typePath?: readonly Key[] | undefined;
23
27
  /** The number of documents around this document. The root has 0. */
24
28
  readonly depth: number;
25
29
  /** The names that the kinds around this document bind for it. */
26
30
  readonly names: ReadonlySet<string>;
27
31
  /** The names that a sequence around this document binds only for the parts after it. */
28
32
  readonly later: ReadonlySet<string>;
33
+ /** `true` when the position of the document declares an address: a reference there is not a read of a value. */
34
+ readonly address?: true | undefined;
35
+ /**
36
+ * The capabilities of the document: the capabilities of the walk, narrowed by the capability rules of the kinds
37
+ * around it (`Kind.caps`). A walk without capabilities gives none here.
38
+ */
39
+ readonly caps?: readonly Cap[] | undefined;
29
40
  }
30
41
 
31
42
  /** A position that a walk cannot read as a document. */
32
43
  export interface WalkProblem {
44
+ /** The keys from the root document to the document with the problem. */
33
45
  readonly place: readonly Key[];
46
+ /** The keys from the root JSON value to the value with the problem. */
34
47
  readonly at: readonly Key[];
35
48
  readonly code: "#PART?" | "#VALUE!";
36
49
  readonly reason: string;
@@ -42,6 +55,8 @@ export interface WalkOptions {
42
55
  readonly declaredType?: string;
43
56
  /** The names that the host binds at the root. */
44
57
  readonly names?: readonly string[];
58
+ /** The capabilities at the root. With them, each visit gets the capabilities of its document. */
59
+ readonly caps?: readonly Cap[];
45
60
  /**
46
61
  * This function gets each position that is not a document.
47
62
  *
@@ -120,74 +135,155 @@ function withoutNames(options: WalkOptions): WalkOptions {
120
135
  return rest;
121
136
  }
122
137
 
138
+ /** A path that shares its start with other paths: the keys of one step, and the path before them. */
139
+ interface Trail {
140
+ readonly up: Trail | undefined;
141
+ readonly keys: readonly Key[];
142
+ full?: readonly Key[];
143
+ }
144
+
145
+ /** This function gives the keys of a trail, from its start. It keeps the result in the trail. */
146
+ function keysOf(trail: Trail | undefined): readonly Key[] {
147
+ if (trail === undefined) return [];
148
+ if (trail.full !== undefined) return trail.full;
149
+ const steps: Trail[] = [];
150
+ let here: Trail | undefined = trail;
151
+ while (here !== undefined && here.full === undefined) {
152
+ steps.push(here);
153
+ here = here.up;
154
+ }
155
+ const keys: Key[] = here?.full === undefined ? [] : [...here.full];
156
+ for (let index = steps.length - 1; index >= 0; index -= 1) for (const key of (steps[index] as Trail).keys) keys.push(key);
157
+ trail.full = keys;
158
+ return keys;
159
+ }
160
+
161
+ /** A visit with its paths as trails. It makes the keys of a path only when a reader asks for them. */
162
+ class Visit implements DocVisit {
163
+ readonly #place: Trail | undefined;
164
+ readonly #at: Trail | undefined;
165
+
166
+ constructor(
167
+ place: Trail | undefined,
168
+ at: Trail | undefined,
169
+ readonly doc: Envelope,
170
+ readonly declared: boolean,
171
+ readonly depth: number,
172
+ readonly names: ReadonlySet<string>,
173
+ readonly later: ReadonlySet<string>,
174
+ readonly slot: readonly Key[],
175
+ readonly parent: DocVisit | undefined,
176
+ readonly kind: Kind | undefined,
177
+ readonly typePath: readonly Key[] | undefined,
178
+ readonly address: true | undefined,
179
+ readonly caps: readonly Cap[] | undefined,
180
+ ) {
181
+ this.#place = place;
182
+ this.#at = at;
183
+ }
184
+
185
+ get place(): readonly Key[] {
186
+ return keysOf(this.#place);
187
+ }
188
+
189
+ get at(): readonly Key[] {
190
+ return keysOf(this.#at);
191
+ }
192
+ }
193
+
123
194
  /**
124
195
  * This function visits each document of a document: the root and each part at each depth, by the contracts of the
125
- * kinds. A part of a kind that the world does not have is not visited. The walk uses no stack of the host for the
126
- * depth, thus a deep document cannot overflow it.
196
+ * kinds. A part of a kind that the world does not have is not visited. With capabilities in the options, the walk
197
+ * narrows them for each part by the rules of its kind, as a run does. The walk uses no stack of the host for the
198
+ * depth, thus a deep document cannot overflow it. The time and the memory of a walk are linear in the size of the
199
+ * document. A visit makes the keys of its place and of its path only when a reader asks for them.
127
200
  *
128
201
  * @param world - The world of the kinds.
129
202
  * @param doc - The document, as JSON.
130
203
  * @param visit - The function that gets each document. It gives `"skip"` to not visit the parts of a document.
131
- * @param options - The type of a bare root value, the names of the host, and a function for the problems.
204
+ * @param options - The type of a bare root value, the names, the capabilities and a function for the problems.
132
205
  */
133
206
  export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => void | "skip", options: WalkOptions = {}): void {
134
207
  interface Item {
135
208
  readonly value: unknown;
136
- readonly place: readonly Key[];
137
- readonly at: readonly Key[];
209
+ readonly place: Trail | undefined;
210
+ readonly at: Trail | undefined;
138
211
  readonly declared: string | undefined;
139
212
  readonly depth: number;
140
213
  readonly names: ReadonlySet<string>;
141
214
  readonly later: ReadonlySet<string>;
215
+ readonly address: boolean;
216
+ readonly slot: readonly Key[];
217
+ readonly parent: DocVisit | undefined;
218
+ readonly caps: readonly Cap[] | undefined;
142
219
  }
143
- const names: ReadonlySet<string> = options.names === undefined || options.names.length === 0 ? NO_NAMES : new Set(options.names);
144
- const stack: Item[] = [{ value: doc, place: [], at: [], declared: options.declaredType, depth: 0, names, later: NO_NAMES }];
220
+ const stack: Item[] = [
221
+ {
222
+ value: doc,
223
+ place: undefined,
224
+ at: undefined,
225
+ declared: options.declaredType,
226
+ depth: 0,
227
+ names: options.names === undefined || options.names.length === 0 ? NO_NAMES : new Set(options.names),
228
+ later: NO_NAMES,
229
+ address: false,
230
+ slot: [],
231
+ parent: undefined,
232
+ caps: options.caps,
233
+ },
234
+ ];
145
235
  while (stack.length > 0) {
146
236
  const item = stack.pop() as Item;
147
237
  const envelope = isEnvelope(item.value) ? (item.value as Envelope) : undefined;
148
238
  if (envelope === undefined && item.declared === undefined) {
149
- options.problem?.({ place: item.place, at: item.at, code: "#PART?", reason: "The value has no envelope, and its position declares no type." });
239
+ options.problem?.({ place: keysOf(item.place), at: keysOf(item.at), code: "#PART?", reason: "The value has no envelope, and its position declares no type." });
150
240
  continue;
151
241
  }
152
242
  const doc: Envelope = envelope ?? { type: item.declared ?? "?", value: item.value as Json };
153
243
  const found = lookupKind(world, doc.type);
154
244
  const kind = found.ok ? found.kind : undefined;
155
- const seen: DocVisit =
156
- kind === undefined
157
- ? { place: item.place, at: item.at, doc, declared: envelope === undefined, depth: item.depth, names: item.names, later: item.later }
158
- : {
159
- place: item.place,
160
- at: item.at,
161
- doc,
162
- declared: envelope === undefined,
163
- kind,
164
- typePath: found.ok ? found.path : [],
165
- depth: item.depth,
166
- names: item.names,
167
- later: item.later,
168
- };
169
- const answer = visit(seen);
245
+ const here = new Visit(
246
+ item.place,
247
+ item.at,
248
+ doc,
249
+ envelope === undefined,
250
+ item.depth,
251
+ item.names,
252
+ item.later,
253
+ item.slot,
254
+ item.parent,
255
+ kind,
256
+ found.ok ? found.path : undefined,
257
+ item.address ? true : undefined,
258
+ item.caps,
259
+ );
260
+ const answer = visit(here);
170
261
  if (answer === "skip" || kind === undefined) continue;
171
262
  const split = splitParts(kind.parts, doc.value);
263
+ const base: Trail | undefined = envelope === undefined ? item.at : { up: item.at, keys: ["value"] };
172
264
  if (!split.ok) {
173
265
  const at = split.at.length === 0 ? "" : ` (at ${split.at.join(".")})`;
174
- options.problem?.({ place: item.place, at: item.at, code: "#VALUE!", reason: `${split.reason}${at} The kind is ${typeName(doc.type)}.` });
266
+ const where = keysOf({ up: base, keys: split.at });
267
+ options.problem?.({ place: keysOf(item.place), at: where, code: "#VALUE!", reason: `${split.reason}${at} The kind is ${typeName(doc.type)}.` });
175
268
  continue;
176
269
  }
177
270
  const { sees, later } = scopesOf(kind.scope, doc.value, split.parts);
178
- const base = envelope === undefined ? item.at : [...item.at, "value"];
179
271
  for (let index = split.parts.length - 1; index >= 0; index -= 1) {
180
272
  const slot = split.parts[index] as PartSlot;
181
273
  const extra = sees[index] ?? [];
182
274
  const hidden = later[index] ?? [];
183
275
  stack.push({
184
276
  value: slot.item,
185
- place: [...item.place, ...slot.path],
186
- at: [...base, ...slot.path],
277
+ place: slot.path.length === 0 ? item.place : { up: item.place, keys: slot.path },
278
+ at: slot.path.length === 0 ? base : { up: base, keys: slot.path },
187
279
  declared: slot.of,
188
280
  depth: item.depth + 1,
189
281
  names: extra.length === 0 ? item.names : new Set([...item.names, ...extra]),
190
282
  later: hidden.length === 0 ? item.later : new Set([...item.later, ...hidden]),
283
+ address: slot.address === true,
284
+ slot: slot.path,
285
+ parent: here,
286
+ caps: item.caps === undefined || kind.caps === undefined ? item.caps : partCaps(kind.caps, slot.path, item.caps),
191
287
  });
192
288
  }
193
289
  }
@@ -195,6 +291,8 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
195
291
 
196
292
  /** One reference of a document. */
197
293
  export interface RefUse {
294
+ /** The visit of the reference document: its parent, its depth and the names that it sees. */
295
+ readonly visit: DocVisit;
198
296
  /** The place of the reference document, through the parts. */
199
297
  readonly place: readonly Key[];
200
298
  /** The keys from the root JSON value to the reference document. */
@@ -204,12 +302,14 @@ export interface RefUse {
204
302
  readonly root: string;
205
303
  /** `false` when a kind around the reference binds its first name. A reference to another root is free. */
206
304
  readonly free: boolean;
305
+ /** `true` when the reference is an address: its position declares it. An address does not read a value. */
306
+ readonly address: boolean;
207
307
  }
208
308
 
209
309
  /**
210
310
  * This function finds each reference of a document: each document of the type `ref` that a walk reaches. A
211
311
  * reference to a name that a kind around it binds is not free. The others are what the document reads from
212
- * outside: its dependencies.
312
+ * outside: its dependencies. When the kind `ref` has parts, the walk also finds the references in its step documents.
213
313
  *
214
314
  * @param world - The world of the kinds.
215
315
  * @param doc - The document, as JSON.
@@ -229,8 +329,13 @@ export function refsOf(world: World, doc: unknown, options: WalkOptions = {}): r
229
329
  const root = typeof path[0] === "string" ? path[0] : "";
230
330
  const first = path[1];
231
331
  const bound = root === "names" && isKey(first) && v.names.has(String(first));
232
- out.push({ place: v.place, at: v.at, path, root, free: !bound });
233
- return "skip";
332
+ // The place and the path of a reference are made only when a reader asks for them.
333
+ const use = { path, root, free: !bound, address: v.address === true } as RefUse;
334
+ Object.defineProperty(use, "place", { enumerable: true, get: () => v.place });
335
+ Object.defineProperty(use, "at", { enumerable: true, get: () => v.at });
336
+ Object.defineProperty(use, "visit", { enumerable: false, value: v });
337
+ out.push(use);
338
+ return undefined;
234
339
  },
235
340
  rest,
236
341
  );
@@ -278,6 +383,8 @@ export interface Issue {
278
383
  /** The keys from the root document to the document with the problem. */
279
384
  readonly place: readonly Key[];
280
385
  readonly reason: string;
386
+ /** The keys from the root JSON value to the value with the problem. */
387
+ readonly at?: readonly Key[];
281
388
  }
282
389
 
283
390
  /** The options of a check. */
@@ -286,10 +393,27 @@ export interface CheckOptions {
286
393
  readonly declaredType?: string;
287
394
  /** The names that the host binds at the root. `"any"` does not check the names. The default is no names. */
288
395
  readonly names?: readonly string[] | "any";
289
- /** The capabilities at the root. Without them, the check does not check the effects. */
396
+ /**
397
+ * The capabilities at the root. Without them, the check does not check the effects. The check narrows them for
398
+ * each part by the capability rules of the kinds, as a run does.
399
+ */
290
400
  readonly caps?: readonly Cap[];
291
401
  }
292
402
 
403
+ /**
404
+ * This function gives the reason of a `#CAP!`. When a kind around the document narrowed the capabilities, the
405
+ * reason names the nearest such kind.
406
+ */
407
+ function capReason(v: DocVisit, kind: readonly Key[]): string {
408
+ const text = kind.join(".");
409
+ for (let up = v.parent; up !== undefined; up = up.parent) {
410
+ if (up.caps !== undefined && permits(up.caps, kind)) {
411
+ return `The capabilities do not permit the effect ${text} here. The kind ${typeName(up.doc.type)} around it narrows them for its parts.`;
412
+ }
413
+ }
414
+ return `The capabilities do not permit the effect ${text}.`;
415
+ }
416
+
293
417
  /**
294
418
  * This function checks a document before an evaluation, by the contracts of its kinds. It finds the problems
295
419
  * that an evaluation finds when it reaches them:
@@ -301,7 +425,7 @@ export interface CheckOptions {
301
425
  * - `#DEPTH!`: too much nesting.
302
426
  * - `#REF!` and `#STEP?`: a bad path.
303
427
  * - `#NAME?`: a name that nothing binds.
304
- * - `#CAP!`: an effect that the capabilities do not permit.
428
+ * - `#CAP!`: an effect that the capabilities of its document do not permit. A kind around the document can narrow them.
305
429
  *
306
430
  * @param machine - The world and the code of the evaluators.
307
431
  * @param doc - The document, as JSON.
@@ -319,34 +443,41 @@ export function check(machine: Machine, doc: unknown, options: CheckOptions = {}
319
443
  doc,
320
444
  (v) => {
321
445
  if (v.depth > maxDepth) {
322
- issues.push({ code: "#DEPTH!", place: v.place, reason: `The nesting is deeper than ${maxDepth} documents.` });
446
+ issues.push({ code: "#DEPTH!", place: v.place, reason: `The nesting is deeper than ${maxDepth} documents.`, at: v.at });
323
447
  return "skip";
324
448
  }
325
449
  const header = v.declared ? {} : v.doc;
326
450
  const answer = ask(machine.world, header, "eval", v.declared ? { declaredType: typeName(v.doc.type) } : {});
327
451
  if (answer.error === "unknown-kind") {
328
- issues.push({ code: "#KIND?", place: v.place, reason: `The world has no kind ${typeName(v.doc.type)}.` });
452
+ issues.push({ code: "#KIND?", place: v.place, reason: `The world has no kind ${typeName(v.doc.type)}.`, at: v.at });
329
453
  return "skip";
330
454
  }
331
455
  const name = answer.evaluator;
332
456
  if (name === undefined || (!has(machine.evaluators, name) && !has(machine.runners, name))) {
333
- issues.push({ code: "#NOEVAL", place: v.place, reason: `No rung gives an evaluator for ${typeName(v.doc.type)}.` });
457
+ issues.push({ code: "#NOEVAL", place: v.place, reason: `No rung gives an evaluator for ${typeName(v.doc.type)}.`, at: v.at });
334
458
  }
335
- if (options.caps !== undefined && v.kind?.effects !== undefined) {
459
+ const caps = v.caps;
460
+ if (caps !== undefined && v.kind?.effects !== undefined) {
336
461
  const kinds: readonly (readonly Key[])[] = v.kind.effects === "type" ? [v.typePath ?? []] : v.kind.effects.map((kind) => kind.split("."));
337
462
  for (const kind of kinds) {
338
- if (!permits(options.caps, kind)) issues.push({ code: "#CAP!", place: v.place, reason: `The capabilities do not permit the effect ${kind.join(".")}.` });
463
+ if (!permits(caps, kind)) issues.push({ code: "#CAP!", place: v.place, reason: capReason(v, kind), at: v.at });
339
464
  }
340
465
  }
341
- if (!isRef(v.doc)) return undefined;
466
+ const isPath = isRef(v.doc);
467
+ if (!isPath && v.typePath?.length === 1 && v.typePath[0] === "ref") {
468
+ issues.push({ code: "#VALUE!", place: v.place, reason: "The value of ref is a path: a list that starts with a root.", at: [...v.at, ...(v.declared ? [] : ["value"])] });
469
+ }
470
+ if (!isPath) return undefined;
342
471
  const path = v.doc.value as readonly Json[];
343
472
  const root = path[0];
473
+ // The JSON path of a problem of a path goes to its step.
474
+ const stepAt = (index: number): readonly Key[] => [...v.at, "value", ...(index < path.length ? [index] : [])];
344
475
  if (typeof root !== "string" || !(ROOTS as readonly string[]).includes(root)) {
345
- issues.push({ code: "#REF!", place: v.place, reason: `A path starts with a root: ${ROOTS.join(", ")}.` });
476
+ issues.push({ code: "#REF!", place: v.place, reason: `A path starts with a root: ${ROOTS.join(", ")}.`, at: stepAt(0) });
346
477
  return "skip";
347
478
  }
348
479
  if (!roots.includes(root)) {
349
- issues.push({ code: "#REF!", place: v.place, reason: `The root ${root} is not open here. The open roots are ${roots.join(", ")}.` });
480
+ issues.push({ code: "#REF!", place: v.place, reason: `The root ${root} is not open here. The open roots are ${roots.join(", ")}.`, at: stepAt(0) });
350
481
  }
351
482
  const first = path[1];
352
483
  if (root === ("names" as Root) && !anyName && isKey(first) && !v.names.has(String(first))) {
@@ -355,22 +486,25 @@ export function check(machine: Machine, doc: unknown, options: CheckOptions = {}
355
486
  code: "#NAME?",
356
487
  place: v.place,
357
488
  reason: later ? `The name ${String(first)} is bound by a later step.` : `Nothing binds the name ${String(first)}.`,
489
+ at: stepAt(1),
358
490
  });
359
491
  }
360
- path.slice(1).forEach((step) => {
361
- if (isKey(step)) return;
492
+ path.forEach((step, index) => {
493
+ if (index === 0 || isKey(step)) return;
362
494
  const found = isEnvelope(step) ? lookupKind(machine.world, step.type) : undefined;
363
495
  const walker = found?.ok === true && Object.hasOwn(found.kind.verbs, "step") ? found.kind.verbs["step"] : undefined;
364
496
  if (walker === undefined || !has(machine.walkers, walker)) {
365
- issues.push({ code: "#STEP?", place: v.place, reason: "A step is a key, or a document of a kind with the verb step." });
497
+ issues.push({ code: "#STEP?", place: v.place, reason: "A step is a key, or a document of a kind with the verb step.", at: stepAt(index) });
366
498
  }
367
499
  });
368
- return "skip";
500
+ // The step documents of the path are parts when the kind ref has a deep shape. The walk checks them too.
501
+ return undefined;
369
502
  },
370
503
  {
371
504
  ...(options.declaredType === undefined ? {} : { declaredType: options.declaredType }),
372
505
  ...(Array.isArray(options.names) ? { names: options.names } : {}),
373
- problem: (problem) => issues.push({ code: problem.code, place: problem.place, reason: problem.reason }),
506
+ ...(options.caps === undefined ? {} : { caps: options.caps }),
507
+ problem: (problem) => issues.push({ code: problem.code, place: problem.place, reason: problem.reason, at: problem.at }),
374
508
  },
375
509
  );
376
510
  return issues;
package/src/stdlib.ts CHANGED
@@ -69,18 +69,21 @@ export const STD_WORLD: World = {
69
69
  pure: {
70
70
  verbs: { eval: "control.pure", view: "view.op" },
71
71
  parts: { shape: "list" },
72
+ caps: [{ rule: "mask" }],
72
73
  summary: "It evaluates its part without capabilities, thus an effect in it fails.",
73
74
  },
74
75
  log: {
75
76
  verbs: { eval: "effect.log", view: "view.op" },
76
77
  parts: { shape: "list", of: "text" },
77
78
  side: "run",
79
+ effects: ["log"],
78
80
  summary: "It writes the result of its part to the log. This is an effect.",
79
81
  },
80
82
  write: {
81
83
  verbs: { eval: "effect.write", view: "view.op" },
82
84
  parts: { shape: "record", fields: ["value"] },
83
85
  side: "run",
86
+ effects: ["write"],
84
87
  summary: "It writes the result of its part to the store, at its key. This is an effect.",
85
88
  },
86
89
  },
@@ -269,10 +272,11 @@ export const STD_RUNNERS: Readonly<Record<string, Runner>> = {
269
272
  }
270
273
  return ok(last);
271
274
  },
272
- "control.pure": function* ({ parts, context }) {
275
+ // The contract of pure masks the capabilities of its part, and the kernel applies it. Thus the runner does not.
276
+ "control.pure": function* ({ parts }) {
273
277
  const only = parts[0];
274
278
  if (only === undefined) return fail("#VALUE!", "pure needs one part.");
275
- return yield* only.evaluate({ ...context, caps: [] });
279
+ return yield* only.evaluate();
276
280
  },
277
281
  "control.if": function* ({ parts }) {
278
282
  const test = parts.find((part) => part.key === "test");
package/src/world.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type Envelope, type Json, type Key, type TypeRef, field, isRecord, typePath } from "./doc.ts";
2
+ import type { Cap } from "./deref.ts";
2
3
 
3
4
  /** The two sides of evaluation. `derive` is pure. `run` causes effects in order. */
4
5
  export type Side = "derive" | "run";
@@ -14,11 +15,16 @@ export type Side = "derive" | "run";
14
15
  * - `record`: the value is a record. Each named field has its shape. A list of names makes each named field a
15
16
  * part. The other fields have the shape `rest` (the default is data). `required` names the fields that must be.
16
17
  * - `deep`: each envelope at any depth of the value is a part, and the rest is data.
18
+ * - `text`, `number` and `one`: the value is data of that type. `one` names the values that it permits.
19
+ *
20
+ * A `term` with `address: true` is an address: a reference there gives what its path reaches, not the value. A
21
+ * `term` with `kinds` permits only a part whose type starts with one of the paths. A `record` with `closed: true`
22
+ * permits only its named fields. A `list` with `distinct: true` permits no two equal items.
17
23
  */
18
24
  export type Shape =
19
25
  | { readonly shape: "none" }
20
- | { readonly shape: "term"; readonly of?: string }
21
- | { readonly shape: "list"; readonly of?: string; readonly item?: Shape }
26
+ | { readonly shape: "term"; readonly of?: string; readonly address?: boolean; readonly kinds?: readonly (readonly Key[])[] }
27
+ | { readonly shape: "list"; readonly of?: string; readonly item?: Shape; readonly distinct?: boolean }
22
28
  | { readonly shape: "map"; readonly of?: string; readonly item?: Shape }
23
29
  | {
24
30
  readonly shape: "record";
@@ -26,8 +32,12 @@ export type Shape =
26
32
  readonly of?: string;
27
33
  readonly required?: readonly string[];
28
34
  readonly rest?: Shape;
35
+ readonly closed?: boolean;
29
36
  }
30
- | { readonly shape: "deep"; readonly of?: string };
37
+ | { readonly shape: "deep"; readonly of?: string }
38
+ | { readonly shape: "text" }
39
+ | { readonly shape: "number"; readonly whole?: boolean; readonly min?: number; readonly max?: number }
40
+ | { readonly shape: "one"; readonly values: readonly Json[] };
31
41
 
32
42
  /** The former name of `Shape`. */
33
43
  export type PartsSpec = Shape;
@@ -60,6 +70,22 @@ export interface ScopeRule {
60
70
  readonly sequential?: boolean;
61
71
  }
62
72
 
73
+ /**
74
+ * A capability rule of a kind: what the kind gives some of its parts from the capabilities of its document. A rule
75
+ * only narrows. A part does not get a capability that its document does not have (CAP.NARROW).
76
+ *
77
+ * - `same`: the parts get the capabilities of the document. This is the default.
78
+ * - `mask`: the parts get no capabilities, as the part of `pure`.
79
+ * - `only`: the parts get only the effects that the capabilities of the document and the list `caps` both permit.
80
+ *
81
+ * `in` names the parts that the rule covers, as patterns of their paths in the value. The key `"*"` matches each
82
+ * key. A rule without `in` covers each part.
83
+ */
84
+ export type CapsRule =
85
+ | { readonly rule: "same"; readonly in?: readonly (readonly Key[])[] }
86
+ | { readonly rule: "mask"; readonly in?: readonly (readonly Key[])[] }
87
+ | { readonly rule: "only"; readonly caps: readonly Cap[]; readonly in?: readonly (readonly Key[])[] };
88
+
63
89
  /** A kind gives the name of an evaluator for each verb, and the contract of its value. */
64
90
  export interface Kind {
65
91
  readonly verbs: Readonly<Record<string, string>>;
@@ -67,6 +93,13 @@ export interface Kind {
67
93
  readonly parts?: Shape;
68
94
  /** The names that the kind binds for its parts. */
69
95
  readonly scope?: readonly ScopeRule[];
96
+ /**
97
+ * The capabilities that the kind gives its parts, as rules. A part that no rule covers gets the capabilities of
98
+ * the document. A part that several rules cover gets only what each rule permits. The static kit reads the rules,
99
+ * and the kernel narrows the context of each part by them. Thus an evaluator does not narrow the capabilities
100
+ * itself.
101
+ */
102
+ readonly caps?: readonly CapsRule[];
70
103
  /** The side of the kind. A kind of the run side causes effects. The default is `derive`. */
71
104
  readonly side?: Side;
72
105
  /**
@@ -209,6 +242,9 @@ export function entriesAsJson(entries: Readonly<Record<string, WorldEntry>>): Js
209
242
 
210
243
  function describeShape(shape: Shape | undefined): Json {
211
244
  if (shape === undefined || shape.shape === "none") return "none";
245
+ if (shape.shape === "text") return "a text";
246
+ if (shape.shape === "number") return "a number";
247
+ if (shape.shape === "one") return `one of ${JSON.stringify(shape.values)}`;
212
248
  if (shape.shape === "term") return `a part${shape.of === undefined ? "" : ` of ${shape.of}`}`;
213
249
  if (shape.shape === "deep") return "each document in the value";
214
250
  if (shape.shape === "list") return `a list${shape.of === undefined ? "" : ` of ${shape.of}`}`;