nervur 0.8.0 → 0.8.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/README.md CHANGED
@@ -313,6 +313,62 @@ MEMBER — once, and never again. `Clock` and `Entropy` are not floor: they
313
313
  are ordinary modules a program declares, pinnable so a program never
314
314
  reaches for the wall.
315
315
 
316
+ ## Extending the administration
317
+
318
+ A ground's administration is a being like any other, and the only one whose
319
+ context holds the registry — so a door that must create, upgrade or destroy
320
+ beings belongs on it and nowhere else. Adopters extend it; nobody replaces
321
+ it.
322
+
323
+ Pass an extension through the `blueprints` module and the ground composes it
324
+ with the standard administration. Write it as an ordinary blueprint whose
325
+ schema **extends** the roots:
326
+
327
+ ```js
328
+ const cases = `({
329
+ name: 'procese.cases',
330
+ needs: { caller: true, modules: [{ module: 'memory', contract: 'nervur.memory' }] },
331
+ memory: { 'case/*': { type: 'String', class: 'data' } },
332
+ interface: \`
333
+ extend type Mutation {
334
+ openCase(title: String!): String @rights(is: [SELF, MEMBER])
335
+ }
336
+ \`,
337
+ resolvers: {
338
+ Mutation: {
339
+ openCase: (_, { title }, { from, modules, registry }) => {
340
+ const at = registry.create({ program: CASE, refs: [{ address: from }] });
341
+ modules.memory.write('case/' + at, title);
342
+ return at ?? null;
343
+ },
344
+ },
345
+ },
346
+ })`;
347
+
348
+ const ground = Ground({ cells, keychain, blueprints: { ground: () => cases } });
349
+ ```
350
+
351
+ The composed program is one blueprint with one digest, so `describe` answers
352
+ what this ground's administration actually is, and an outsider can pin it.
353
+
354
+ Three rules hold, and each refuses the whole ground rather than bending:
355
+
356
+ 1. **Standard door names are reserved.** Declare `create`, `census`,
357
+ `upgrade`, `destroy`, `alias`, `attest`, `opened` or `init` and no ground
358
+ rises. You cannot change what a standard door means — which is why a
359
+ stranger may trust that if `create` answers, it created.
360
+ 2. **Add only.** Use `extend type Query` / `extend type Mutation`; cell
361
+ names, module slots and `needs.config` may not collide with the
362
+ administration's own.
363
+ 3. **Close a door the way anything is closed here** — fence it behind a
364
+ right the ground never grants. The door stands, means what the
365
+ constitution says, and answers nobody: that is how an appliance ground
366
+ stops creating beings, without redefining a thing.
367
+
368
+ The acts stay the core's own: an extension reaches them through the same
369
+ registry face the standard doors use, so it can invent no power the standard
370
+ administration lacked.
371
+
316
372
  ## Writing a module
317
373
 
318
374
  Five rules, each load-bearing:
package/blueprint.mjs CHANGED
@@ -156,6 +156,67 @@ const resolverSources = (resolvers) =>
156
156
  .map((field) => `${type}.${field}=${resolvers[type][field].toString()}`),
157
157
  );
158
158
 
159
+ const merged = (base, added) =>
160
+ Object.fromEntries(
161
+ [...new Set([...Object.keys(base ?? {}), ...Object.keys(added ?? {})])].map((key) => [
162
+ key,
163
+ { ...(base ?? {})[key], ...(added ?? {})[key] },
164
+ ]),
165
+ );
166
+
167
+ const doorsIn = (sdl) =>
168
+ parse(sdl)
169
+ .definitions.filter(
170
+ (one) => one.kind === Kind.OBJECT_TYPE_DEFINITION || one.kind === Kind.OBJECT_TYPE_EXTENSION,
171
+ )
172
+ .flatMap((one) => (one.fields ?? []).map((field) => `${one.name.value}.${field.name.value}`));
173
+
174
+ const composed = (baseSource, addedSource) => {
175
+ try {
176
+ const base = evaluated(baseSource);
177
+ const added = evaluated(addedSource);
178
+ const standard = doorsIn(base.interface);
179
+ if (doorsIn(added.interface).some((one) => standard.includes(one))) {
180
+ return undefined;
181
+ }
182
+ const cells = { ...(base.memory ?? {}), ...(added.memory ?? {}) };
183
+ if (
184
+ Object.keys(base.memory ?? {}).some((key) => (added.memory ?? {})[key] !== undefined) ||
185
+ (added.needs?.config !== undefined && base.needs?.config !== undefined)
186
+ ) {
187
+ return undefined;
188
+ }
189
+ const slots = [...(base.needs?.modules ?? []), ...(added.needs?.modules ?? [])];
190
+ if (new Set(slots.map((one) => one.module)).size !== slots.length) {
191
+ return undefined;
192
+ }
193
+ const needs = {
194
+ ...(base.needs ?? {}),
195
+ ...(added.needs ?? {}),
196
+ caller: base.needs?.caller === true || added.needs?.caller === true,
197
+ modules: slots,
198
+ };
199
+ const doors = merged(base.resolvers, added.resolvers);
200
+ const written = Object.entries(doors)
201
+ .map(
202
+ ([type, fields]) =>
203
+ `${type}: { ${Object.entries(fields)
204
+ .map(([field, one]) => `${field}: ${one.toString()}`)
205
+ .join(', ')} }`,
206
+ )
207
+ .join(', ');
208
+ return `({
209
+ name: ${JSON.stringify(added.name)},
210
+ needs: ${JSON.stringify(needs)},
211
+ memory: ${JSON.stringify(cells)},
212
+ interface: ${JSON.stringify(`${base.interface}\n${added.interface}`)},
213
+ resolvers: { ${written} },
214
+ })`;
215
+ } catch {
216
+ return undefined;
217
+ }
218
+ };
219
+
159
220
  const inputCheck = (schema, typeName, given) => {
160
221
  const type = schema.getType(typeName);
161
222
  if (type?.getFields === undefined) {
@@ -279,4 +340,4 @@ const Blueprint = (source) => {
279
340
  }
280
341
  };
281
342
 
282
- export { Blueprint, CLASSES, CORE_ONLY, FORBIDDEN, OPEN, STRUCTURAL, coreClass, reads };
343
+ export { Blueprint, CLASSES, CORE_ONLY, FORBIDDEN, OPEN, STRUCTURAL, composed, coreClass, reads };
package/ground.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ADMIN } from './admin.mjs';
2
2
  import { Being, Compartment, Registry, make, rootFor } from './being.mjs';
3
- import { Blueprint, coreClass } from './blueprint.mjs';
3
+ import { Blueprint, composed, coreClass } from './blueprint.mjs';
4
4
  import { Client } from './client.mjs';
5
5
  import { Contracts } from './contract.mjs';
6
6
  import { Memory, Paths, Wire } from './modules.mjs';
@@ -13,22 +13,6 @@ const GROUND = 'Ground';
13
13
 
14
14
  const FLOOR = ['blueprints', 'cells', 'contracts', 'keychain', 'paths', 'watch', 'wire'];
15
15
 
16
- const doorsOf = (source) => {
17
- const written = Blueprint(source);
18
- if (written === undefined) {
19
- return undefined;
20
- }
21
- return [written.schema.getQueryType(), written.schema.getMutationType()]
22
- .filter((one) => one !== undefined && one !== null)
23
- .flatMap((one) => Object.keys(one.getFields()));
24
- };
25
-
26
- const redefines = (source) => {
27
- const custom = doorsOf(source);
28
- const standard = doorsOf(ADMIN);
29
- return custom === undefined || custom.some((one) => standard.includes(one));
30
- };
31
-
32
16
  const install = ({ cells, keychain, program }) => {
33
17
  const memory = Memory(cells);
34
18
  const voice = Voice();
@@ -58,10 +42,10 @@ const Ground = (modules = {}) => {
58
42
  }
59
43
 
60
44
  const extension = modules.blueprints?.ground();
61
- if (extension !== undefined && redefines(extension)) {
45
+ const program = extension === undefined ? ADMIN : composed(ADMIN, extension);
46
+ if (program === undefined || Blueprint(program)?.digest === undefined) {
62
47
  return undefined;
63
48
  }
64
- const program = extension ?? ADMIN;
65
49
  if (cells.peek('ground/admin') === undefined) {
66
50
  install({ cells, keychain, program });
67
51
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nervur",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "A distributed system of sovereign beings — one door, one question: by whose authority.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Razvan Gherghina",