@graview/core 0.1.14 → 0.1.15
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 +12 -4
- package/dist/capabilities.js +1 -1
- package/dist/capabilities.js.map +1 -1
- package/dist/cli/check/hidden-reads.js +1 -1
- package/dist/cli/check/hidden-reads.js.map +1 -1
- package/dist/compiled.d.ts +19 -0
- package/dist/compiled.d.ts.map +1 -0
- package/dist/compiled.js +60 -0
- package/dist/compiled.js.map +1 -0
- package/dist/conformance/fixtures.d.ts.map +1 -1
- package/dist/conformance/fixtures.js +22 -0
- package/dist/conformance/fixtures.js.map +1 -1
- package/dist/document/blocks.d.ts.map +1 -1
- package/dist/document/blocks.js +2 -1
- package/dist/document/blocks.js.map +1 -1
- package/dist/document/compile.d.ts +4 -29
- package/dist/document/compile.d.ts.map +1 -1
- package/dist/document/compile.js +72 -382
- package/dist/document/compile.js.map +1 -1
- package/dist/document/compiled.d.ts +130 -0
- package/dist/document/compiled.d.ts.map +1 -0
- package/dist/document/compiled.js +466 -0
- package/dist/document/compiled.js.map +1 -0
- package/dist/document/edit.d.ts.map +1 -1
- package/dist/document/edit.js +1 -1
- package/dist/document/edit.js.map +1 -1
- package/dist/document/index.d.ts +4 -1
- package/dist/document/index.d.ts.map +1 -1
- package/dist/document/index.js +3 -1
- package/dist/document/index.js.map +1 -1
- package/dist/document/remembered.d.ts +6 -0
- package/dist/document/remembered.d.ts.map +1 -0
- package/dist/document/remembered.js +16 -0
- package/dist/document/remembered.js.map +1 -0
- 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.map +1 -1
- package/dist/document/schema.js +10 -2
- package/dist/document/schema.js.map +1 -1
- package/dist/document/template-parse.d.ts +18 -0
- package/dist/document/template-parse.d.ts.map +1 -0
- package/dist/document/template-parse.js +124 -0
- package/dist/document/template-parse.js.map +1 -0
- package/dist/document/template.d.ts +1 -17
- package/dist/document/template.d.ts.map +1 -1
- package/dist/document/template.js +6 -115
- package/dist/document/template.js.map +1 -1
- package/dist/document/thumbnail.d.ts +4 -1
- package/dist/document/thumbnail.d.ts.map +1 -1
- package/dist/document/thumbnail.js +32 -4
- package/dist/document/thumbnail.js.map +1 -1
- package/dist/document/to-document.d.ts +1 -4
- package/dist/document/to-document.d.ts.map +1 -1
- package/dist/document/to-document.js +2 -9
- package/dist/document/to-document.js.map +1 -1
- package/dist/document/views.d.ts.map +1 -1
- package/dist/document/views.js +1 -1
- package/dist/document/views.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mutations/define-mutation.d.ts +12 -0
- package/dist/mutations/define-mutation.d.ts.map +1 -1
- package/dist/mutations/define-mutation.js +37 -9
- package/dist/mutations/define-mutation.js.map +1 -1
- package/dist/mutations/derive-edits.js +1 -1
- package/dist/mutations/derive-edits.js.map +1 -1
- package/dist/refusal.d.ts +11 -6
- package/dist/refusal.d.ts.map +1 -1
- package/dist/refusal.js +11 -6
- package/dist/refusal.js.map +1 -1
- package/dist/refused.d.ts +13 -6
- package/dist/refused.d.ts.map +1 -1
- package/dist/refused.js +14 -7
- package/dist/refused.js.map +1 -1
- package/dist/scaffold/project.d.ts.map +1 -1
- package/dist/scaffold/project.js +1 -0
- package/dist/scaffold/project.js.map +1 -1
- package/dist/schema/json-schema.d.ts.map +1 -1
- package/dist/schema/json-schema.js +4 -1
- package/dist/schema/json-schema.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compile.d.ts","sourceRoot":"","sources":["../../src/document/compile.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"compile.d.ts","sourceRoot":"","sources":["../../src/document/compile.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAE3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAEpD,OAAO,EAA6B,KAAK,OAAO,EAAE,MAAM,eAAe,CAAC;AACxE,OAAO,EAGL,KAAK,OAAO,EAGZ,KAAK,eAAe,EAErB,MAAM,aAAa,CAAC;AAOrB,OAAO,EAML,KAAK,SAAS,EAId,KAAK,gBAAgB,EAGrB,KAAK,eAAe,EAErB,MAAM,eAAe,CAAC;AAEvB,YAAY,EAAE,SAAS,EAAE,gBAAgB,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAkBjG,MAAM,WAAW,cAAc;IAC7B,6DAA6D;IAC7D,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,MAAM,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,eAAe,CAAC;CACrC;AAID,mGAAmG;AACnG,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG;IAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,eAAe,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAA;CAAE,CAoCzH;AA2BD,wBAAgB,UAAU,CAAC,QAAQ,EAAE,eAAe,GAAG,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAW5E;AAED,sDAAsD;AACtD,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,eAAe,GAAG,SAAS,SAAS,EAAE,CA+BvF;AAmPD;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,GAAE,cAAmB,GAAG,gBAAgB,GAAG,eAAe,CAS1H;AAuID,4GAA4G;AAC5G,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,GAAG,MAAM,CAM9E;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,GAAG,OAAO,CAMnG;AAED,OAAO,EAAE,UAAU,EAAE,CAAC"}
|
package/dist/document/compile.js
CHANGED
|
@@ -1,20 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import * as z from "../schema/zod.js";
|
|
3
|
-
import { refTo } from "../mutations/node-ref.js";
|
|
1
|
+
import { brandFromAccent, SCHEMES } from "../index.js";
|
|
4
2
|
import { ActRefusal } from "../refused.js";
|
|
5
3
|
import { analyzeExpr } from "./expr/analyze.js";
|
|
6
|
-
import { rememberDocument } from "./to-document.js";
|
|
7
|
-
import { evaluateExpr, ExprBudgetError, ExprEvalError } from "./expr/evaluate.js";
|
|
8
4
|
import { ExprSyntaxError, parseExpr } from "./expr/parse.js";
|
|
9
5
|
import { error, hasErrors, warning } from "./findings.js";
|
|
10
6
|
import { DocumentSpec, MAX_DOCUMENT_BYTES, } from "./schema.js";
|
|
11
|
-
import { parseTemplate,
|
|
7
|
+
import { parseTemplate, TemplateError } from "./template-parse.js";
|
|
12
8
|
import { upgradeDocument } from "./upgrade.js";
|
|
13
9
|
import { homeOf, validateViews, viewsOf } from "./views.js";
|
|
14
10
|
import { farEnd } from "./far-end.js";
|
|
15
11
|
import { computedOf, parsedComputed, validateComputed, workedOutAlone } from "./computed.js";
|
|
12
|
+
import { build, COMPILED_FORMAT, defaultLabelField, refName, SUBJECT_ARG, } from "./compiled.js";
|
|
16
13
|
const BUILTIN_REFS = new Set(["subject", "now", "today"]);
|
|
17
|
-
const SUBJECT_ARG = "id";
|
|
18
14
|
/** Parse and validate without compiling — the document's own sentences, before the framework's. */
|
|
19
15
|
export function readDocument(raw) {
|
|
20
16
|
if (typeof raw === "string") {
|
|
@@ -129,7 +125,6 @@ export function effectsOf(act, document) {
|
|
|
129
125
|
effects.push(...act.effects.filter((e) => !(act.creates && "set" in e && !("create" in e) && refName(e.target) === "new")));
|
|
130
126
|
return effects;
|
|
131
127
|
}
|
|
132
|
-
const refName = (v) => (typeof v === "string" && /^\$[A-Za-z][A-Za-z0-9]*$/.test(v) ? v.slice(1) : undefined);
|
|
133
128
|
function tryExpr(source, path, findings) {
|
|
134
129
|
try {
|
|
135
130
|
const expr = parseExpr(source);
|
|
@@ -351,67 +346,8 @@ function validate(document) {
|
|
|
351
346
|
findings.push(...validateComputed(new Map(Object.entries(document.kinds).map(([kind, spec]) => [kind, { fields: new Set(Object.keys(spec.fields)), edges: new Set(Object.keys(spec.edges ?? {})), computed: computedOf(spec) }])), (kind, name) => `kinds.${kind}.computed.${name}`));
|
|
352
347
|
return findings;
|
|
353
348
|
}
|
|
354
|
-
/*
|
|
355
|
-
* A FIELD'S SCHEMA, in zod/mini (schema/zod.ts): a page that compiles a
|
|
356
|
-
* document in the browser pays for the checks it calls, not for classic
|
|
357
|
-
* zod whole (FR-57). The checks, their messages and their definitions are
|
|
358
|
-
* the ones classic's methods make.
|
|
359
|
-
*/
|
|
360
|
-
function fieldSchema(spec, optional) {
|
|
361
|
-
let schema;
|
|
362
|
-
switch (spec.type) {
|
|
363
|
-
case "string":
|
|
364
|
-
schema = z.string().check(z.maxLength(500));
|
|
365
|
-
break;
|
|
366
|
-
case "text":
|
|
367
|
-
schema = z.string().check(z.maxLength(20_000));
|
|
368
|
-
break;
|
|
369
|
-
case "number":
|
|
370
|
-
case "integer": {
|
|
371
|
-
// Its range (FR-114) is the schema's: every form, tool and apply reads it from here.
|
|
372
|
-
const range = [
|
|
373
|
-
...(spec.type === "integer" ? [z.int()] : []),
|
|
374
|
-
...(spec.min === undefined ? [] : [z.gte(spec.min)]),
|
|
375
|
-
...(spec.max === undefined ? [] : [z.lte(spec.max)]),
|
|
376
|
-
...(spec.step === undefined ? [] : [z.multipleOf(spec.step)]),
|
|
377
|
-
];
|
|
378
|
-
schema = range.length > 0 ? z.number().check(...range) : z.number();
|
|
379
|
-
break;
|
|
380
|
-
}
|
|
381
|
-
case "boolean":
|
|
382
|
-
schema = z.boolean();
|
|
383
|
-
break;
|
|
384
|
-
case "date":
|
|
385
|
-
schema = z.string().check(z.regex(/^\d{4}-\d{2}-\d{2}$/, "a date like 2026-10-02"));
|
|
386
|
-
break;
|
|
387
|
-
case "datetime":
|
|
388
|
-
schema = z.string().check(z.refine((s) => !Number.isNaN(Date.parse(s)), "a date and time like 2026-10-02T14:30:00Z"));
|
|
389
|
-
break;
|
|
390
|
-
case "enum":
|
|
391
|
-
schema = z.enum(spec.options);
|
|
392
|
-
break;
|
|
393
|
-
case "list":
|
|
394
|
-
schema = z.array(spec.of === "number" ? z.number() : spec.of === "date" ? z.string().check(z.regex(/^\d{4}-\d{2}-\d{2}$/)) : z.string().check(z.maxLength(500))).check(z.maxLength(200));
|
|
395
|
-
break;
|
|
396
|
-
case "url":
|
|
397
|
-
schema = z.url();
|
|
398
|
-
break;
|
|
399
|
-
case "email":
|
|
400
|
-
schema = z.email();
|
|
401
|
-
break;
|
|
402
|
-
}
|
|
403
|
-
if (spec.description)
|
|
404
|
-
schema = schema.check(z.describe(spec.description));
|
|
405
|
-
return optional ? z.optional(schema) : schema;
|
|
406
|
-
}
|
|
407
|
-
/** What one of these is called, when the kind says nothing: the first required word-ish field. */
|
|
408
|
-
function defaultLabelField(spec) {
|
|
409
|
-
const entries = Object.entries(spec.fields);
|
|
410
|
-
return (entries.find(([, f]) => f.required && (f.type === "string" || f.type === "text"))?.[0] ??
|
|
411
|
-
entries.find(([, f]) => f.type === "string")?.[0]);
|
|
412
|
-
}
|
|
413
349
|
/** The arguments an act asks for: declared ones, then whatever its effects reference. */
|
|
414
|
-
function argsOf(
|
|
350
|
+
function argsOf(act, effects, document, edges) {
|
|
415
351
|
const args = new Map();
|
|
416
352
|
const subjectKinds = asArray(act.on);
|
|
417
353
|
const created = new Map();
|
|
@@ -420,7 +356,7 @@ function argsOf(name, act, effects, document, edges) {
|
|
|
420
356
|
const usesSubject = subjectKinds.length > 0 ||
|
|
421
357
|
effects.some((e) => ("remove" in e && refName(e.remove) === "subject") || ("from" in e && (refName(e.from) === "subject" || refName(e.to) === "subject")) || ("set" in e && !("create" in e) && refName(e.target ?? "$subject") === "subject"));
|
|
422
358
|
if (usesSubject)
|
|
423
|
-
args.set(SUBJECT_ARG, {
|
|
359
|
+
args.set(SUBJECT_ARG, { ref: subjectKinds.length > 0 ? subjectKinds : "*", required: true });
|
|
424
360
|
const declared = act.args ?? {};
|
|
425
361
|
const writesOnlyOne = act.writes?.length === 1;
|
|
426
362
|
/*
|
|
@@ -433,7 +369,7 @@ function argsOf(name, act, effects, document, edges) {
|
|
|
433
369
|
if (arg === SUBJECT_ARG || BUILTIN_REFS.has(arg) || created.has(arg) || args.has(arg))
|
|
434
370
|
return;
|
|
435
371
|
const d = declared[arg] ? withRangeOf(declared[arg], fed) : undefined;
|
|
436
|
-
args.set(arg, d ? {
|
|
372
|
+
args.set(arg, d ? { field: d, optional: !d.required, required: Boolean(d.required) } : schema());
|
|
437
373
|
};
|
|
438
374
|
for (const effect of effects) {
|
|
439
375
|
if ("create" in effect) {
|
|
@@ -443,7 +379,7 @@ function argsOf(name, act, effects, document, edges) {
|
|
|
443
379
|
const spec = kind?.fields[field];
|
|
444
380
|
if (arg && spec) {
|
|
445
381
|
const required = Boolean(spec.required) && spec.default === undefined;
|
|
446
|
-
note(arg, () => ({
|
|
382
|
+
note(arg, () => ({ field: spec, optional: !required, required }), spec);
|
|
447
383
|
}
|
|
448
384
|
}
|
|
449
385
|
if (effect.as)
|
|
@@ -458,36 +394,33 @@ function argsOf(name, act, effects, document, edges) {
|
|
|
458
394
|
const kinds = end === "to" ? edge?.to ?? "*" : edge?.from ?? "*";
|
|
459
395
|
if (kinds !== "*" && !named.has(arg))
|
|
460
396
|
named.set(arg, kinds);
|
|
461
|
-
note(arg, () => ({
|
|
397
|
+
note(arg, () => ({ ref: kinds, required: true }));
|
|
462
398
|
}
|
|
463
399
|
}
|
|
464
400
|
else if ("set" in effect) {
|
|
465
401
|
const target = refName(effect.target ?? "$subject");
|
|
466
402
|
const targetKind = target === "subject" ? subjectKinds[0] : (created.get(target ?? "") ?? named.get(target ?? "")?.[0]);
|
|
467
403
|
if (target && target !== "subject" && !created.has(target))
|
|
468
|
-
note(target, () => ({
|
|
404
|
+
note(target, () => ({ ref: "*", required: true }));
|
|
469
405
|
for (const [field, value] of Object.entries(effect.set)) {
|
|
470
406
|
const arg = refName(value);
|
|
471
407
|
if (!arg)
|
|
472
408
|
continue;
|
|
473
409
|
const spec = targetKind ? document.kinds[targetKind]?.fields[field] : undefined;
|
|
474
|
-
note(arg, () => (spec ? {
|
|
410
|
+
note(arg, () => (spec ? { field: spec, optional: !writesOnlyOne, required: writesOnlyOne } : { any: true, required: false }), spec);
|
|
475
411
|
}
|
|
476
412
|
}
|
|
477
413
|
else if ("remove" in effect) {
|
|
478
414
|
const arg = refName(effect.remove);
|
|
479
415
|
if (arg)
|
|
480
|
-
note(arg, () => ({
|
|
416
|
+
note(arg, () => ({ ref: "*", required: true }));
|
|
481
417
|
}
|
|
482
418
|
}
|
|
483
419
|
for (const [arg, d] of Object.entries(declared))
|
|
484
420
|
if (!args.has(arg))
|
|
485
|
-
args.set(arg, {
|
|
486
|
-
void name;
|
|
421
|
+
args.set(arg, { field: d, optional: !d.required, required: Boolean(d.required) });
|
|
487
422
|
return args;
|
|
488
423
|
}
|
|
489
|
-
/** An act's own refusal: the framework's typed one (FR-110), which a host shows rather than calling it a failure. */
|
|
490
|
-
const Refused = ActRefusal;
|
|
491
424
|
/**
|
|
492
425
|
* A DOCUMENT COMPILED WITHOUT THE FRAMEWORK'S CHECKER: its own sentences
|
|
493
426
|
* still judged, `checkApp` not asked. For a page drawing a document its host
|
|
@@ -504,65 +437,36 @@ export function compileDocumentWithoutCheck(raw, options = {}) {
|
|
|
504
437
|
const findings = [...read.findings];
|
|
505
438
|
if (options.previous)
|
|
506
439
|
findings.push(...renamesFromNothing(document, options.previous));
|
|
507
|
-
const
|
|
508
|
-
|
|
440
|
+
const plan = planOf(document, findings);
|
|
441
|
+
if (hasErrors(plan.findings))
|
|
442
|
+
return { ok: false, findings: plan.findings };
|
|
443
|
+
return build(plan, options);
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* The first half of compiling (FR-123): what a document that has been read
|
|
447
|
+
* and judged compiles to, as data — each template and expression parsed,
|
|
448
|
+
* each act's arguments and effects said, the brand derived. `findings` is
|
|
449
|
+
* added to (a brand that cannot be read is a warning) and carried.
|
|
450
|
+
*/
|
|
451
|
+
function planOf(document, findings) {
|
|
509
452
|
const edges = edgeIndex(document);
|
|
510
|
-
// What `{x | money}` says in a label, a sentence or a refusal: the app's currency and locale (FR-100).
|
|
511
|
-
const { accent, name: wordmark, ...money } = document.brand ?? {};
|
|
512
453
|
// ── kinds ────────────────────────────────────────────────────────────────
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
...[...computed].filter(([, c]) => c.label).map(([n, c]) => [n, c.label]),
|
|
525
|
-
]);
|
|
526
|
-
// Defaults stay out of the zod schema (hydration must not invent values) and ride beside it, for a repair to read.
|
|
527
|
-
const defaults = Object.fromEntries(Object.entries(spec.fields).filter(([, f]) => f.default !== undefined).map(([n, f]) => [n, f.default]));
|
|
528
|
-
const edgeDecls = Object.fromEntries(Object.entries(spec.edges ?? {}).map(([n, e]) => [
|
|
529
|
-
n,
|
|
530
|
-
{
|
|
531
|
-
to: e.to,
|
|
532
|
-
...(e.cardinality ? { cardinality: e.cardinality } : {}),
|
|
533
|
-
...(e.description ? { description: e.description } : {}),
|
|
534
|
-
...(e.inverse ? { inverse: e.inverse } : {}),
|
|
535
|
-
...(e.appendOnly ? { appendOnly: e.appendOnly } : {}),
|
|
536
|
-
},
|
|
537
|
-
]));
|
|
538
|
-
return defineNode(kind, {
|
|
539
|
-
fields: z.object(shape),
|
|
540
|
-
edges: edgeDecls,
|
|
541
|
-
...(spec.plural ? { plural: spec.plural } : {}),
|
|
542
|
-
...(spec.noun ? { noun: spec.noun } : {}),
|
|
543
|
-
...(spec.description ? { description: spec.description } : {}),
|
|
544
|
-
...(labelParts ? { label: (node) => renderTemplate(labelParts, { node: node, kinds: shapes, today: today(), money }) || node.id } : {}),
|
|
545
|
-
...(describeParts ? { describe: (node) => renderTemplate(describeParts, { node: node, kinds: shapes, today: today(), money }) } : {}),
|
|
546
|
-
...(spec.lifecycle ? { lifecycle: { field: spec.lifecycle.field, retired: spec.lifecycle.retired } } : {}),
|
|
547
|
-
...(spec.figure ? { figure: spec.figure } : {}),
|
|
548
|
-
...(spec.computed && Object.keys(spec.computed).length > 0 ? { computed: spec.computed } : {}),
|
|
549
|
-
...(Object.keys(labels).length > 0 || spec.glance ? { display: { ...(Object.keys(labels).length > 0 ? { labels } : {}), ...(spec.glance ? { glance: [...spec.glance] } : {}) } } : {}),
|
|
550
|
-
...(Object.keys(defaults).length > 0 ? { defaults } : {}),
|
|
551
|
-
});
|
|
552
|
-
});
|
|
553
|
-
const schema = createSchema(definitions);
|
|
454
|
+
const kinds = {};
|
|
455
|
+
for (const [kind, spec] of Object.entries(document.kinds)) {
|
|
456
|
+
const labelField = defaultLabelField(spec);
|
|
457
|
+
const labelSource = spec.label ?? (labelField ? `{${labelField}}` : undefined);
|
|
458
|
+
const computed = parsedComputed(spec);
|
|
459
|
+
kinds[kind] = {
|
|
460
|
+
...(labelSource ? { label: parseTemplate(labelSource) } : {}),
|
|
461
|
+
...(spec.describe ? { describe: parseTemplate(spec.describe) } : {}),
|
|
462
|
+
...(computed ? { computed: [...computed] } : {}),
|
|
463
|
+
};
|
|
464
|
+
}
|
|
554
465
|
// ── acts ─────────────────────────────────────────────────────────────────
|
|
555
|
-
const
|
|
466
|
+
const acts = {};
|
|
556
467
|
for (const [name, act] of Object.entries(document.acts ?? {})) {
|
|
557
468
|
const effects = effectsOf(act, document);
|
|
558
|
-
const args = argsOf(
|
|
559
|
-
const shape = {};
|
|
560
|
-
for (const [arg, a] of args)
|
|
561
|
-
shape[arg] = a.schema;
|
|
562
|
-
const subjectKinds = asArray(act.on);
|
|
563
|
-
const title = act.title ?? name.replace(/-/g, " ");
|
|
564
|
-
const guard = act.allowedWhen ? parseExpr(act.allowedWhen) : undefined;
|
|
565
|
-
const refusal = act.refusal ? parseTemplate(act.refusal) : undefined;
|
|
469
|
+
const args = argsOf(act, effects, document, edges);
|
|
566
470
|
const destructive = act.destructive ?? effects.some((e) => "remove" in e || "sever" in e || "replace" in e);
|
|
567
471
|
const creates = [...new Set(effects.flatMap((e) => ("create" in e ? [e.create] : [])))];
|
|
568
472
|
const connects = [...new Set(effects.flatMap((e) => ("connect" in e ? [e.connect] : [])))];
|
|
@@ -621,242 +525,37 @@ export function compileDocumentWithoutCheck(raw, options = {}) {
|
|
|
621
525
|
if (v && typeof v === "object" && !Array.isArray(v) && "expr" in v)
|
|
622
526
|
exprs.set(v.expr, parseExpr(v.expr));
|
|
623
527
|
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
...(act.
|
|
629
|
-
...(
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
528
|
+
acts[name] = {
|
|
529
|
+
title: act.title ?? name.replace(/-/g, " "),
|
|
530
|
+
effects,
|
|
531
|
+
args: [...args],
|
|
532
|
+
...(act.allowedWhen ? { guard: parseExpr(act.allowedWhen) } : {}),
|
|
533
|
+
...(act.refusal ? { refusal: parseTemplate(act.refusal) } : {}),
|
|
534
|
+
exprs: [...exprs],
|
|
535
|
+
destructive,
|
|
536
|
+
idempotent,
|
|
537
|
+
creates,
|
|
538
|
+
connects,
|
|
539
|
+
severs,
|
|
636
540
|
writes,
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
const subject = typeof input[SUBJECT_ARG] === "string" ? graph.getNode(input[SUBJECT_ARG]) : undefined;
|
|
641
|
-
const what = subject ? labelFor(subject) : Object.values(input).find((v) => typeof v === "string");
|
|
642
|
-
return what ? `${title}: ${String(what)}` : title;
|
|
643
|
-
},
|
|
644
|
-
apply(context, input) {
|
|
645
|
-
const graph = context.graph;
|
|
646
|
-
const subjectId = typeof input[SUBJECT_ARG] === "string" ? input[SUBJECT_ARG] : undefined;
|
|
647
|
-
const subject = subjectId ? graph.getNode(subjectId) : undefined;
|
|
648
|
-
if (subjectId && !subject)
|
|
649
|
-
throw new Refused(`there is no record "${subjectId}"`);
|
|
650
|
-
if (subject && subjectKinds.length > 0 && !subjectKinds.includes(subject.kind))
|
|
651
|
-
throw new Refused(`"${title}" acts on ${subjectKinds.join(" or ")}, not on ${withArticle(subject.kind)}`);
|
|
652
|
-
const bindings = {};
|
|
653
|
-
for (const [k, v] of Object.entries(input))
|
|
654
|
-
if (v !== undefined)
|
|
655
|
-
bindings[k] = v;
|
|
656
|
-
if (guard && subject) {
|
|
657
|
-
let allowed;
|
|
658
|
-
try {
|
|
659
|
-
allowed = evaluateExpr(guard, { graph, subject, kinds: shapes, bindings, today: today() });
|
|
660
|
-
}
|
|
661
|
-
catch (e) {
|
|
662
|
-
if (e instanceof ExprEvalError)
|
|
663
|
-
throw new Refused(`"${title}" could not be judged: ${e.sentence}`);
|
|
664
|
-
throw e;
|
|
665
|
-
}
|
|
666
|
-
if (allowed !== true) {
|
|
667
|
-
throw new Refused(refusal ? renderTemplate(refusal, { node: subject, kinds: shapes, graph, bindings, today: today(), money }) : `"${title}" is not allowed for ${labelFor(subject)} right now`);
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
const made = new Map();
|
|
671
|
-
const resolve = (value, current) => {
|
|
672
|
-
if (value && typeof value === "object" && !Array.isArray(value) && "expr" in value) {
|
|
673
|
-
try {
|
|
674
|
-
return evaluateExpr(exprs.get(value.expr), { graph, subject: current ?? subject ?? null, kinds: shapes, bindings, today: today() });
|
|
675
|
-
}
|
|
676
|
-
catch (e) {
|
|
677
|
-
if (e instanceof ExprEvalError)
|
|
678
|
-
throw new Refused(`"${title}" could not work out a value: ${e.sentence}`);
|
|
679
|
-
throw e;
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
const ref = refName(value);
|
|
683
|
-
if (ref === undefined)
|
|
684
|
-
return typeof value === "string" && value.startsWith("$$") ? value.slice(1) : value;
|
|
685
|
-
if (ref === "subject")
|
|
686
|
-
return subjectId;
|
|
687
|
-
if (ref === "now")
|
|
688
|
-
return new Date().toISOString();
|
|
689
|
-
if (ref === "today")
|
|
690
|
-
return today();
|
|
691
|
-
if (made.has(ref))
|
|
692
|
-
return made.get(ref);
|
|
693
|
-
return input[ref];
|
|
694
|
-
};
|
|
695
|
-
for (const effect of effects) {
|
|
696
|
-
if ("create" in effect) {
|
|
697
|
-
const kind = document.kinds[effect.create];
|
|
698
|
-
const node = { kind: effect.create };
|
|
699
|
-
for (const [field, f] of Object.entries(kind.fields))
|
|
700
|
-
if (f.default !== undefined)
|
|
701
|
-
node[field] = f.default;
|
|
702
|
-
for (const [field, value] of Object.entries(effect.set ?? {})) {
|
|
703
|
-
const v = resolve(value, undefined);
|
|
704
|
-
if (v !== undefined)
|
|
705
|
-
node[field] = v;
|
|
706
|
-
}
|
|
707
|
-
const labelField = defaultLabelField(kind);
|
|
708
|
-
const id = context.freshId(String((labelField && node[labelField]) ?? effect.create), effect.create);
|
|
709
|
-
context.addNode({ id, ...node });
|
|
710
|
-
if (effect.as)
|
|
711
|
-
made.set(effect.as, id);
|
|
712
|
-
}
|
|
713
|
-
else if ("connect" in effect || "sever" in effect) {
|
|
714
|
-
const kind = "connect" in effect ? effect.connect : effect.sever;
|
|
715
|
-
const from = String(resolve(effect.from, undefined) ?? "");
|
|
716
|
-
const to = String(resolve(effect.to, undefined) ?? "");
|
|
717
|
-
if (!graph.has(from) && ![...made.values()].includes(from))
|
|
718
|
-
throw new Refused(`there is no record "${from}"`);
|
|
719
|
-
if (!graph.has(to) && ![...made.values()].includes(to))
|
|
720
|
-
throw new Refused(`there is no record "${to}"`);
|
|
721
|
-
if ("connect" in effect) {
|
|
722
|
-
if (edges.get(kind)?.cardinality === "one")
|
|
723
|
-
for (const existing of graph.out(from, kind))
|
|
724
|
-
if (existing.id !== to)
|
|
725
|
-
context.removeEdge({ kind, from, to: existing.id });
|
|
726
|
-
if (!graph.out(from, kind).some((n) => n.id === to))
|
|
727
|
-
context.addEdge({ kind, from, to });
|
|
728
|
-
}
|
|
729
|
-
else {
|
|
730
|
-
context.removeEdge({ kind, from, to });
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
else if ("set" in effect) {
|
|
734
|
-
const targetId = String(resolve(effect.target ?? "$subject", undefined) ?? "");
|
|
735
|
-
const target = graph.getNode(targetId);
|
|
736
|
-
if (!target && ![...made.values()].includes(targetId))
|
|
737
|
-
throw new Refused(`there is no record "${targetId}"`);
|
|
738
|
-
const patch = {};
|
|
739
|
-
for (const [field, value] of Object.entries(effect.set)) {
|
|
740
|
-
const v = resolve(value, target);
|
|
741
|
-
if (v !== undefined)
|
|
742
|
-
patch[field] = v;
|
|
743
|
-
}
|
|
744
|
-
if (Object.keys(patch).length > 0)
|
|
745
|
-
context.patchNode(targetId, patch);
|
|
746
|
-
}
|
|
747
|
-
else if ("remove" in effect) {
|
|
748
|
-
const id = String(resolve(effect.remove, undefined) ?? "");
|
|
749
|
-
if (!graph.has(id))
|
|
750
|
-
throw new Refused(`there is no record "${id}"`);
|
|
751
|
-
context.removeNode(id);
|
|
752
|
-
}
|
|
753
|
-
else if ("replace" in effect && subject) {
|
|
754
|
-
// The subject's links of each relation, at its own end, severed — all but the one being made (FR-115).
|
|
755
|
-
const kept = input["to"];
|
|
756
|
-
for (const relation of effect.replace) {
|
|
757
|
-
const fromSubject = edges.get(relation)?.from.includes(subject.kind) ?? true;
|
|
758
|
-
for (const other of fromSubject ? graph.out(subject.id, relation) : graph.in(subject.id, relation)) {
|
|
759
|
-
if (relation === effect.keep && other.id === kept)
|
|
760
|
-
continue;
|
|
761
|
-
context.removeEdge(fromSubject ? { kind: relation, from: subject.id, to: other.id } : { kind: relation, from: other.id, to: subject.id });
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
},
|
|
767
|
-
}));
|
|
768
|
-
}
|
|
769
|
-
function labelFor(node) {
|
|
770
|
-
const definition = schema.tryDefinition(node.kind);
|
|
771
|
-
return definition?.label?.(node) ?? node.id;
|
|
541
|
+
writesOther,
|
|
542
|
+
sets,
|
|
543
|
+
};
|
|
772
544
|
}
|
|
773
545
|
// ── rules ────────────────────────────────────────────────────────────────
|
|
774
|
-
const
|
|
546
|
+
const rules = {};
|
|
775
547
|
for (const [name, rule] of Object.entries(document.rules ?? {})) {
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
const judge = (graph, subject) => {
|
|
782
|
-
const ctx = { graph, subject, kinds: shapes, today: today() };
|
|
783
|
-
let holds;
|
|
784
|
-
/*
|
|
785
|
-
* A RULE THAT CANNOT ANSWER IS THE ENGINE'S TO SAY (FR-29). Out of
|
|
786
|
-
* budget it throws RuleBudgetError, and the violation's status is
|
|
787
|
-
* `over-budget`; any other mistake (a word added to a number) is
|
|
788
|
-
* `could-not-judge`. Both name the rule and its subject, and neither
|
|
789
|
-
* is a hang or a crash that takes the other rules down.
|
|
790
|
-
*/
|
|
791
|
-
try {
|
|
792
|
-
if (when && evaluateExpr(when, ctx) !== true)
|
|
793
|
-
return [];
|
|
794
|
-
holds = evaluateExpr(require, ctx) === true;
|
|
795
|
-
}
|
|
796
|
-
catch (e) {
|
|
797
|
-
if (e instanceof ExprBudgetError)
|
|
798
|
-
throw new RuleBudgetError(e.sentence);
|
|
799
|
-
if (e instanceof ExprEvalError)
|
|
800
|
-
throw new Error(e.sentence);
|
|
801
|
-
throw e;
|
|
802
|
-
}
|
|
803
|
-
if (holds)
|
|
804
|
-
return [];
|
|
805
|
-
const repairList = repairs.flatMap((r) => {
|
|
806
|
-
const act = document.acts?.[r.act];
|
|
807
|
-
const args = {};
|
|
808
|
-
if (subject && (act?.on || /^(edit|remove)-/.test(r.act)))
|
|
809
|
-
args[SUBJECT_ARG] = subject.id;
|
|
810
|
-
for (const [k, v] of Object.entries(r.args ?? {}))
|
|
811
|
-
args[k] = refName(v) === "subject" ? subject?.id : v;
|
|
812
|
-
const declared = act ? argsOf(r.act, act, effectsOf(act, document), document, edges) : new Map();
|
|
813
|
-
const missing = [...declared].filter(([k, a]) => a.required && args[k] === undefined).map(([k]) => k);
|
|
814
|
-
return [{ mutation: r.act, label: r.label ?? act?.title ?? r.act.replace(/-/g, " "), args, ...(missing.length > 0 ? { missing } : {}) }];
|
|
815
|
-
});
|
|
816
|
-
return [
|
|
817
|
-
{
|
|
818
|
-
invariant: name,
|
|
819
|
-
...(subject ? { subjectId: subject.id } : {}),
|
|
820
|
-
label: title,
|
|
821
|
-
message: (says ? renderTemplate(says, { node: subject, kinds: shapes, graph, today: today(), money }) : subject ? `${labelFor(subject)}: ${title}` : title),
|
|
822
|
-
nodeIds: subject ? [subject.id] : [],
|
|
823
|
-
repairs: repairList,
|
|
824
|
-
},
|
|
825
|
-
];
|
|
548
|
+
rules[name] = {
|
|
549
|
+
title: rule.title ?? name.replace(/-/g, " "),
|
|
550
|
+
require: parseExpr(rule.require),
|
|
551
|
+
...(rule.when ? { when: parseExpr(rule.when) } : {}),
|
|
552
|
+
...(rule.says ? { says: parseTemplate(rule.says) } : {}),
|
|
826
553
|
};
|
|
827
|
-
invariants.push((rule.over === "graph"
|
|
828
|
-
? defineInvariant(name, {
|
|
829
|
-
scope: "graph",
|
|
830
|
-
label: title,
|
|
831
|
-
judgement: { require: rule.require, ...(rule.when ? { when: rule.when } : {}), ...(rule.says ? { says: rule.says } : {}) },
|
|
832
|
-
...(rule.description ? { description: rule.description } : {}),
|
|
833
|
-
...(repairs.length > 0 ? { repairs: repairs.map((r) => r.act) } : {}),
|
|
834
|
-
evaluate: ({ graph }) => judge(graph, null),
|
|
835
|
-
})
|
|
836
|
-
: defineInvariant(name, {
|
|
837
|
-
scope: { kind: rule.over },
|
|
838
|
-
label: title,
|
|
839
|
-
judgement: { require: rule.require, ...(rule.when ? { when: rule.when } : {}), ...(rule.says ? { says: rule.says } : {}) },
|
|
840
|
-
...(rule.description ? { description: rule.description } : {}),
|
|
841
|
-
...(repairs.length > 0 ? { repairs: repairs.map((r) => r.act) } : {}),
|
|
842
|
-
evaluate: ({ graph, subject }) => judge(graph, subject),
|
|
843
|
-
})));
|
|
844
554
|
}
|
|
845
|
-
// ──
|
|
846
|
-
/*
|
|
847
|
-
* WHO SEES WHAT is the app's, in the framework's one meaning (FR-02):
|
|
848
|
-
* absent, everybody sees everything; present, a kind no sight names is
|
|
849
|
-
* seen by nobody but the system, and `own` is the principal's own records.
|
|
850
|
-
*/
|
|
851
|
-
const policy = document.policy
|
|
852
|
-
? {
|
|
853
|
-
grants: document.policy.grants,
|
|
854
|
-
...(document.policy.sees ? { sees: document.policy.sees } : {}),
|
|
855
|
-
...(document.roles ? { roles: document.roles } : {}),
|
|
856
|
-
}
|
|
857
|
-
: undefined;
|
|
555
|
+
// ── brand, views ─────────────────────────────────────────────────────────
|
|
858
556
|
let brand;
|
|
859
557
|
if (document.brand) {
|
|
558
|
+
const { accent, name: wordmark, ...money } = document.brand;
|
|
860
559
|
const derived = accent ? brandFromAccent({ accent, base: SCHEMES }) : undefined;
|
|
861
560
|
// A colour that cannot be read is not a reason to refuse an app: it wears the default colours and says why.
|
|
862
561
|
if (derived && !derived.ok)
|
|
@@ -865,29 +564,20 @@ export function compileDocumentWithoutCheck(raw, options = {}) {
|
|
|
865
564
|
if (!derived || derived.ok || money.currency || money.locale)
|
|
866
565
|
brand = { name: wordmark ?? document.name, schemes: derived?.ok ? derived.schemes : SCHEMES, ...money };
|
|
867
566
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
...(
|
|
567
|
+
const viewSpecs = viewsOf(document);
|
|
568
|
+
const home = homeOf(document);
|
|
569
|
+
return {
|
|
570
|
+
format: COMPILED_FORMAT,
|
|
571
|
+
document,
|
|
572
|
+
findings,
|
|
573
|
+
kinds,
|
|
574
|
+
edges: [...edges].map(([name, { from, to, cardinality }]) => [name, { from, to, cardinality }]),
|
|
575
|
+
acts,
|
|
576
|
+
rules,
|
|
876
577
|
...(brand ? { brand } : {}),
|
|
877
|
-
...(
|
|
878
|
-
...(
|
|
879
|
-
// The arrangement is the app's (FR-80): every face reads it beside the places.
|
|
880
|
-
...(document.pages ? { pages: document.pages } : {}),
|
|
881
|
-
...(document.settings ? { settings: document.settings } : {}),
|
|
882
|
-
// The document's views are the declaration's view specs (FR-03): data the framework draws.
|
|
883
|
-
...(Object.keys(viewsOf(document)).length > 0 ? { viewSpecs: viewsOf(document) } : {}),
|
|
884
|
-
// And its home view, about no one record, is the app's home (FR-81).
|
|
885
|
-
...(homeOf(document) ? { home: homeOf(document) } : {}),
|
|
886
|
-
version: document.version ?? 1,
|
|
578
|
+
...(Object.keys(viewSpecs).length > 0 ? { viewSpecs } : {}),
|
|
579
|
+
...(home ? { home } : {}),
|
|
887
580
|
};
|
|
888
|
-
// `toDocument(app)` gives this document back, byte for byte.
|
|
889
|
-
rememberDocument(app, document);
|
|
890
|
-
return { ok: true, app, document, findings, sights: document.policy?.sees, kinds: shapes };
|
|
891
581
|
}
|
|
892
582
|
/** Map a framework check's "where" (a kind, an act, a rule by name) back to a document path when we can. */
|
|
893
583
|
export function frameworkPath(where, document) {
|