@graview/core 0.1.60 → 0.1.62
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 +23 -1
- package/dist/app-places.d.ts.map +1 -1
- package/dist/app-places.js +2 -2
- package/dist/app-places.js.map +1 -1
- package/dist/app.d.ts +10 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js.map +1 -1
- package/dist/arrangement.d.ts +2 -0
- package/dist/arrangement.d.ts.map +1 -1
- package/dist/arrangement.js +14 -0
- package/dist/arrangement.js.map +1 -1
- package/dist/capabilities.js +1 -1
- package/dist/capabilities.js.map +1 -1
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +2 -1
- package/dist/catalog.js.map +1 -1
- package/dist/cli/check/context.d.ts +5 -0
- package/dist/cli/check/context.d.ts.map +1 -1
- package/dist/cli/check/context.js.map +1 -1
- package/dist/cli/check/face.d.ts +14 -0
- package/dist/cli/check/face.d.ts.map +1 -0
- package/dist/cli/check/face.js +161 -0
- package/dist/cli/check/face.js.map +1 -0
- package/dist/cli/check/hidden-reads.d.ts.map +1 -1
- package/dist/cli/check/hidden-reads.js +26 -0
- package/dist/cli/check/hidden-reads.js.map +1 -1
- package/dist/cli/check/index.d.ts +1 -0
- package/dist/cli/check/index.d.ts.map +1 -1
- package/dist/cli/check/index.js +1 -0
- package/dist/cli/check/index.js.map +1 -1
- package/dist/cli/check/tidying.d.ts +14 -0
- package/dist/cli/check/tidying.d.ts.map +1 -0
- package/dist/cli/check/tidying.js +24 -0
- package/dist/cli/check/tidying.js.map +1 -0
- package/dist/cli/check.d.ts.map +1 -1
- package/dist/cli/check.js +4 -4
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/describe.d.ts.map +1 -1
- package/dist/cli/describe.js +14 -1
- package/dist/cli/describe.js.map +1 -1
- package/dist/cli/docs.d.ts.map +1 -1
- package/dist/cli/docs.js +5 -3
- package/dist/cli/docs.js.map +1 -1
- package/dist/cli/document-schema.d.ts.map +1 -1
- package/dist/cli/document-schema.js +8 -2
- package/dist/cli/document-schema.js.map +1 -1
- package/dist/cli/index.d.ts +2 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +15 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli/types.js +2 -1
- package/dist/cli/types.js.map +1 -1
- package/dist/compose.d.ts +14 -0
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +16 -1
- package/dist/compose.js.map +1 -1
- package/dist/css/sanitize.d.ts +125 -0
- package/dist/css/sanitize.d.ts.map +1 -0
- package/dist/css/sanitize.js +989 -0
- package/dist/css/sanitize.js.map +1 -0
- package/dist/css/tables.d.ts +55 -0
- package/dist/css/tables.d.ts.map +1 -0
- package/dist/css/tables.js +137 -0
- package/dist/css/tables.js.map +1 -0
- package/dist/css.d.ts +20 -0
- package/dist/css.d.ts.map +1 -0
- package/dist/css.js +19 -0
- package/dist/css.js.map +1 -0
- package/dist/days.d.ts +15 -0
- package/dist/days.d.ts.map +1 -1
- package/dist/days.js +21 -0
- package/dist/days.js.map +1 -1
- package/dist/describe.d.ts +1 -1
- package/dist/describe.d.ts.map +1 -1
- package/dist/document/act-findings.d.ts.map +1 -1
- package/dist/document/act-findings.js +80 -1
- package/dist/document/act-findings.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/brand-check.d.ts +15 -1
- package/dist/document/brand-check.d.ts.map +1 -1
- package/dist/document/brand-check.js +80 -0
- package/dist/document/brand-check.js.map +1 -1
- package/dist/document/brand.d.ts.map +1 -1
- package/dist/document/brand.js +2 -0
- package/dist/document/brand.js.map +1 -1
- package/dist/document/compile-checked.d.ts.map +1 -1
- package/dist/document/compile-checked.js +4 -3
- package/dist/document/compile-checked.js.map +1 -1
- package/dist/document/compile.d.ts.map +1 -1
- package/dist/document/compile.js +54 -9
- package/dist/document/compile.js.map +1 -1
- package/dist/document/compiled.d.ts +18 -3
- package/dist/document/compiled.d.ts.map +1 -1
- package/dist/document/compiled.js +91 -10
- package/dist/document/compiled.js.map +1 -1
- package/dist/document/connection-findings.d.ts.map +1 -1
- package/dist/document/connection-findings.js +219 -19
- package/dist/document/connection-findings.js.map +1 -1
- package/dist/document/connection-spec.d.ts +7 -0
- package/dist/document/connection-spec.d.ts.map +1 -1
- package/dist/document/connection-spec.js +26 -2
- package/dist/document/connection-spec.js.map +1 -1
- package/dist/document/covers.d.ts +48 -0
- package/dist/document/covers.d.ts.map +1 -0
- package/dist/document/covers.js +90 -0
- package/dist/document/covers.js.map +1 -0
- package/dist/document/describe-place.d.ts +3 -0
- package/dist/document/describe-place.d.ts.map +1 -1
- package/dist/document/describe-place.js +11 -0
- package/dist/document/describe-place.js.map +1 -1
- package/dist/document/described.d.ts +28 -0
- package/dist/document/described.d.ts.map +1 -1
- package/dist/document/diff.js +10 -1
- 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 +186 -18
- package/dist/document/edit.js.map +1 -1
- package/dist/document/expr/evaluate.d.ts +10 -1
- package/dist/document/expr/evaluate.d.ts.map +1 -1
- package/dist/document/expr/evaluate.js +110 -2
- package/dist/document/expr/evaluate.js.map +1 -1
- package/dist/document/expr/index.d.ts +1 -0
- package/dist/document/expr/index.d.ts.map +1 -1
- package/dist/document/expr/index.js +2 -0
- package/dist/document/expr/index.js.map +1 -1
- package/dist/document/expr/typing.d.ts +24 -0
- package/dist/document/expr/typing.d.ts.map +1 -0
- package/dist/document/expr/typing.js +219 -0
- package/dist/document/expr/typing.js.map +1 -0
- package/dist/document/expr/when.d.ts +52 -0
- package/dist/document/expr/when.d.ts.map +1 -0
- package/dist/document/expr/when.js +215 -0
- package/dist/document/expr/when.js.map +1 -0
- package/dist/document/expr-type-findings.d.ts +5 -0
- package/dist/document/expr-type-findings.d.ts.map +1 -0
- package/dist/document/expr-type-findings.js +90 -0
- package/dist/document/expr-type-findings.js.map +1 -0
- package/dist/document/field-spec.d.ts.map +1 -1
- package/dist/document/field-spec.js +5 -0
- package/dist/document/field-spec.js.map +1 -1
- package/dist/document/findings.d.ts +33 -2
- package/dist/document/findings.d.ts.map +1 -1
- package/dist/document/findings.js +14 -17
- package/dist/document/findings.js.map +1 -1
- package/dist/document/index.d.ts +6 -3
- package/dist/document/index.d.ts.map +1 -1
- package/dist/document/index.js +5 -2
- package/dist/document/index.js.map +1 -1
- package/dist/document/linked-findings.js +1 -1
- package/dist/document/linked-findings.js.map +1 -1
- package/dist/document/migrate.d.ts.map +1 -1
- package/dist/document/migrate.js +10 -4
- package/dist/document/migrate.js.map +1 -1
- package/dist/document/rule-line.d.ts +7 -3
- package/dist/document/rule-line.d.ts.map +1 -1
- package/dist/document/rule-line.js +17 -2
- package/dist/document/rule-line.js.map +1 -1
- package/dist/document/rule-reads.d.ts.map +1 -1
- package/dist/document/rule-reads.js +3 -1
- package/dist/document/rule-reads.js.map +1 -1
- package/dist/document/rules.d.ts +30 -0
- package/dist/document/rules.d.ts.map +1 -1
- package/dist/document/rules.js +74 -0
- package/dist/document/rules.js.map +1 -1
- package/dist/document/schema.d.ts +80 -2
- package/dist/document/schema.d.ts.map +1 -1
- package/dist/document/schema.js +90 -4
- package/dist/document/schema.js.map +1 -1
- package/dist/document/to-document.d.ts.map +1 -1
- package/dist/document/to-document.js +3 -1
- package/dist/document/to-document.js.map +1 -1
- package/dist/document.schema.json +304 -5
- package/dist/formats.d.ts +21 -13
- package/dist/formats.d.ts.map +1 -1
- package/dist/formats.js +21 -13
- package/dist/formats.js.map +1 -1
- package/dist/import/coerce.d.ts.map +1 -1
- package/dist/import/coerce.js +16 -4
- package/dist/import/coerce.js.map +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/invariants/types.d.ts +7 -2
- package/dist/invariants/types.d.ts.map +1 -1
- package/dist/kind-list.d.ts +1 -0
- package/dist/kind-list.d.ts.map +1 -1
- package/dist/kind-list.js +2 -1
- package/dist/kind-list.js.map +1 -1
- package/dist/lines.d.ts.map +1 -1
- package/dist/lines.js +6 -2
- package/dist/lines.js.map +1 -1
- package/dist/mutations/form.d.ts +2 -0
- package/dist/mutations/form.d.ts.map +1 -1
- package/dist/mutations/form.js +6 -3
- package/dist/mutations/form.js.map +1 -1
- package/dist/mutations/types.d.ts +9 -0
- package/dist/mutations/types.d.ts.map +1 -1
- package/dist/mutations/vote-suggestion.d.ts +67 -0
- package/dist/mutations/vote-suggestion.d.ts.map +1 -0
- package/dist/mutations/vote-suggestion.js +36 -0
- package/dist/mutations/vote-suggestion.js.map +1 -0
- package/dist/navigate.d.ts +7 -3
- package/dist/navigate.d.ts.map +1 -1
- package/dist/navigate.js +7 -3
- package/dist/navigate.js.map +1 -1
- package/dist/ops/lineage.d.ts +11 -1
- package/dist/ops/lineage.d.ts.map +1 -1
- package/dist/ops/lineage.js +6 -2
- package/dist/ops/lineage.js.map +1 -1
- package/dist/ops/suggestions.d.ts +33 -1
- package/dist/ops/suggestions.d.ts.map +1 -1
- package/dist/ops/suggestions.js +44 -2
- package/dist/ops/suggestions.js.map +1 -1
- package/dist/ops/types.d.ts +13 -0
- package/dist/ops/types.d.ts.map +1 -1
- package/dist/ops/types.js.map +1 -1
- package/dist/place-names.d.ts +42 -0
- package/dist/place-names.d.ts.map +1 -0
- package/dist/place-names.js +43 -0
- package/dist/place-names.js.map +1 -0
- package/dist/places.d.ts +2 -0
- package/dist/places.d.ts.map +1 -1
- package/dist/places.js +2 -0
- package/dist/places.js.map +1 -1
- package/dist/scaffold/agents.d.ts +9 -1
- package/dist/scaffold/agents.d.ts.map +1 -1
- package/dist/scaffold/agents.js +33 -2
- package/dist/scaffold/agents.js.map +1 -1
- package/dist/scaffold/project.js +1 -1
- package/dist/schema/choice-words.d.ts.map +1 -1
- package/dist/schema/choice-words.js +3 -0
- package/dist/schema/choice-words.js.map +1 -1
- package/dist/starting-document.d.ts +73 -0
- package/dist/starting-document.d.ts.map +1 -0
- package/dist/starting-document.js +645 -0
- package/dist/starting-document.js.map +1 -0
- package/dist/store.d.ts +50 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +167 -27
- package/dist/store.js.map +1 -1
- package/dist/sync/connection.d.ts +20 -1
- package/dist/sync/connection.d.ts.map +1 -1
- package/dist/sync/connection.js +198 -34
- package/dist/sync/connection.js.map +1 -1
- package/dist/sync/engine.d.ts +13 -4
- package/dist/sync/engine.d.ts.map +1 -1
- package/dist/sync/engine.js +10 -6
- package/dist/sync/engine.js.map +1 -1
- package/dist/sync/google-calendar.d.ts +8 -0
- package/dist/sync/google-calendar.d.ts.map +1 -1
- package/dist/sync/google-calendar.js +16 -4
- package/dist/sync/google-calendar.js.map +1 -1
- package/dist/sync/guests.d.ts +44 -0
- package/dist/sync/guests.d.ts.map +1 -0
- package/dist/sync/guests.js +57 -0
- package/dist/sync/guests.js.map +1 -0
- package/dist/sync/notion-types.d.ts +7 -0
- package/dist/sync/notion-types.d.ts.map +1 -1
- package/dist/sync/notion-types.js +6 -1
- package/dist/sync/notion-types.js.map +1 -1
- package/dist/sync/recurrence.d.ts +41 -0
- package/dist/sync/recurrence.d.ts.map +1 -0
- package/dist/sync/recurrence.js +192 -0
- package/dist/sync/recurrence.js.map +1 -0
- package/dist/sync/types.d.ts +9 -0
- package/dist/sync/types.d.ts.map +1 -1
- package/dist/sync/types.js.map +1 -1
- package/dist/testing.js +1 -1
- package/dist/testing.js.map +1 -1
- package/dist/theme/contrast.d.ts +2 -0
- package/dist/theme/contrast.d.ts.map +1 -1
- package/dist/theme/contrast.js +4 -0
- package/dist/theme/contrast.js.map +1 -1
- package/dist/theme/ground.d.ts +1 -2
- package/dist/theme/ground.d.ts.map +1 -1
- package/dist/theme/ground.js +2 -5
- package/dist/theme/ground.js.map +1 -1
- package/dist/theme/marks.d.ts +14 -2
- package/dist/theme/marks.d.ts.map +1 -1
- package/dist/theme/marks.js +20 -3
- package/dist/theme/marks.js.map +1 -1
- package/dist/theme/types.d.ts +22 -0
- package/dist/theme/types.d.ts.map +1 -1
- package/dist/theme/types.js.map +1 -1
- package/dist/tidying.d.ts +30 -4
- package/dist/tidying.d.ts.map +1 -1
- package/dist/tidying.js +110 -5
- package/dist/tidying.js.map +1 -1
- package/dist/trait-vocabulary.d.ts +17 -0
- package/dist/trait-vocabulary.d.ts.map +1 -0
- package/dist/trait-vocabulary.js +16 -0
- package/dist/trait-vocabulary.js.map +1 -0
- package/dist/traits.d.ts +79 -0
- package/dist/traits.d.ts.map +1 -0
- package/dist/traits.js +468 -0
- package/dist/traits.js.map +1 -0
- package/dist/typed.d.ts +4 -0
- package/dist/typed.d.ts.map +1 -1
- package/dist/typed.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +11 -1
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A STARTING DOCUMENT COMPOSED FROM PARTS (Roadmap f-compose-from-parts;
|
|
3
|
+
* the starter's second framework half).
|
|
4
|
+
*
|
|
5
|
+
* A person who says "kinda" to a template keeps some of its parts, drops
|
|
6
|
+
* others, and asks for parts it lacks; one nothing fits composes from what
|
|
7
|
+
* they described alone. `startingDocument` is that composition as a
|
|
8
|
+
* function: it starts from a template's document or from nothing, takes
|
|
9
|
+
* each dropped kind out with the rules, lenses, acts and relations only it
|
|
10
|
+
* served (`editDocument`'s `remove-kind`), or a dropped trait's carriers,
|
|
11
|
+
* and brings each added trait's MODULE in — a `person` kind, an owner link
|
|
12
|
+
* and a "has an owner" rule for `owners`; a status field, named step acts
|
|
13
|
+
* and a board for `status`; an amount, a budget and the over-budget rule
|
|
14
|
+
* for `budget` — each joining the kind people see most where it can
|
|
15
|
+
* rather than standing apart. A trait the document already carries is left
|
|
16
|
+
* as it is. Every change is one `editDocument` accepts, so the result
|
|
17
|
+
* compiles; what it took from where is said as `took`, for a host to show
|
|
18
|
+
* as chips. Then `composeDesign` gives it its jobs and look, as today.
|
|
19
|
+
*
|
|
20
|
+
* Exported on `@graview/core/compose`, on no page.
|
|
21
|
+
*/
|
|
22
|
+
import { designFindings } from "./document/design-findings.js";
|
|
23
|
+
import { editDocument } from "./document/edit.js";
|
|
24
|
+
import { FORMAT, FORMAT_VERSION } from "./document/schema.js";
|
|
25
|
+
import { humanizeField } from "./schema/define-node.js";
|
|
26
|
+
import { an, DUE_FIELD, hasTrait, isPeopleKind, listedWords, mainKindOf, movingChoices, TRAITS, traitNounOf, traitPluralOf, traitsOfDocument, traitWords } from "./traits.js";
|
|
27
|
+
const DONE = new Set(["done", "complete", "completed", "closed", "shipped", "booked", "paid", "finished", "resolved", "published", "delivered", "approved", "merged", "released", "signed", "won", "returned", "fixed", "filed", "chosen"]);
|
|
28
|
+
const ENDED = new Set(["canceled", "declined", "rejected", "withdrawn", "abandoned", "lost", "dropped", "archived", "wontfix", "won't do", "duplicate", "passed", "dismissed", "scrapped"]);
|
|
29
|
+
const CHOSEN = /^(chosen|picked|selected|winner|won|booked|hired|accepted)$/i;
|
|
30
|
+
const PUBLISHED = /^(published|live|public)$/i;
|
|
31
|
+
const STATUS_FIELD = /status|stage|state|phase|progress/i;
|
|
32
|
+
const GROUPING = /categor|room|area|group|project|section|event|trip|exchange|phase|department|team|bucket|budget|folder|list|board|topic|channel|release|season|sprint|milestone/i;
|
|
33
|
+
const PLACE_NOUNS = new Set(["place", "venue", "location", "room", "site", "building", "store", "warehouse", "city", "country", "region", "address", "hall", "stadium", "office", "shelf"]);
|
|
34
|
+
const SOURCE_NOUNS = new Set(["source", "reference", "citation", "evidence"]);
|
|
35
|
+
const GROUPING_NOUNS = new Set(["season", "era", "period", "epoch", "edition", "generation", "age", "campaign", "tournament"]);
|
|
36
|
+
const quote = (value) => `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
37
|
+
const capital = (words) => words.charAt(0).toUpperCase() + words.slice(1);
|
|
38
|
+
const kebab = (words) => words.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
39
|
+
const isMoney = (name, field) => (field.type === "number" || field.type === "integer") && (field.format === "money" || /cost|price|quote|amount|salary|fee|spend|total|paid|owed/i.test(name)) && !/budget|limit|cap$|allowance/i.test(name);
|
|
40
|
+
const isDate = (field) => field.type === "date" || field.type === "datetime";
|
|
41
|
+
/** A document with one kind and nothing else: where a person who matched no template begins. */
|
|
42
|
+
function emptyDocument(name, roster) {
|
|
43
|
+
const kind = roster ? "person" : "item";
|
|
44
|
+
const noun = roster ? "person" : "item";
|
|
45
|
+
const plural = roster ? "people" : "items";
|
|
46
|
+
return {
|
|
47
|
+
format: FORMAT,
|
|
48
|
+
formatVersion: FORMAT_VERSION,
|
|
49
|
+
name,
|
|
50
|
+
kinds: { [kind]: { noun, plural, fields: { name: { type: "string", required: true } }, label: "{name}" } },
|
|
51
|
+
acts: { [`add-${kind}`]: { title: `Add ${an(noun)} ${noun}`, description: `Add a new ${noun}.`, creates: kind } },
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/*
|
|
55
|
+
* THE COMPOSER: one document, changed one edit at a time, each change said
|
|
56
|
+
* and each part it brought recorded with where it came from. An edit the
|
|
57
|
+
* editor refuses is said and passed over, so a module that cannot join a
|
|
58
|
+
* particular base still leaves a document that compiles.
|
|
59
|
+
*/
|
|
60
|
+
class Composer {
|
|
61
|
+
doc;
|
|
62
|
+
took = [];
|
|
63
|
+
left = [];
|
|
64
|
+
said = [];
|
|
65
|
+
edits = [];
|
|
66
|
+
constructor(doc) {
|
|
67
|
+
this.doc = doc;
|
|
68
|
+
}
|
|
69
|
+
/** Apply one edit; true when it took. */
|
|
70
|
+
apply(edit, from, parts = []) {
|
|
71
|
+
const outcome = editDocument(this.doc, [edit]);
|
|
72
|
+
if (!outcome.ok) {
|
|
73
|
+
this.said.push(`Not done (${edit.op}${typeof edit["kind"] === "string" ? ` on ${edit["kind"]}` : ""}): ${outcome.findings.map((finding) => finding.message).join("; ")}.`);
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
this.doc = outcome.document;
|
|
77
|
+
this.said.push(...outcome.said);
|
|
78
|
+
this.edits.push(edit);
|
|
79
|
+
if (from !== undefined)
|
|
80
|
+
for (const part of parts)
|
|
81
|
+
this.took.push({ ...part, from });
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
// ── what a module reads off the document as it stands ──
|
|
85
|
+
get main() {
|
|
86
|
+
return mainKindOf(this.doc);
|
|
87
|
+
}
|
|
88
|
+
spec(kind) {
|
|
89
|
+
return this.doc.kinds[kind];
|
|
90
|
+
}
|
|
91
|
+
noun(kind) {
|
|
92
|
+
return traitNounOf(this.doc, kind);
|
|
93
|
+
}
|
|
94
|
+
plural(kind) {
|
|
95
|
+
return traitPluralOf(this.doc, kind);
|
|
96
|
+
}
|
|
97
|
+
Plural(kind) {
|
|
98
|
+
return capital(this.plural(kind));
|
|
99
|
+
}
|
|
100
|
+
/** How a rule's sentence says one record: the kind's label, else its first word field. */
|
|
101
|
+
label(kind) {
|
|
102
|
+
const spec = this.spec(kind);
|
|
103
|
+
if (spec.label)
|
|
104
|
+
return spec.label;
|
|
105
|
+
const word = Object.entries(spec.fields).find(([, field]) => field.type === "string")?.[0];
|
|
106
|
+
return word ? `{${word}}` : `${an(this.noun(kind))} ${this.noun(kind)}`;
|
|
107
|
+
}
|
|
108
|
+
freeField(kind, ...wanted) {
|
|
109
|
+
const spec = this.spec(kind);
|
|
110
|
+
const taken = (name) => spec.fields[name] !== undefined || spec.edges?.[name] !== undefined;
|
|
111
|
+
for (const name of wanted)
|
|
112
|
+
if (!taken(name))
|
|
113
|
+
return name;
|
|
114
|
+
let n = 2;
|
|
115
|
+
while (taken(`${wanted[0]}${n}`))
|
|
116
|
+
n += 1;
|
|
117
|
+
return `${wanted[0]}${n}`;
|
|
118
|
+
}
|
|
119
|
+
freeAct(...wanted) {
|
|
120
|
+
for (const name of wanted)
|
|
121
|
+
if (!this.doc.acts?.[name])
|
|
122
|
+
return name;
|
|
123
|
+
let n = 2;
|
|
124
|
+
while (this.doc.acts?.[`${wanted[0]}-${n}`])
|
|
125
|
+
n += 1;
|
|
126
|
+
return `${wanted[0]}-${n}`;
|
|
127
|
+
}
|
|
128
|
+
freeRule(name) {
|
|
129
|
+
if (!this.doc.rules?.[name])
|
|
130
|
+
return name;
|
|
131
|
+
let n = 2;
|
|
132
|
+
while (this.doc.rules?.[`${name}-${n}`])
|
|
133
|
+
n += 1;
|
|
134
|
+
return `${name}-${n}`;
|
|
135
|
+
}
|
|
136
|
+
freeKind(name) {
|
|
137
|
+
if (!this.doc.kinds[name])
|
|
138
|
+
return name;
|
|
139
|
+
let n = 2;
|
|
140
|
+
while (this.doc.kinds[`${name}-${n}`])
|
|
141
|
+
n += 1;
|
|
142
|
+
return `${name}-${n}`;
|
|
143
|
+
}
|
|
144
|
+
freeTitle(title) {
|
|
145
|
+
const titles = new Set((this.doc.lenses ?? []).map((lens) => String(lens.title ?? "").toLowerCase()));
|
|
146
|
+
if (!titles.has(title.toLowerCase()))
|
|
147
|
+
return title;
|
|
148
|
+
let n = 2;
|
|
149
|
+
while (titles.has(`${title} ${n}`.toLowerCase()))
|
|
150
|
+
n += 1;
|
|
151
|
+
return `${title} ${n}`;
|
|
152
|
+
}
|
|
153
|
+
/** The kind that is people, if the document has one. */
|
|
154
|
+
peopleKind() {
|
|
155
|
+
return Object.keys(this.doc.kinds).find((kind) => isPeopleKind(this.doc, kind));
|
|
156
|
+
}
|
|
157
|
+
kindNamed(nouns) {
|
|
158
|
+
return Object.keys(this.doc.kinds).find((kind) => nouns.has(kind.toLowerCase()) || nouns.has(this.noun(kind)));
|
|
159
|
+
}
|
|
160
|
+
/** The kind's status: a choice that moves, or one named so. */
|
|
161
|
+
status(kind) {
|
|
162
|
+
const moving = movingChoices(this.doc, kind)[0];
|
|
163
|
+
if (moving)
|
|
164
|
+
return { field: moving.field, options: moving.options };
|
|
165
|
+
const named = Object.entries(this.spec(kind).fields).find(([name, field]) => field.type === "enum" && Array.isArray(field.options) && field.options.length >= 2 && STATUS_FIELD.test(name));
|
|
166
|
+
return named ? { field: named[0], options: named[1].options } : undefined;
|
|
167
|
+
}
|
|
168
|
+
/** The condition under which a record is still on its way: none when the kind has no status that ends. */
|
|
169
|
+
stillOpen(kind) {
|
|
170
|
+
const status = this.status(kind);
|
|
171
|
+
if (!status)
|
|
172
|
+
return undefined;
|
|
173
|
+
// An option a template spells with a doubled l is read as its American spelling.
|
|
174
|
+
const ends = status.options.filter((option) => DONE.has(option.toLowerCase()) || ENDED.has(option.toLowerCase().replace(/ll/g, "l")));
|
|
175
|
+
if (ends.length === 0)
|
|
176
|
+
return undefined;
|
|
177
|
+
return ends.length === 1 ? `${status.field} != ${quote(ends[0])}` : `not (${status.field} in [${ends.map(quote).join(", ")}])`;
|
|
178
|
+
}
|
|
179
|
+
/** The kind the main kind is grouped under: a relation of one to a kind named like a grouping, never people. */
|
|
180
|
+
groupOf(kind) {
|
|
181
|
+
for (const [relation, edge] of Object.entries(this.spec(kind).edges ?? {})) {
|
|
182
|
+
const to = Array.isArray(edge.to) && edge.to.length === 1 ? edge.to[0] : undefined;
|
|
183
|
+
if (edge.cardinality !== "one" || !to || !this.doc.kinds[to] || isPeopleKind(this.doc, to))
|
|
184
|
+
continue;
|
|
185
|
+
if (GROUPING.test(to) || GROUPING.test(this.noun(to)))
|
|
186
|
+
return { relation, kind: to };
|
|
187
|
+
}
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** `add-kind`, with its "add" act, said as parts; where sights say who sees what, everyone sees the new kind, since a kind no sight names is seen by nobody. */
|
|
192
|
+
function addKind(c, from, kind, spec) {
|
|
193
|
+
const before = new Set(Object.keys(c.doc.acts ?? {}));
|
|
194
|
+
if (!c.apply({ op: "add-kind", kind, ...spec }, from, [{ part: kind, what: "kind" }]))
|
|
195
|
+
return false;
|
|
196
|
+
for (const act of Object.keys(c.doc.acts ?? {}))
|
|
197
|
+
if (!before.has(act))
|
|
198
|
+
c.took.push({ part: act, what: "act", from });
|
|
199
|
+
if (c.doc.policy?.sees?.length)
|
|
200
|
+
c.apply({ op: "add-sight", roles: "*", kinds: [kind] }, from, [{ part: `${kind}: everyone`, what: "sight" }]);
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
/** The lenses that draw by a kind's field or relation — a board's column, a calendar's start, a coverage's link — which cannot stand without it. */
|
|
204
|
+
function lensesDrawnBy(c, kind, name) {
|
|
205
|
+
return (c.doc.lenses ?? []).flatMap((lens) => {
|
|
206
|
+
const { title, bindings } = lens;
|
|
207
|
+
if (typeof title !== "string" || !bindings)
|
|
208
|
+
return [];
|
|
209
|
+
const byField = Object.values(bindings[kind] ?? {}).includes(name);
|
|
210
|
+
const link = bindings["link"] ?? {};
|
|
211
|
+
const byLink = link["edge"] === name || (Array.isArray(link["path"]) && link["path"].includes(name));
|
|
212
|
+
return byField || byLink ? [title] : [];
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
const addField = (c, from, kind, field, spec) => c.apply({ op: "add-field", kind, field, ...spec }, from, [{ part: `${kind}.${field}`, what: "field" }]);
|
|
216
|
+
const addRelation = (c, from, kind, relation, spec) => c.apply({ op: "add-relation", kind, relation, ...spec }, from, [{ part: `${kind}.${relation}`, what: "relation" }]);
|
|
217
|
+
const addAct = (c, from, act, spec) => c.apply({ op: "add-act", act, ...spec }, from, [{ part: act, what: "act" }]);
|
|
218
|
+
const addRule = (c, from, rule, spec) => c.apply({ op: "add-rule", rule, ...spec }, from, [{ part: rule, what: "rule" }]);
|
|
219
|
+
const addLens = (c, from, title, spec) => c.apply({ op: "add-lens", title, ...spec }, from, [{ part: title, what: "lens" }]);
|
|
220
|
+
/*
|
|
221
|
+
* THE MODULES, one a trait. Each reads the document as it stands, reuses
|
|
222
|
+
* what is there (a kind that is people, a status, a money field) and adds
|
|
223
|
+
* only what is missing, joined to the kind people see most.
|
|
224
|
+
*/
|
|
225
|
+
const MODULES = {
|
|
226
|
+
owners(c, from) {
|
|
227
|
+
const main = c.main;
|
|
228
|
+
// The people, or a person kind brought in; where the people are what the app is about, one of them has each on them.
|
|
229
|
+
let person = c.peopleKind();
|
|
230
|
+
if (!person) {
|
|
231
|
+
person = c.freeKind("person");
|
|
232
|
+
if (!addKind(c, from, person, { noun: "person", plural: "people", description: "Somebody who has things on them.", fields: { name: { type: "string", required: true } }, label: "{name}" }))
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const owner = c.freeField(main, "owner", "ownedBy");
|
|
236
|
+
if (!addRelation(c, from, main, owner, { to: [person], cardinality: "one", description: "who it is on", inverse: `their ${c.plural(main)}` }))
|
|
237
|
+
return;
|
|
238
|
+
const give = c.freeAct(`give-${main}`, "give-to");
|
|
239
|
+
addAct(c, from, give, { title: "Give it to someone", on: main, connects: owner, description: `Say who ${an(c.noun(main))} ${c.noun(main)} is on.`, fromTheOtherEnd: `Give them one of the ${c.plural(main)}` });
|
|
240
|
+
addRule(c, from, c.freeRule(`${main}-has-an-owner`), { title: `Every ${c.noun(main)} has somebody on it`, description: `${capital(an(c.noun(main)))} ${c.noun(main)} that is everyone's is nobody's.`, over: main, require: `present(${owner})`, says: `Nobody is on ${c.label(main)}`, repairs: [{ act: give, label: "Give it to someone" }] });
|
|
241
|
+
addLens(c, from, c.freeTitle("Who has what"), { lens: "coverage", bindings: { rows: { kind: main }, columns: { kind: person }, link: { edge: owner } } });
|
|
242
|
+
},
|
|
243
|
+
status(c, from) {
|
|
244
|
+
const main = c.main;
|
|
245
|
+
let status = c.status(main);
|
|
246
|
+
if (!status) {
|
|
247
|
+
const field = c.freeField(main, "status", "stage");
|
|
248
|
+
if (!addField(c, from, main, field, { type: "enum", options: ["open", "doing", "done"], default: "open", required: true }))
|
|
249
|
+
return;
|
|
250
|
+
status = { field, options: ["open", "doing", "done"] };
|
|
251
|
+
addAct(c, from, c.freeAct(`start-${main}`, "start"), { title: "Start", on: main, sets: { [field]: "doing" }, allowedWhen: `${field} == 'open'`, refusal: `${c.label(main)} is already under way`, description: "Take it up." });
|
|
252
|
+
addAct(c, from, c.freeAct(`finish-${main}`, "finish"), { title: "Finish", on: main, sets: { [field]: "done" }, allowedWhen: `${field} != 'done'`, refusal: `${c.label(main)} is already done`, description: "Mark it done." });
|
|
253
|
+
addAct(c, from, c.freeAct(`reopen-${main}`, "reopen"), { title: "Reopen", on: main, sets: { [field]: "open" }, allowedWhen: `${field} != 'open'`, refusal: `${c.label(main)} is already open`, description: "Put it back at the start." });
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
// A status that nothing moves: a named step to each of its values.
|
|
257
|
+
const moved = new Set(movingChoices(c.doc, main).flatMap((choice) => (choice.field === status.field ? choice.steps : [])).flatMap((act) => [c.doc.acts?.[act]?.sets?.[status.field]].filter((value) => typeof value === "string")));
|
|
258
|
+
for (const option of status.options) {
|
|
259
|
+
if (moved.has(option))
|
|
260
|
+
continue;
|
|
261
|
+
addAct(c, from, c.freeAct(`mark-${kebab(option)}`), { title: `Mark ${option.toLowerCase()}`, on: main, sets: { [status.field]: option }, allowedWhen: `${status.field} != ${quote(option)}`, refusal: `${c.label(main)} is already ${option.toLowerCase()}`, description: `Move it to ${option.toLowerCase()}.` });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
addLens(c, from, c.freeTitle(`${c.Plural(main)} by ${humanizeField(status.field).toLowerCase()}`), { lens: "columns", bindings: { [main]: { column: status.field } } });
|
|
265
|
+
},
|
|
266
|
+
schedule(c, from) {
|
|
267
|
+
const main = c.main;
|
|
268
|
+
let due = Object.entries(c.spec(main).fields).find(([name, field]) => isDate(field) && DUE_FIELD.test(name))?.[0];
|
|
269
|
+
if (!due) {
|
|
270
|
+
due = c.freeField(main, "due", "dueOn");
|
|
271
|
+
if (!addField(c, from, main, due, { type: "date", description: "when it is due" }))
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const set = c.freeAct(`set-${main}-date`, "set-date");
|
|
275
|
+
addAct(c, from, set, { title: "Set the date", on: main, writes: [due], description: "Say when it is due, or move the date." });
|
|
276
|
+
const open = c.stillOpen(main);
|
|
277
|
+
addRule(c, from, c.freeRule(`${main}-on-time`), { title: `${c.Plural(main)} are done by their date`, over: main, when: open ? `present(${due}) and ${open}` : `present(${due})`, require: `days(today(), ${due}) >= 0`, says: `${c.label(main)} is past its date`, repairs: [{ act: set, label: "Move the date" }] });
|
|
278
|
+
addLens(c, from, c.freeTitle(`${c.Plural(main)} by date`), { lens: "calendar", bindings: { [main]: { start: due } } });
|
|
279
|
+
},
|
|
280
|
+
budget(c, from) {
|
|
281
|
+
const main = c.main;
|
|
282
|
+
let amount = Object.entries(c.spec(main).fields).find(([name, field]) => isMoney(name, field))?.[0];
|
|
283
|
+
if (!amount) {
|
|
284
|
+
amount = c.freeField(main, "cost", "amount");
|
|
285
|
+
if (!addField(c, from, main, amount, { type: "number", format: "money", description: "what it costs" }))
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const group = c.groupOf(main);
|
|
289
|
+
const holder = group?.kind ?? main;
|
|
290
|
+
let budget = Object.entries(c.spec(holder).fields).find(([name, field]) => (field.type === "number" || field.type === "integer") && /budget|limit|allowance/i.test(name))?.[0];
|
|
291
|
+
if (!budget) {
|
|
292
|
+
budget = c.freeField(holder, "budget");
|
|
293
|
+
if (!addField(c, from, holder, budget, { type: "number", format: "money", description: "the most to spend" }))
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const set = c.freeAct("set-budget", `set-${holder}-budget`);
|
|
297
|
+
addAct(c, from, set, { title: "Set the budget", on: holder, writes: [budget], description: "Set or raise the most to spend." });
|
|
298
|
+
if (group) {
|
|
299
|
+
addRule(c, from, c.freeRule(`${holder}-within-budget`), { title: `${c.Plural(main)} fit the ${c.noun(holder)}'s budget`, over: holder, when: `present(${budget})`, require: `sum(in(${quote(group.relation)}), ${quote(amount)}) <= ${budget}`, says: `${c.label(holder)} is over its {${budget}|money} budget`, repairs: [{ act: set, label: "Raise the budget" }] });
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
addRule(c, from, c.freeRule(`${main}-within-budget`), { title: `${c.Plural(main)} fit their budget`, over: main, when: `present(${budget}) and present(${amount})`, require: `${amount} <= ${budget}`, says: `${c.label(main)} is over its {${budget}|money} budget`, repairs: [{ act: set, label: "Raise the budget" }] });
|
|
303
|
+
}
|
|
304
|
+
// Money with no currency is said as a bare number: the app's is said once.
|
|
305
|
+
if (!c.doc.brand?.currency)
|
|
306
|
+
c.apply({ op: "set-brand", currency: "USD" }, from);
|
|
307
|
+
},
|
|
308
|
+
places(c, from) {
|
|
309
|
+
const main = c.main;
|
|
310
|
+
let place = c.kindNamed(PLACE_NOUNS);
|
|
311
|
+
if (!place || place === main) {
|
|
312
|
+
place = c.freeKind("place");
|
|
313
|
+
if (!addKind(c, from, place, { noun: "place", plural: "places", description: "Somewhere things happen or are kept.", fields: { name: { type: "string", required: true }, address: { type: "string" } }, label: "{name}" }))
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const at = c.freeField(main, "at", "location");
|
|
317
|
+
if (!addRelation(c, from, main, at, { to: [place], cardinality: "one", description: "where it is", inverse: "what is here" }))
|
|
318
|
+
return;
|
|
319
|
+
addLens(c, from, c.freeTitle(`${c.Plural(main)} by place`), { lens: "coverage", bindings: { rows: { kind: main }, columns: { kind: place }, link: { edge: at } } });
|
|
320
|
+
},
|
|
321
|
+
sources(c, from) {
|
|
322
|
+
const main = c.main;
|
|
323
|
+
let source = c.kindNamed(SOURCE_NOUNS);
|
|
324
|
+
if (!source || source === main) {
|
|
325
|
+
source = c.freeKind("source");
|
|
326
|
+
if (!addKind(c, from, source, { noun: "source", plural: "sources", description: "Where a claim comes from: an article, a page, a report.", fields: { title: { type: "string", required: true }, link: { type: "url" }, publisher: { type: "string" } }, label: "{title}" }))
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const cites = c.freeField(main, "cites", "citesSource");
|
|
330
|
+
if (!addRelation(c, from, main, cites, { to: [source], cardinality: "many", description: "what backs it up", inverse: "cited by" }))
|
|
331
|
+
return;
|
|
332
|
+
const cite = c.freeAct("cite", `cite-${main}`);
|
|
333
|
+
addAct(c, from, cite, { title: "Cite a source", on: main, connects: cites, description: "Say where this comes from.", fromTheOtherEnd: `Cite it on one of the ${c.plural(main)}` });
|
|
334
|
+
const status = c.status(main);
|
|
335
|
+
const done = status?.options.filter((option) => DONE.has(option.toLowerCase())) ?? [];
|
|
336
|
+
addRule(c, from, c.freeRule(`${main}-cites-a-source`), { title: `${done.length > 0 ? `${capital(listedWords(done.map((value) => value.toLowerCase()), "or"))} ${c.plural(main)} cite` : `${c.Plural(main)} cite`} a source`, over: main, ...(done.length > 0 ? { when: done.length === 1 ? `${status.field} == ${quote(done[0])}` : `${status.field} in [${done.map(quote).join(", ")}]` } : {}), require: `count(out(${quote(cites)})) >= 1`, says: `${c.label(main)} cites no source`, repairs: [{ act: cite, label: "Cite a source" }] });
|
|
337
|
+
},
|
|
338
|
+
stock(c, from) {
|
|
339
|
+
const main = c.main;
|
|
340
|
+
const onHand = c.freeField(main, "onHand", "inStock");
|
|
341
|
+
if (!addField(c, from, main, onHand, { type: "integer", label: "On hand", description: "how many there are", min: 0 }))
|
|
342
|
+
return;
|
|
343
|
+
const reorderAt = c.freeField(main, "reorderAt");
|
|
344
|
+
if (!addField(c, from, main, reorderAt, { type: "integer", label: "Reorder at", description: "the count to reorder at", min: 0 }))
|
|
345
|
+
return;
|
|
346
|
+
const count = c.freeAct("count-stock", `count-${main}`);
|
|
347
|
+
addAct(c, from, count, { title: "Count the stock", on: main, writes: [onHand], description: "Say how many there are now." });
|
|
348
|
+
addAct(c, from, c.freeAct("set-reorder-point"), { title: "Set the reorder point", on: main, writes: [reorderAt], description: "Say the count to reorder at." });
|
|
349
|
+
addRule(c, from, c.freeRule(`${main}-above-reorder-point`), { title: `${c.Plural(main)} are above their reorder point`, over: main, when: `present(${reorderAt})`, require: `present(${onHand}) and ${onHand} >= ${reorderAt}`, says: `${c.label(main)} is running low`, repairs: [{ act: count, label: "Count it" }] });
|
|
350
|
+
},
|
|
351
|
+
votes(c, from) {
|
|
352
|
+
const main = c.main;
|
|
353
|
+
const votes = c.freeField(main, "votes");
|
|
354
|
+
if (!addField(c, from, main, votes, { type: "integer", default: 0, min: 0, description: "how many asked for it" }))
|
|
355
|
+
return;
|
|
356
|
+
addAct(c, from, c.freeAct("vote", `vote-for-${main}`), { title: "Vote for it", on: main, sets: { [votes]: { expr: `if(${votes} == null, 1, ${votes} + 1)` } }, description: "Count one more person for it." });
|
|
357
|
+
},
|
|
358
|
+
private(c, from) {
|
|
359
|
+
const main = c.main;
|
|
360
|
+
const others = Object.keys(c.doc.kinds).filter((kind) => kind !== main);
|
|
361
|
+
// Present, a sight is the whole story: a kind no sight names is seen by nobody, so the rest are shown to everyone first.
|
|
362
|
+
if (!c.doc.policy?.sees?.length && others.length > 0)
|
|
363
|
+
c.apply({ op: "add-sight", roles: "*", kinds: others }, from, [{ part: `${others.join(", ")}: everyone`, what: "sight" }]);
|
|
364
|
+
c.apply({ op: "add-sight", roles: "*", kinds: [main], own: true }, from, [{ part: `${main}: own`, what: "sight" }]);
|
|
365
|
+
},
|
|
366
|
+
history(c, from) {
|
|
367
|
+
const main = c.main;
|
|
368
|
+
let season = c.kindNamed(GROUPING_NOUNS);
|
|
369
|
+
if (!season || season === main) {
|
|
370
|
+
season = c.freeKind("season");
|
|
371
|
+
if (!addKind(c, from, season, { noun: "season", plural: "seasons", description: "A stretch of the record: a year, a run, an era.", fields: { name: { type: "string", required: true }, year: { type: "integer" }, notes: { type: "text" } }, label: "{name}" }))
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const inSeason = c.freeField(main, "inSeason", "partOf");
|
|
375
|
+
if (!addRelation(c, from, main, inSeason, { to: [season], cardinality: "one", description: "the season it belongs to", inverse: `its ${c.plural(main)}` }))
|
|
376
|
+
return;
|
|
377
|
+
if (!Object.values(c.spec(main).fields).some(isDate))
|
|
378
|
+
addField(c, from, main, c.freeField(main, "date", "happenedOn"), { type: "date", description: "when it happened" });
|
|
379
|
+
if (!Object.entries(c.spec(main).fields).some(([name]) => /result|outcome|score/i.test(name)))
|
|
380
|
+
addField(c, from, main, c.freeField(main, "result"), { type: "string", description: "how it ended" });
|
|
381
|
+
addLens(c, from, c.freeTitle(`${c.Plural(main)} by ${c.noun(season)}`), { lens: "coverage", bindings: { rows: { kind: main }, columns: { kind: season }, link: { edge: inSeason } } });
|
|
382
|
+
},
|
|
383
|
+
publishing(c, from) {
|
|
384
|
+
const main = c.main;
|
|
385
|
+
let body = Object.entries(c.spec(main).fields).find(([name, field]) => field.type === "text" && /body|content|copy|words|summary/i.test(name))?.[0];
|
|
386
|
+
if (!body) {
|
|
387
|
+
body = c.freeField(main, "body", "words");
|
|
388
|
+
if (!addField(c, from, main, body, { type: "text", format: "markdown", description: "the words themselves" }))
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
let status = c.status(main);
|
|
392
|
+
let published;
|
|
393
|
+
if (status) {
|
|
394
|
+
published = status.options.find((option) => PUBLISHED.test(option)) ?? "published";
|
|
395
|
+
if (!status.options.includes(published) && !c.apply({ op: "set-options", kind: main, field: status.field, add: [published] }, from, [{ part: `${main}.${status.field}: ${published}`, what: "field" }]))
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
const field = c.freeField(main, "status", "stage");
|
|
400
|
+
if (!addField(c, from, main, field, { type: "enum", options: ["draft", "published"], default: "draft", required: true }))
|
|
401
|
+
return;
|
|
402
|
+
status = { field, options: ["draft", "published"] };
|
|
403
|
+
published = "published";
|
|
404
|
+
}
|
|
405
|
+
const publish = c.freeAct("publish", `publish-${main}`);
|
|
406
|
+
addAct(c, from, publish, { title: "Publish", on: main, sets: { [status.field]: published }, allowedWhen: `${status.field} != ${quote(published)}`, refusal: `${c.label(main)} is already ${published}`, description: "Put it out." });
|
|
407
|
+
const first = status.options.find((option) => !PUBLISHED.test(option));
|
|
408
|
+
if (first)
|
|
409
|
+
addAct(c, from, c.freeAct("unpublish", `unpublish-${main}`), { title: "Take it down", on: main, sets: { [status.field]: first }, allowedWhen: `${status.field} == ${quote(published)}`, refusal: `${c.label(main)} is not ${published}`, description: `Take it back to ${first}.` });
|
|
410
|
+
const write = c.freeAct(`write-${main}`, "write");
|
|
411
|
+
addAct(c, from, write, { title: "Write it", on: main, writes: [body], description: "Write or change the words." });
|
|
412
|
+
addRule(c, from, c.freeRule(`${published}-${main}-has-words`), { title: `${capital(published)} ${c.plural(main)} have words`, over: main, when: `${status.field} == ${quote(published)}`, require: `present(${body})`, says: `${c.label(main)} is ${published} with no words`, repairs: [{ act: write, label: "Write it" }] });
|
|
413
|
+
},
|
|
414
|
+
roster(c, from) {
|
|
415
|
+
let person = c.peopleKind();
|
|
416
|
+
if (!person) {
|
|
417
|
+
person = c.freeKind("person");
|
|
418
|
+
if (!addKind(c, from, person, { noun: "person", plural: "people", description: "Somebody on the roster.", fields: { name: { type: "string", required: true } }, label: "{name}" }))
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const spec = c.spec(person);
|
|
422
|
+
if (!Object.keys(spec.fields).some((name) => /role|position|part/i.test(name)))
|
|
423
|
+
addField(c, from, person, c.freeField(person, "role"), { type: "string", description: "what they do" });
|
|
424
|
+
if (!Object.values(spec.fields).some((field) => field.type === "email"))
|
|
425
|
+
addField(c, from, person, c.freeField(person, "email"), { type: "email" });
|
|
426
|
+
if (!Object.values(spec.fields).some((field) => field.type === "phone"))
|
|
427
|
+
addField(c, from, person, c.freeField(person, "phone"), { type: "phone" });
|
|
428
|
+
},
|
|
429
|
+
routines(c, from) {
|
|
430
|
+
const main = c.main;
|
|
431
|
+
const every = c.freeField(main, "every", "often");
|
|
432
|
+
if (!addField(c, from, main, every, { type: "enum", options: ["daily", "weekly", "fortnightly", "monthly"], default: "weekly", required: true, label: "How often" }))
|
|
433
|
+
return;
|
|
434
|
+
const lastDone = c.freeField(main, "lastDone");
|
|
435
|
+
if (!addField(c, from, main, lastDone, { type: "date", label: "Last done" }))
|
|
436
|
+
return;
|
|
437
|
+
const done = c.freeAct("done-today", `${main}-done-today`);
|
|
438
|
+
addAct(c, from, done, { title: "Done today", on: main, sets: { [lastDone]: "$today" }, description: "Note that it was done today." });
|
|
439
|
+
addRule(c, from, c.freeRule(`${main}-not-overdue`), { title: `${c.Plural(main)} are done as often as they come round`, over: main, when: `present(${lastDone})`, require: `days(${lastDone}, today()) <= if(${every} == 'daily', 1, if(${every} == 'weekly', 7, if(${every} == 'fortnightly', 14, 31)))`, says: `${c.label(main)} is overdue — last done {${lastDone}|relative}`, repairs: [{ act: done, label: "Done it today" }] });
|
|
440
|
+
},
|
|
441
|
+
comparison(c, from) {
|
|
442
|
+
const main = c.main;
|
|
443
|
+
let status = c.status(main);
|
|
444
|
+
let chosen;
|
|
445
|
+
if (status) {
|
|
446
|
+
chosen = status.options.find((option) => CHOSEN.test(option)) ?? "chosen";
|
|
447
|
+
if (!status.options.includes(chosen) && !c.apply({ op: "set-options", kind: main, field: status.field, add: [chosen] }, from, [{ part: `${main}.${status.field}: ${chosen}`, what: "field" }]))
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
const field = c.freeField(main, "status", "stage");
|
|
452
|
+
if (!addField(c, from, main, field, { type: "enum", options: ["considering", "chosen", "passed"], default: "considering", required: true }))
|
|
453
|
+
return;
|
|
454
|
+
status = { field, options: ["considering", "chosen", "passed"] };
|
|
455
|
+
chosen = "chosen";
|
|
456
|
+
addAct(c, from, c.freeAct(`pass-on-${main}`, "pass"), { title: "Pass on it", on: main, sets: { [field]: "passed" }, allowedWhen: `${field} != 'passed'`, refusal: `${c.label(main)} was already passed on`, description: "Leave it out of the running; it is kept, not deleted." });
|
|
457
|
+
}
|
|
458
|
+
// The step that chooses, unless one already does ("Book").
|
|
459
|
+
const field = status.field;
|
|
460
|
+
if (!Object.values(c.doc.acts ?? {}).some((act) => [act.on ?? []].flat().includes(main) && act.sets?.[field] === chosen))
|
|
461
|
+
addAct(c, from, c.freeAct(`choose-${main}`, "choose"), { title: "Choose it", on: main, sets: { [field]: chosen }, allowedWhen: `${field} != ${quote(chosen)}`, refusal: `${c.label(main)} is already ${chosen}`, description: "Pick this one." });
|
|
462
|
+
const group = c.groupOf(main);
|
|
463
|
+
if (group) {
|
|
464
|
+
addRule(c, from, c.freeRule(`one-${chosen}-per-${group.kind}`), { title: `One ${c.noun(main)} ${chosen} per ${c.noun(group.kind)}`, over: group.kind, require: `count(in(${quote(group.relation)}) where ${status.field} == ${quote(chosen)}) <= 1`, says: `${c.label(group.kind)} has more than one ${c.noun(main)} ${chosen}` });
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
addRule(c, from, c.freeRule(`one-${chosen}`), { title: `One ${c.noun(main)} ${chosen}`, over: "graph", require: `count(all(${quote(main)}) where ${status.field} == ${quote(chosen)}) <= 1`, says: `More than one ${c.noun(main)} is ${chosen}` });
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
};
|
|
471
|
+
/**
|
|
472
|
+
* DROP A TRAIT: what only it carries goes — its rules, its lenses, its
|
|
473
|
+
* sights, and its relations and fields no other trait names (with the
|
|
474
|
+
* lenses drawn by them, which cannot stand without them), then a kind of
|
|
475
|
+
* its own left standing apart. A carrier two traits share (a status a
|
|
476
|
+
* board and a publish step both stand on) stays, since the other still
|
|
477
|
+
* needs it; so does the kind people see most and the field its label says.
|
|
478
|
+
*/
|
|
479
|
+
function dropTrait(c, name, from) {
|
|
480
|
+
const traits = traitsOfDocument(c.doc);
|
|
481
|
+
const trait = traits.find((one) => one.trait === name);
|
|
482
|
+
if (!trait)
|
|
483
|
+
return c.said.push(`${c.doc.name} has no ${traitWords(name)} to leave out.`);
|
|
484
|
+
const shared = new Set(traits.filter((one) => one.trait !== name).flatMap((one) => [...one.fields, ...one.relations]));
|
|
485
|
+
const main = c.main;
|
|
486
|
+
const removeLens = (title) => c.apply({ op: "remove-lens", title }) && c.left.push({ part: title, what: "lens", from });
|
|
487
|
+
for (const rule of trait.rules)
|
|
488
|
+
if (c.doc.rules?.[rule] && c.apply({ op: "remove-rule", rule }))
|
|
489
|
+
c.left.push({ part: rule, what: "rule", from });
|
|
490
|
+
for (const title of trait.lenses)
|
|
491
|
+
removeLens(title);
|
|
492
|
+
if (name === "private") {
|
|
493
|
+
for (const sight of [...(c.doc.policy?.sees ?? [])]) {
|
|
494
|
+
if ((sight.own || sight.via) && c.apply({ op: "remove-sight", roles: sight.roles, kinds: sight.kinds, ...(sight.own ? { own: true } : {}), ...(sight.via ? { via: sight.via } : {}) }))
|
|
495
|
+
c.left.push({ part: `${sight.kinds.join(", ")}: ${sight.via ? `via ${sight.via}` : "own"}`, what: "sight", from });
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
for (const relation of trait.relations) {
|
|
499
|
+
const [kind, edge] = relation.split(".");
|
|
500
|
+
if (shared.has(relation) || !c.doc.kinds[kind]?.edges?.[edge])
|
|
501
|
+
continue;
|
|
502
|
+
for (const title of lensesDrawnBy(c, kind, edge))
|
|
503
|
+
removeLens(title);
|
|
504
|
+
if (c.apply({ op: "remove-relation", kind, relation: edge }))
|
|
505
|
+
c.left.push({ part: relation, what: "relation", from });
|
|
506
|
+
}
|
|
507
|
+
for (const field of trait.fields) {
|
|
508
|
+
const [kind, fieldName] = field.split(".");
|
|
509
|
+
const spec = c.doc.kinds[kind];
|
|
510
|
+
if (shared.has(field) || !spec?.fields[fieldName] || Object.keys(spec.fields).length < 2 || c.label(kind).includes(`{${fieldName}}`))
|
|
511
|
+
continue;
|
|
512
|
+
for (const title of lensesDrawnBy(c, kind, fieldName))
|
|
513
|
+
removeLens(title);
|
|
514
|
+
if (c.apply({ op: "remove-field", kind, field: fieldName }))
|
|
515
|
+
c.left.push({ part: field, what: "field", from });
|
|
516
|
+
}
|
|
517
|
+
for (const kind of trait.kinds) {
|
|
518
|
+
if (kind === main || !c.doc.kinds[kind] || Object.keys(c.doc.kinds).length < 2)
|
|
519
|
+
continue;
|
|
520
|
+
const linked = Object.values(c.doc.kinds).some((spec) => Object.values(spec.edges ?? {}).some((edge) => Array.isArray(edge.to) && edge.to.includes(kind))) || Object.keys(c.doc.kinds[kind].edges ?? {}).length > 0;
|
|
521
|
+
if (linked)
|
|
522
|
+
continue;
|
|
523
|
+
dropKind(c, kind, from);
|
|
524
|
+
}
|
|
525
|
+
c.left.push({ part: name, what: "trait", from });
|
|
526
|
+
}
|
|
527
|
+
/** Drop a kind, with what only it served (`remove-kind`); the sights that showed only it go first, as the editor asks. */
|
|
528
|
+
function dropKind(c, kind, from) {
|
|
529
|
+
if (!Object.hasOwn(c.doc.kinds, kind))
|
|
530
|
+
return c.said.push(`${c.doc.name} has no kind called ${kind} to leave out.`);
|
|
531
|
+
if (Object.keys(c.doc.kinds).length === 1)
|
|
532
|
+
return c.said.push(`${c.doc.name} keeps at least one kind, so ${kind} stays.`);
|
|
533
|
+
const sees = c.doc.policy?.sees ?? [];
|
|
534
|
+
if (sees.length > 0 && sees.every((sight) => sight.kinds.every((k) => k === kind)))
|
|
535
|
+
for (const sight of [...sees])
|
|
536
|
+
c.apply({ op: "remove-sight", roles: sight.roles, kinds: sight.kinds, ...(sight.own ? { own: true } : {}), ...(sight.via ? { via: sight.via } : {}) });
|
|
537
|
+
if (c.apply({ op: "remove-kind", kind }))
|
|
538
|
+
c.left.push({ part: kind, what: "kind", from });
|
|
539
|
+
}
|
|
540
|
+
const isTrait = (name) => TRAITS.includes(name);
|
|
541
|
+
/**
|
|
542
|
+
* A STARTING DOCUMENT FROM PARTS: the base (or nothing) named, its
|
|
543
|
+
* dropped kinds and traits taken out with what only they served, each
|
|
544
|
+
* added trait's module joined to the kind people see most, and what was
|
|
545
|
+
* taken from where said. A trait the document already carries is left as
|
|
546
|
+
* it is and said so. Every change is one `editDocument` accepts in turn,
|
|
547
|
+
* so the document compiles; `composeDesign` then gives it its jobs and
|
|
548
|
+
* look.
|
|
549
|
+
*/
|
|
550
|
+
export function startingDocument(options) {
|
|
551
|
+
const adds = [...new Set(options.add ?? [])];
|
|
552
|
+
const base = options.base;
|
|
553
|
+
const c = new Composer(base ? JSON.parse(JSON.stringify(base)) : emptyDocument(options.name, adds.includes("roster")));
|
|
554
|
+
const from = base?.name ?? "nothing";
|
|
555
|
+
if (base) {
|
|
556
|
+
if (base.name !== options.name)
|
|
557
|
+
c.apply({ op: "set-name", name: options.name });
|
|
558
|
+
for (const kind of Object.keys(base.kinds))
|
|
559
|
+
c.took.push({ part: kind, what: "kind", from });
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
for (const kind of Object.keys(c.doc.kinds))
|
|
563
|
+
c.took.push({ part: kind, what: "kind", from });
|
|
564
|
+
}
|
|
565
|
+
if (options.purpose !== undefined && options.purpose !== (base?.purpose?.for ?? undefined))
|
|
566
|
+
c.apply({ op: "set-purpose", for: options.purpose });
|
|
567
|
+
for (const name of options.drop ?? []) {
|
|
568
|
+
if (isTrait(name) && !Object.hasOwn(c.doc.kinds, name))
|
|
569
|
+
dropTrait(c, name, from);
|
|
570
|
+
else
|
|
571
|
+
dropKind(c, name, from);
|
|
572
|
+
}
|
|
573
|
+
// What the base still carries after the drops, said as its own: its traits, rules and lenses kept.
|
|
574
|
+
const dropped = new Set(c.left.map((part) => `${part.what}:${part.part}`));
|
|
575
|
+
c.took.splice(0, c.took.length, ...c.took.filter((part) => !dropped.has(`${part.what}:${part.part}`)));
|
|
576
|
+
if (base) {
|
|
577
|
+
for (const trait of traitsOfDocument(c.doc))
|
|
578
|
+
c.took.push({ part: trait.trait, what: "trait", from });
|
|
579
|
+
for (const rule of Object.keys(c.doc.rules ?? {}))
|
|
580
|
+
c.took.push({ part: rule, what: "rule", from });
|
|
581
|
+
for (const lens of c.doc.lenses ?? [])
|
|
582
|
+
if (typeof lens.title === "string")
|
|
583
|
+
c.took.push({ part: lens.title, what: "lens", from });
|
|
584
|
+
}
|
|
585
|
+
for (const trait of adds) {
|
|
586
|
+
if (hasTrait(c.doc, trait)) {
|
|
587
|
+
c.said.push(`${c.doc.name} already has ${traitWords(trait)}.`);
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
const before = c.took.length;
|
|
591
|
+
MODULES[trait](c, trait);
|
|
592
|
+
if (c.took.length > before)
|
|
593
|
+
c.took.push({ part: trait, what: "trait", from: trait });
|
|
594
|
+
}
|
|
595
|
+
/*
|
|
596
|
+
* What the parts made together, judged by the critic once: a kind now the
|
|
597
|
+
* one people see most with no glance, a thirteenth place. Each edit is the
|
|
598
|
+
* critic's own, asked again after it until it has no warning left — as
|
|
599
|
+
* `composeDesign` asks it — so the document is handed on with none.
|
|
600
|
+
*/
|
|
601
|
+
const applied = new Set();
|
|
602
|
+
for (let round = 0; round < 40; round += 1) {
|
|
603
|
+
const next = designFindings(c.doc).find((finding) => finding.severity === "warning" && (finding.edits ?? []).some((edit) => !applied.has(JSON.stringify(edit))));
|
|
604
|
+
if (!next)
|
|
605
|
+
break;
|
|
606
|
+
for (const edit of next.edits ?? []) {
|
|
607
|
+
const key = JSON.stringify(edit);
|
|
608
|
+
if (applied.has(key))
|
|
609
|
+
continue;
|
|
610
|
+
applied.add(key);
|
|
611
|
+
c.apply(edit);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return { document: c.doc, took: c.took, left: c.left, said: c.said, edits: c.edits };
|
|
615
|
+
}
|
|
616
|
+
/** What a removal loses, in plain words; nothing for a rule, a lens or an act, which hold no records. */
|
|
617
|
+
export function breakingOf(base, edit) {
|
|
618
|
+
const kind = typeof edit["kind"] === "string" ? edit["kind"] : undefined;
|
|
619
|
+
if (edit.op === "remove-kind" && kind)
|
|
620
|
+
return `All ${base.kinds[kind] ? traitPluralOf(base, kind) : kind} records go, with their links.`;
|
|
621
|
+
if (edit.op === "remove-field" && kind && typeof edit["field"] === "string")
|
|
622
|
+
return `${capital(kind)}'s ${edit["field"]} values go.`;
|
|
623
|
+
if (edit.op === "remove-relation" && kind && typeof edit["relation"] === "string")
|
|
624
|
+
return `The ${kind} → ${edit["relation"]} links go.`;
|
|
625
|
+
return undefined;
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* A CHANGE TO AN APP COMPOSED FROM A SEAT'S READING (Roadmap f-seat-reshape):
|
|
629
|
+
* the traits to add and the kinds and traits to drop, as the seat read them
|
|
630
|
+
* off a person's words and the person left them on the card, composed onto
|
|
631
|
+
* the app's own document by `startingDocument` — its edits the change a
|
|
632
|
+
* host applies as one, with what each removal loses said. The host's half,
|
|
633
|
+
* on no page: a page says the card in words (`@graview/tools/reshape`) and
|
|
634
|
+
* hands the reading to its host, which composes here, previews with its
|
|
635
|
+
* data and applies as the person.
|
|
636
|
+
*/
|
|
637
|
+
export function composeReshape(options) {
|
|
638
|
+
const made = startingDocument({ base: options.document, name: options.document.name, add: [...new Set(options.add)], drop: [...new Set(options.drop)] });
|
|
639
|
+
return {
|
|
640
|
+
...made,
|
|
641
|
+
breaking: [...new Set(made.edits.map((edit) => breakingOf(options.document, edit)).filter((words) => words !== undefined))],
|
|
642
|
+
notDone: made.said.filter((line) => line.startsWith("Not done")),
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
//# sourceMappingURL=starting-document.js.map
|