@graview/core 0.1.0
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/LICENSE +96 -0
- package/README.md +35 -0
- package/dist/app.d.ts +294 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +49 -0
- package/dist/app.js.map +1 -0
- package/dist/arrange.d.ts +185 -0
- package/dist/arrange.d.ts.map +1 -0
- package/dist/arrange.js +456 -0
- package/dist/arrange.js.map +1 -0
- package/dist/beginning.d.ts +65 -0
- package/dist/beginning.d.ts.map +1 -0
- package/dist/beginning.js +94 -0
- package/dist/beginning.js.map +1 -0
- package/dist/bind.d.ts +42 -0
- package/dist/bind.d.ts.map +1 -0
- package/dist/bind.js +26 -0
- package/dist/bind.js.map +1 -0
- package/dist/city.d.ts +93 -0
- package/dist/city.d.ts.map +1 -0
- package/dist/city.js +160 -0
- package/dist/city.js.map +1 -0
- package/dist/cli/check/arrangement.d.ts +13 -0
- package/dist/cli/check/arrangement.d.ts.map +1 -0
- package/dist/cli/check/arrangement.js +63 -0
- package/dist/cli/check/arrangement.js.map +1 -0
- package/dist/cli/check/beginnings.d.ts +16 -0
- package/dist/cli/check/beginnings.d.ts.map +1 -0
- package/dist/cli/check/beginnings.js +53 -0
- package/dist/cli/check/beginnings.js.map +1 -0
- package/dist/cli/check/brand.d.ts +17 -0
- package/dist/cli/check/brand.d.ts.map +1 -0
- package/dist/cli/check/brand.js +153 -0
- package/dist/cli/check/brand.js.map +1 -0
- package/dist/cli/check/city.d.ts +17 -0
- package/dist/cli/check/city.d.ts.map +1 -0
- package/dist/cli/check/city.js +58 -0
- package/dist/cli/check/city.js.map +1 -0
- package/dist/cli/check/context.d.ts +50 -0
- package/dist/cli/check/context.d.ts.map +1 -0
- package/dist/cli/check/context.js +37 -0
- package/dist/cli/check/context.js.map +1 -0
- package/dist/cli/check/fields.d.ts +16 -0
- package/dist/cli/check/fields.d.ts.map +1 -0
- package/dist/cli/check/fields.js +237 -0
- package/dist/cli/check/fields.js.map +1 -0
- package/dist/cli/check/index.d.ts +14 -0
- package/dist/cli/check/index.d.ts.map +1 -0
- package/dist/cli/check/index.js +14 -0
- package/dist/cli/check/index.js.map +1 -0
- package/dist/cli/check/intelligence.d.ts +17 -0
- package/dist/cli/check/intelligence.d.ts.map +1 -0
- package/dist/cli/check/intelligence.js +136 -0
- package/dist/cli/check/intelligence.js.map +1 -0
- package/dist/cli/check/lenses.d.ts +17 -0
- package/dist/cli/check/lenses.d.ts.map +1 -0
- package/dist/cli/check/lenses.js +239 -0
- package/dist/cli/check/lenses.js.map +1 -0
- package/dist/cli/check/migrations.d.ts +16 -0
- package/dist/cli/check/migrations.d.ts.map +1 -0
- package/dist/cli/check/migrations.js +43 -0
- package/dist/cli/check/migrations.js.map +1 -0
- package/dist/cli/check/modules.d.ts +16 -0
- package/dist/cli/check/modules.d.ts.map +1 -0
- package/dist/cli/check/modules.js +123 -0
- package/dist/cli/check/modules.js.map +1 -0
- package/dist/cli/check/policy.d.ts +16 -0
- package/dist/cli/check/policy.d.ts.map +1 -0
- package/dist/cli/check/policy.js +90 -0
- package/dist/cli/check/policy.js.map +1 -0
- package/dist/cli/check/relations.d.ts +29 -0
- package/dist/cli/check/relations.d.ts.map +1 -0
- package/dist/cli/check/relations.js +279 -0
- package/dist/cli/check/relations.js.map +1 -0
- package/dist/cli/check/routes.d.ts +16 -0
- package/dist/cli/check/routes.d.ts.map +1 -0
- package/dist/cli/check/routes.js +31 -0
- package/dist/cli/check/routes.js.map +1 -0
- package/dist/cli/check/settings.d.ts +16 -0
- package/dist/cli/check/settings.d.ts.map +1 -0
- package/dist/cli/check/settings.js +81 -0
- package/dist/cli/check/settings.js.map +1 -0
- package/dist/cli/check.d.ts +36 -0
- package/dist/cli/check.d.ts.map +1 -0
- package/dist/cli/check.js +76 -0
- package/dist/cli/check.js.map +1 -0
- package/dist/cli/create.d.ts +20 -0
- package/dist/cli/create.d.ts.map +1 -0
- package/dist/cli/create.js +251 -0
- package/dist/cli/create.js.map +1 -0
- package/dist/cli/describe.d.ts +35 -0
- package/dist/cli/describe.d.ts.map +1 -0
- package/dist/cli/describe.js +230 -0
- package/dist/cli/describe.js.map +1 -0
- package/dist/cli/docs.d.ts +11 -0
- package/dist/cli/docs.d.ts.map +1 -0
- package/dist/cli/docs.js +152 -0
- package/dist/cli/docs.js.map +1 -0
- package/dist/cli/index.d.ts +18 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +281 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/extend.d.ts +23 -0
- package/dist/extend.d.ts.map +1 -0
- package/dist/extend.js +53 -0
- package/dist/extend.js.map +1 -0
- package/dist/graph/diff.d.ts +31 -0
- package/dist/graph/diff.d.ts.map +1 -0
- package/dist/graph/diff.js +71 -0
- package/dist/graph/diff.js.map +1 -0
- package/dist/graph/graph.d.ts +69 -0
- package/dist/graph/graph.d.ts.map +1 -0
- package/dist/graph/graph.js +284 -0
- package/dist/graph/graph.js.map +1 -0
- package/dist/graph/primitives.d.ts +52 -0
- package/dist/graph/primitives.d.ts.map +1 -0
- package/dist/graph/primitives.js +62 -0
- package/dist/graph/primitives.js.map +1 -0
- package/dist/graph/tracked.d.ts +31 -0
- package/dist/graph/tracked.d.ts.map +1 -0
- package/dist/graph/tracked.js +69 -0
- package/dist/graph/tracked.js.map +1 -0
- package/dist/graph/types.d.ts +34 -0
- package/dist/graph/types.d.ts.map +1 -0
- package/dist/graph/types.js +0 -0
- package/dist/graph/types.js.map +1 -0
- package/dist/index.d.ts +100 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +90 -0
- package/dist/index.js.map +1 -0
- package/dist/installation.d.ts +112 -0
- package/dist/installation.d.ts.map +1 -0
- package/dist/installation.js +193 -0
- package/dist/installation.js.map +1 -0
- package/dist/intelligence-bridge.d.ts +73 -0
- package/dist/intelligence-bridge.d.ts.map +1 -0
- package/dist/intelligence-bridge.js +31 -0
- package/dist/intelligence-bridge.js.map +1 -0
- package/dist/invariants/engine.d.ts +36 -0
- package/dist/invariants/engine.d.ts.map +1 -0
- package/dist/invariants/engine.js +88 -0
- package/dist/invariants/engine.js.map +1 -0
- package/dist/invariants/types.d.ts +75 -0
- package/dist/invariants/types.d.ts.map +1 -0
- package/dist/invariants/types.js +2 -0
- package/dist/invariants/types.js.map +1 -0
- package/dist/modules.d.ts +75 -0
- package/dist/modules.d.ts.map +1 -0
- package/dist/modules.js +87 -0
- package/dist/modules.js.map +1 -0
- package/dist/mutations/decidable.d.ts +26 -0
- package/dist/mutations/decidable.d.ts.map +1 -0
- package/dist/mutations/decidable.js +39 -0
- package/dist/mutations/decidable.js.map +1 -0
- package/dist/mutations/define-mutation.d.ts +32 -0
- package/dist/mutations/define-mutation.d.ts.map +1 -0
- package/dist/mutations/define-mutation.js +181 -0
- package/dist/mutations/define-mutation.js.map +1 -0
- package/dist/mutations/derive-edits.d.ts +75 -0
- package/dist/mutations/derive-edits.d.ts.map +1 -0
- package/dist/mutations/derive-edits.js +240 -0
- package/dist/mutations/derive-edits.js.map +1 -0
- package/dist/mutations/form.d.ts +63 -0
- package/dist/mutations/form.d.ts.map +1 -0
- package/dist/mutations/form.js +113 -0
- package/dist/mutations/form.js.map +1 -0
- package/dist/mutations/node-ref.d.ts +83 -0
- package/dist/mutations/node-ref.d.ts.map +1 -0
- package/dist/mutations/node-ref.js +113 -0
- package/dist/mutations/node-ref.js.map +1 -0
- package/dist/mutations/types.d.ts +136 -0
- package/dist/mutations/types.d.ts.map +1 -0
- package/dist/mutations/types.js +2 -0
- package/dist/mutations/types.js.map +1 -0
- package/dist/ops/log.d.ts +35 -0
- package/dist/ops/log.d.ts.map +1 -0
- package/dist/ops/log.js +103 -0
- package/dist/ops/log.js.map +1 -0
- package/dist/ops/types.d.ts +52 -0
- package/dist/ops/types.d.ts.map +1 -0
- package/dist/ops/types.js +2 -0
- package/dist/ops/types.js.map +1 -0
- package/dist/ops/undo.d.ts +33 -0
- package/dist/ops/undo.d.ts.map +1 -0
- package/dist/ops/undo.js +65 -0
- package/dist/ops/undo.js.map +1 -0
- package/dist/permissions/policy.d.ts +68 -0
- package/dist/permissions/policy.d.ts.map +1 -0
- package/dist/permissions/policy.js +191 -0
- package/dist/permissions/policy.js.map +1 -0
- package/dist/permissions/types.d.ts +78 -0
- package/dist/permissions/types.d.ts.map +1 -0
- package/dist/permissions/types.js +17 -0
- package/dist/permissions/types.js.map +1 -0
- package/dist/persistence/memory.d.ts +6 -0
- package/dist/persistence/memory.d.ts.map +1 -0
- package/dist/persistence/memory.js +31 -0
- package/dist/persistence/memory.js.map +1 -0
- package/dist/persistence/sqlite.d.ts +59 -0
- package/dist/persistence/sqlite.d.ts.map +1 -0
- package/dist/persistence/sqlite.js +131 -0
- package/dist/persistence/sqlite.js.map +1 -0
- package/dist/persistence/types.d.ts +22 -0
- package/dist/persistence/types.d.ts.map +1 -0
- package/dist/persistence/types.js +2 -0
- package/dist/persistence/types.js.map +1 -0
- package/dist/presence.d.ts +38 -0
- package/dist/presence.d.ts.map +1 -0
- package/dist/presence.js +50 -0
- package/dist/presence.js.map +1 -0
- package/dist/scaffold/domain.d.ts +8 -0
- package/dist/scaffold/domain.d.ts.map +1 -0
- package/dist/scaffold/domain.js +331 -0
- package/dist/scaffold/domain.js.map +1 -0
- package/dist/scaffold/index.d.ts +66 -0
- package/dist/scaffold/index.d.ts.map +1 -0
- package/dist/scaffold/index.js +142 -0
- package/dist/scaffold/index.js.map +1 -0
- package/dist/scaffold/lens.d.ts +31 -0
- package/dist/scaffold/lens.d.ts.map +1 -0
- package/dist/scaffold/lens.js +176 -0
- package/dist/scaffold/lens.js.map +1 -0
- package/dist/scaffold/names.d.ts +44 -0
- package/dist/scaffold/names.d.ts.map +1 -0
- package/dist/scaffold/names.js +69 -0
- package/dist/scaffold/names.js.map +1 -0
- package/dist/scaffold/project.d.ts +41 -0
- package/dist/scaffold/project.d.ts.map +1 -0
- package/dist/scaffold/project.js +604 -0
- package/dist/scaffold/project.js.map +1 -0
- package/dist/scaffold/ui.d.ts +15 -0
- package/dist/scaffold/ui.d.ts.map +1 -0
- package/dist/scaffold/ui.js +536 -0
- package/dist/scaffold/ui.js.map +1 -0
- package/dist/schema/define-node.d.ts +132 -0
- package/dist/schema/define-node.d.ts.map +1 -0
- package/dist/schema/define-node.js +254 -0
- package/dist/schema/define-node.js.map +1 -0
- package/dist/schema/figures.d.ts +42 -0
- package/dist/schema/figures.d.ts.map +1 -0
- package/dist/schema/figures.js +220 -0
- package/dist/schema/figures.js.map +1 -0
- package/dist/schema/json-schema.d.ts +39 -0
- package/dist/schema/json-schema.d.ts.map +1 -0
- package/dist/schema/json-schema.js +74 -0
- package/dist/schema/json-schema.js.map +1 -0
- package/dist/schema/schema.d.ts +43 -0
- package/dist/schema/schema.d.ts.map +1 -0
- package/dist/schema/schema.js +102 -0
- package/dist/schema/schema.js.map +1 -0
- package/dist/schema/types.d.ts +201 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/schema/types.js +2 -0
- package/dist/schema/types.js.map +1 -0
- package/dist/search.d.ts +215 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +449 -0
- package/dist/search.js.map +1 -0
- package/dist/settings.d.ts +30 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +74 -0
- package/dist/settings.js.map +1 -0
- package/dist/store.d.ts +284 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +660 -0
- package/dist/store.js.map +1 -0
- package/dist/studio-door.d.ts +187 -0
- package/dist/studio-door.d.ts.map +1 -0
- package/dist/studio-door.js +20 -0
- package/dist/studio-door.js.map +1 -0
- package/dist/sync/conflict.d.ts +37 -0
- package/dist/sync/conflict.d.ts.map +1 -0
- package/dist/sync/conflict.js +56 -0
- package/dist/sync/conflict.js.map +1 -0
- package/dist/sync/engine.d.ts +118 -0
- package/dist/sync/engine.d.ts.map +1 -0
- package/dist/sync/engine.js +341 -0
- package/dist/sync/engine.js.map +1 -0
- package/dist/sync/google-calendar.d.ts +54 -0
- package/dist/sync/google-calendar.d.ts.map +1 -0
- package/dist/sync/google-calendar.js +167 -0
- package/dist/sync/google-calendar.js.map +1 -0
- package/dist/sync/types.d.ts +125 -0
- package/dist/sync/types.d.ts.map +1 -0
- package/dist/sync/types.js +6 -0
- package/dist/sync/types.js.map +1 -0
- package/dist/temporal/effectivity.d.ts +75 -0
- package/dist/temporal/effectivity.d.ts.map +1 -0
- package/dist/temporal/effectivity.js +89 -0
- package/dist/temporal/effectivity.js.map +1 -0
- package/dist/testing.d.ts +63 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +194 -0
- package/dist/testing.js.map +1 -0
- package/dist/theme/contrast.d.ts +47 -0
- package/dist/theme/contrast.d.ts.map +1 -0
- package/dist/theme/contrast.js +172 -0
- package/dist/theme/contrast.js.map +1 -0
- package/dist/theme/derive.d.ts +81 -0
- package/dist/theme/derive.d.ts.map +1 -0
- package/dist/theme/derive.js +171 -0
- package/dist/theme/derive.js.map +1 -0
- package/dist/theme/kit.d.ts +106 -0
- package/dist/theme/kit.d.ts.map +1 -0
- package/dist/theme/kit.js +84 -0
- package/dist/theme/kit.js.map +1 -0
- package/dist/theme/palettes.d.ts +32 -0
- package/dist/theme/palettes.d.ts.map +1 -0
- package/dist/theme/palettes.js +96 -0
- package/dist/theme/palettes.js.map +1 -0
- package/dist/theme/types.d.ts +131 -0
- package/dist/theme/types.d.ts.map +1 -0
- package/dist/theme/types.js +23 -0
- package/dist/theme/types.js.map +1 -0
- package/dist/views/types.d.ts +89 -0
- package/dist/views/types.d.ts.map +1 -0
- package/dist/views/types.js +88 -0
- package/dist/views/types.js.map +1 -0
- package/dist/watched.d.ts +33 -0
- package/dist/watched.d.ts.map +1 -0
- package/dist/watched.js +50 -0
- package/dist/watched.js.map +1 -0
- package/dist/who.d.ts +26 -0
- package/dist/who.d.ts.map +1 -0
- package/dist/who.js +20 -0
- package/dist/who.js.map +1 -0
- package/package.json +77 -0
package/dist/search.d.ts
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { type Condition } from "./arrange.js";
|
|
2
|
+
import type { Operation } from "./ops/types.js";
|
|
3
|
+
import type { Principal } from "./permissions/types.js";
|
|
4
|
+
import type { AnySchema } from "./schema/schema.js";
|
|
5
|
+
import type { AnyNodeDefinition } from "./schema/types.js";
|
|
6
|
+
import type { Store } from "./store.js";
|
|
7
|
+
import type { Place } from "./views/types.js";
|
|
8
|
+
/**
|
|
9
|
+
* SEARCH IS ONE SEAM, and the graph is the result list.
|
|
10
|
+
*
|
|
11
|
+
* A person asks a picture of a domain four things — where is the thing
|
|
12
|
+
* called X, where do I go for X, what can I do about X, what is X tied to —
|
|
13
|
+
* and the framework answered pieces of each in different places with
|
|
14
|
+
* different code: the list page's `?q=`, the inspector's searcher over acts,
|
|
15
|
+
* the conversation's "a node named in the message", the arrangement's `q`.
|
|
16
|
+
* This is the one matcher behind all of them. It is headless, derived from
|
|
17
|
+
* the declaration (what `readableFields` would show is what is searchable;
|
|
18
|
+
* `display.hide` is the opt-out), policy-honest (what a seat may not see is
|
|
19
|
+
* not a hit) and not fuzzy: a typo is answered with nothing and the words
|
|
20
|
+
* shown back, which is honest and cheap. See docs/search.md.
|
|
21
|
+
*/
|
|
22
|
+
/** How a hit matched, strongest first. */
|
|
23
|
+
export type MatchStrength = "exact" | "prefix" | "word" | "part" | "field";
|
|
24
|
+
/** Why a hit is shown: the field that matched and the words around the match. */
|
|
25
|
+
export interface Why {
|
|
26
|
+
/** `label`, a field name, `plural`, `title` or `name`. */
|
|
27
|
+
readonly field: string;
|
|
28
|
+
/** The field as it reads: "Notes", "Name", "Title". */
|
|
29
|
+
readonly reading: string;
|
|
30
|
+
/** The matching text, cut to a window around the first match: "…the van on Friday…". */
|
|
31
|
+
readonly fragment: string;
|
|
32
|
+
readonly strength: MatchStrength;
|
|
33
|
+
}
|
|
34
|
+
export type Hit = {
|
|
35
|
+
readonly about: "node";
|
|
36
|
+
readonly id: string;
|
|
37
|
+
readonly kind: string;
|
|
38
|
+
readonly label: string;
|
|
39
|
+
readonly why: Why;
|
|
40
|
+
/** False for a record its kind's lifecycle has retired; only seen under `is:any` or `is:past`. */
|
|
41
|
+
readonly current: boolean;
|
|
42
|
+
readonly flagged: boolean;
|
|
43
|
+
/** What tells it apart from another hit of the same name — "single" beside "album". */
|
|
44
|
+
readonly apart?: string;
|
|
45
|
+
} | {
|
|
46
|
+
readonly about: "kind";
|
|
47
|
+
readonly kind: string;
|
|
48
|
+
/** The plural, as a district is headed. */
|
|
49
|
+
readonly label: string;
|
|
50
|
+
readonly why: Why;
|
|
51
|
+
/** How many node hits are of this kind. */
|
|
52
|
+
readonly count: number;
|
|
53
|
+
} | {
|
|
54
|
+
readonly about: "place";
|
|
55
|
+
readonly kind: string;
|
|
56
|
+
readonly title: string;
|
|
57
|
+
/** The place's name in a stop. */
|
|
58
|
+
readonly as: string;
|
|
59
|
+
readonly why: Why;
|
|
60
|
+
} | {
|
|
61
|
+
readonly about: "act";
|
|
62
|
+
readonly name: string;
|
|
63
|
+
readonly title: string;
|
|
64
|
+
/** The node the act would be offered on — an act is never a subjectless hit. */
|
|
65
|
+
readonly subject: string;
|
|
66
|
+
readonly why: Why;
|
|
67
|
+
readonly destructive: boolean;
|
|
68
|
+
} | {
|
|
69
|
+
readonly about: "rule";
|
|
70
|
+
readonly name: string;
|
|
71
|
+
readonly label: string;
|
|
72
|
+
/** The kind the rule judges, when it is scoped to one. */
|
|
73
|
+
readonly kind?: string;
|
|
74
|
+
readonly why: Why;
|
|
75
|
+
};
|
|
76
|
+
export type HitAbout = Hit["about"];
|
|
77
|
+
/** A `key:value` token from the query, and which kinds it applied to. */
|
|
78
|
+
export interface SearchCondition extends Condition {
|
|
79
|
+
/** The searched kinds that can be narrowed by it; the rest ignore it. */
|
|
80
|
+
readonly admittedBy: readonly string[];
|
|
81
|
+
}
|
|
82
|
+
export interface SearchResult {
|
|
83
|
+
readonly hits: readonly Hit[];
|
|
84
|
+
/** The words, without the conditions, as typed. */
|
|
85
|
+
readonly words: string;
|
|
86
|
+
readonly conditions: readonly SearchCondition[];
|
|
87
|
+
/** What was looked through: the kinds (after `kind:` and the policy) and whether past records were. */
|
|
88
|
+
readonly searched: {
|
|
89
|
+
readonly kinds: readonly string[];
|
|
90
|
+
readonly past: boolean;
|
|
91
|
+
};
|
|
92
|
+
/** Node hits per kind, before the limit — what a district counts at altitude. */
|
|
93
|
+
readonly byKind: Readonly<Record<string, number>>;
|
|
94
|
+
/** Node hits in all, before the limit. */
|
|
95
|
+
readonly total: number;
|
|
96
|
+
/**
|
|
97
|
+
* Every matching record's id, in rank order, before the limit — what a
|
|
98
|
+
* picture lights. The hits are an index to read; this is the answer to
|
|
99
|
+
* "is this one of them", and a capped list would dim real matches.
|
|
100
|
+
*/
|
|
101
|
+
readonly matched: readonly string[];
|
|
102
|
+
}
|
|
103
|
+
export interface SearchOptions {
|
|
104
|
+
/** Who is looking. A kind this principal may not see yields no hits of any sort. */
|
|
105
|
+
readonly principal?: Principal;
|
|
106
|
+
/** The subject: the selection, the focus, the pointer's settle. A record one edge from it ranks first in its tier. */
|
|
107
|
+
readonly from?: readonly string[];
|
|
108
|
+
/**
|
|
109
|
+
* The kind the person is in — the district they went into, or the kind of
|
|
110
|
+
* the record they are on. Its records lead the others of the same
|
|
111
|
+
* strength: "Gone Digital" typed inside the songs is the song, not the
|
|
112
|
+
* album that happens to sort first.
|
|
113
|
+
*/
|
|
114
|
+
readonly inKind?: string;
|
|
115
|
+
/** The node hit that is highlighted; its acts become hits. */
|
|
116
|
+
readonly subject?: string;
|
|
117
|
+
/** The places the view registry names — the store cannot see pictures. */
|
|
118
|
+
readonly places?: readonly Place[];
|
|
119
|
+
/** Node ids the rules implicate. Read off the store's violations when absent. */
|
|
120
|
+
readonly flagged?: ReadonlySet<string>;
|
|
121
|
+
/** YYYY-MM-DD, for the lifecycle. */
|
|
122
|
+
readonly today?: string;
|
|
123
|
+
/**
|
|
124
|
+
* What the op log touched lately (`touchWeights`), when the caller holds
|
|
125
|
+
* it — a Find box asks on every keystroke, and the log only changes when
|
|
126
|
+
* the graph does.
|
|
127
|
+
*/
|
|
128
|
+
readonly touched?: ReadonlyMap<string, number>;
|
|
129
|
+
/**
|
|
130
|
+
* The kinds to look in, when fewer than all the seat may see: the scene
|
|
131
|
+
* looks only in the districts it draws. Never widens past the policy.
|
|
132
|
+
*/
|
|
133
|
+
readonly kinds?: readonly string[];
|
|
134
|
+
/** Most hits returned; counts are taken before it. 50 when unsaid. */
|
|
135
|
+
readonly limit?: number;
|
|
136
|
+
}
|
|
137
|
+
/** Lower case, diacritics dropped, every run of punctuation a single space. */
|
|
138
|
+
export declare function fold(text: string): string;
|
|
139
|
+
/** Folded, then with the spaces gone: what `exact` and `prefix` compare. */
|
|
140
|
+
export declare function squeeze(text: string): string;
|
|
141
|
+
/**
|
|
142
|
+
* How strongly `text` answers the words, or undefined when it does not.
|
|
143
|
+
*
|
|
144
|
+
* Every word has to be found, each as the start of a word of the text —
|
|
145
|
+
* `van` finds "Book the van" and "vans", never "caravan". Exact and prefix
|
|
146
|
+
* are judged on the squeezed whole, so "book the" is a prefix of "Book the
|
|
147
|
+
* van" however it was punctuated.
|
|
148
|
+
*/
|
|
149
|
+
export declare function strengthOf(text: string, words: readonly string[]): Exclude<MatchStrength, "field"> | undefined;
|
|
150
|
+
/** The text around the first word's match, with an ellipsis where it was cut. */
|
|
151
|
+
export declare function fragmentOf(text: string, words: readonly string[], width?: number): string;
|
|
152
|
+
export interface ParsedQuery {
|
|
153
|
+
readonly words: string;
|
|
154
|
+
readonly conditions: readonly Condition[];
|
|
155
|
+
}
|
|
156
|
+
export declare function parseQuery(query: string): ParsedQuery;
|
|
157
|
+
/**
|
|
158
|
+
* What a kind is searched by, from its declaration alone: the name, and
|
|
159
|
+
* every field a person would read on a record — which is to say every
|
|
160
|
+
* field but the hidden and the ones with no one-line rendering. For
|
|
161
|
+
* `describe` and llms.txt, so an agent knows what the words reach.
|
|
162
|
+
*/
|
|
163
|
+
export declare function searchableFields(schema: AnySchema, kind: string): readonly {
|
|
164
|
+
readonly key: string;
|
|
165
|
+
readonly reading: string;
|
|
166
|
+
}[];
|
|
167
|
+
/**
|
|
168
|
+
* Whether a node answers the words, and why.
|
|
169
|
+
*
|
|
170
|
+
* The label is judged first and whole; failing that, every word has to be
|
|
171
|
+
* found somewhere on the node — "van large" finds the large job about the
|
|
172
|
+
* van — and the why names the field that carried a word the label did not.
|
|
173
|
+
*/
|
|
174
|
+
export declare function matchNode(definition: AnyNodeDefinition | undefined, node: {
|
|
175
|
+
id: string;
|
|
176
|
+
kind: string;
|
|
177
|
+
} & Record<string, unknown>, words: string): Why | undefined;
|
|
178
|
+
export declare function touchWeights(ops: readonly Operation[]): ReadonlyMap<string, number>;
|
|
179
|
+
/**
|
|
180
|
+
* Finds what the words name, anywhere in the graph, for one seat.
|
|
181
|
+
*
|
|
182
|
+
* Node hits are records whose label or readable fields carry every word;
|
|
183
|
+
* kind, place and rule hits are what the words name in the declaration;
|
|
184
|
+
* act hits are the acts the seat may run on `subject`, the highlighted
|
|
185
|
+
* node hit — never subjectless. A declaration hit leads only when the words
|
|
186
|
+
* name it exactly or as a prefix; otherwise the records lead.
|
|
187
|
+
*
|
|
188
|
+
* Ranking, stable: how the words matched (exact label, label prefix, whole
|
|
189
|
+
* words of the label, parts of it, a field), then near the subject, then
|
|
190
|
+
* current before past, then recently touched, then flagged before clear,
|
|
191
|
+
* then alphabetical, then by id.
|
|
192
|
+
*/
|
|
193
|
+
export declare function search<S extends AnySchema>(store: Store<S>, query: string, options?: SearchOptions): SearchResult;
|
|
194
|
+
/**
|
|
195
|
+
* WHAT CAN BE DONE ABOUT IT: the acts a seat may run on one record, as
|
|
196
|
+
* hits. An act is never a subjectless hit — it is offered on the thing the
|
|
197
|
+
* person highlighted, with its own title — and the words that name the
|
|
198
|
+
* record may name the act too ("van finish"), so each act is judged on the
|
|
199
|
+
* words its own title carries. The named first, the destructive last.
|
|
200
|
+
*
|
|
201
|
+
* Its own function because a Find box asks it for a highlighted row on
|
|
202
|
+
* every keystroke, and has no need to rescan the graph to get it.
|
|
203
|
+
*/
|
|
204
|
+
export declare function actsOn<S extends AnySchema>(store: Store<S>, subjectId: string, words: string, options?: {
|
|
205
|
+
readonly principal?: Principal;
|
|
206
|
+
readonly limit?: number;
|
|
207
|
+
}): readonly Extract<Hit, {
|
|
208
|
+
about: "act";
|
|
209
|
+
}>[];
|
|
210
|
+
/**
|
|
211
|
+
* The sentence for what was searched, for an empty state or a strip's
|
|
212
|
+
* footer: "Tasks and lists, current ones; add is:any for past ones."
|
|
213
|
+
*/
|
|
214
|
+
export declare function describeSearched(schema: AnySchema, searched: SearchResult["searched"]): string;
|
|
215
|
+
//# sourceMappingURL=search.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../src/search.ts"],"names":[],"mappings":"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"}
|
package/dist/search.js
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import { arrangeable, asksForThePast, conditionHolds } from "./arrange.js";
|
|
2
|
+
import { describeArg } from "./mutations/node-ref.js";
|
|
3
|
+
import { humaniseField, isCurrent, labelOf, readableFields, tellApart } from "./schema/define-node.js";
|
|
4
|
+
/*
|
|
5
|
+
* SQUEEZED TEXT. Case, diacritics and punctuation aside — "Café-Noir" and
|
|
6
|
+
* "cafe noir" are the same words to a person typing. Words stay separate,
|
|
7
|
+
* because token alignment is what keeps "bo" out of "elbow".
|
|
8
|
+
*/
|
|
9
|
+
/** Lower case, diacritics dropped, every run of punctuation a single space. */
|
|
10
|
+
export function fold(text) {
|
|
11
|
+
return text
|
|
12
|
+
.normalize("NFD")
|
|
13
|
+
.replace(/\p{M}+/gu, "")
|
|
14
|
+
.toLowerCase()
|
|
15
|
+
.replace(/[^\p{L}\p{N}]+/gu, " ")
|
|
16
|
+
.trim();
|
|
17
|
+
}
|
|
18
|
+
/** Folded, then with the spaces gone: what `exact` and `prefix` compare. */
|
|
19
|
+
export function squeeze(text) {
|
|
20
|
+
return fold(text).replace(/ /g, "");
|
|
21
|
+
}
|
|
22
|
+
const tokensOf = (text) => fold(text).split(" ").filter(Boolean);
|
|
23
|
+
/**
|
|
24
|
+
* How strongly `text` answers the words, or undefined when it does not.
|
|
25
|
+
*
|
|
26
|
+
* Every word has to be found, each as the start of a word of the text —
|
|
27
|
+
* `van` finds "Book the van" and "vans", never "caravan". Exact and prefix
|
|
28
|
+
* are judged on the squeezed whole, so "book the" is a prefix of "Book the
|
|
29
|
+
* van" however it was punctuated.
|
|
30
|
+
*/
|
|
31
|
+
export function strengthOf(text, words) {
|
|
32
|
+
if (words.length === 0)
|
|
33
|
+
return undefined;
|
|
34
|
+
const whole = squeeze(text);
|
|
35
|
+
const asked = words.join("");
|
|
36
|
+
if (whole.length === 0)
|
|
37
|
+
return undefined;
|
|
38
|
+
if (whole === asked)
|
|
39
|
+
return "exact";
|
|
40
|
+
const own = tokensOf(text);
|
|
41
|
+
const found = words.every((word) => own.some((token) => token.startsWith(word)));
|
|
42
|
+
if (!found)
|
|
43
|
+
return undefined;
|
|
44
|
+
if (whole.startsWith(asked))
|
|
45
|
+
return "prefix";
|
|
46
|
+
return words.every((word) => own.includes(word)) ? "word" : "part";
|
|
47
|
+
}
|
|
48
|
+
/** The text around the first word's match, with an ellipsis where it was cut. */
|
|
49
|
+
export function fragmentOf(text, words, width = 48) {
|
|
50
|
+
if (text.length <= width)
|
|
51
|
+
return text;
|
|
52
|
+
const first = words[0];
|
|
53
|
+
const folded = text.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase();
|
|
54
|
+
const at = first ? folded.search(new RegExp(`(^|[^\\p{L}\\p{N}])${escape(first)}`, "u")) : -1;
|
|
55
|
+
const hit = at < 0 ? 0 : at + (folded[at] && /[\p{L}\p{N}]/u.test(folded[at]) ? 0 : 1);
|
|
56
|
+
let start = Math.max(0, hit - Math.floor(width / 3));
|
|
57
|
+
// Start on a word, not in one.
|
|
58
|
+
if (start > 0) {
|
|
59
|
+
const space = text.lastIndexOf(" ", start);
|
|
60
|
+
start = space < 0 ? 0 : space + 1;
|
|
61
|
+
}
|
|
62
|
+
let end = Math.min(text.length, start + width);
|
|
63
|
+
if (end < text.length) {
|
|
64
|
+
const space = text.indexOf(" ", end);
|
|
65
|
+
end = space < 0 || space - end > 16 ? end : space;
|
|
66
|
+
}
|
|
67
|
+
return `${start > 0 ? "…" : ""}${text.slice(start, end).trim()}${end < text.length ? "…" : ""}`;
|
|
68
|
+
}
|
|
69
|
+
const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
70
|
+
const CONDITION = /^([\p{L}][\p{L}\p{N}_-]*):(\S+)$/u;
|
|
71
|
+
export function parseQuery(query) {
|
|
72
|
+
const words = [];
|
|
73
|
+
const conditions = [];
|
|
74
|
+
for (const token of query.trim().split(/\s+/).filter(Boolean)) {
|
|
75
|
+
const match = CONDITION.exec(token);
|
|
76
|
+
if (match)
|
|
77
|
+
conditions.push({ key: match[1], value: match[2] });
|
|
78
|
+
else
|
|
79
|
+
words.push(token);
|
|
80
|
+
}
|
|
81
|
+
return { words: words.join(" "), conditions };
|
|
82
|
+
}
|
|
83
|
+
function textsOf(definition, node) {
|
|
84
|
+
const label = labelOf(definition, node);
|
|
85
|
+
const texts = [{ field: "label", reading: "Name", text: label }];
|
|
86
|
+
for (const field of readableFields(node, definition, { limit: Number.POSITIVE_INFINITY })) {
|
|
87
|
+
if (field.key === "label" || field.value === label)
|
|
88
|
+
continue;
|
|
89
|
+
/*
|
|
90
|
+
* A BOOLEAN IS A STATE, NOT A WORD. It reads as "Yes" or "No", and
|
|
91
|
+
* searching that made "n" — the first letter of anything — find every
|
|
92
|
+
* open task. A state is asked for as a condition: `done:false`.
|
|
93
|
+
*/
|
|
94
|
+
if (typeof node[field.key] === "boolean")
|
|
95
|
+
continue;
|
|
96
|
+
texts.push({ field: field.key, reading: field.label, text: field.value });
|
|
97
|
+
}
|
|
98
|
+
return texts;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* What a kind is searched by, from its declaration alone: the name, and
|
|
102
|
+
* every field a person would read on a record — which is to say every
|
|
103
|
+
* field but the hidden and the ones with no one-line rendering. For
|
|
104
|
+
* `describe` and llms.txt, so an agent knows what the words reach.
|
|
105
|
+
*/
|
|
106
|
+
export function searchableFields(schema, kind) {
|
|
107
|
+
const definition = schema.tryDefinition(kind);
|
|
108
|
+
if (!definition)
|
|
109
|
+
return [];
|
|
110
|
+
const hidden = new Set(definition.display?.hide ?? []);
|
|
111
|
+
const fields = [{ key: "label", reading: "Name" }];
|
|
112
|
+
for (const [key, field] of Object.entries((definition.fields.shape ?? {}))) {
|
|
113
|
+
if (key === "id" || key === "kind" || key === "label" || hidden.has(key))
|
|
114
|
+
continue;
|
|
115
|
+
const type = describeArg(field).type;
|
|
116
|
+
// A state is a condition (`done:false`), not a word to find; see textsOf.
|
|
117
|
+
if (type === "boolean")
|
|
118
|
+
continue;
|
|
119
|
+
if (type === "unknown" && definition.display?.format?.[key] === undefined)
|
|
120
|
+
continue;
|
|
121
|
+
fields.push({ key, reading: definition.display?.labels?.[key] ?? humaniseField(key) });
|
|
122
|
+
}
|
|
123
|
+
return fields;
|
|
124
|
+
}
|
|
125
|
+
const TIER = { exact: 0, prefix: 1, word: 2, part: 3, field: 4 };
|
|
126
|
+
/**
|
|
127
|
+
* Whether a node answers the words, and why.
|
|
128
|
+
*
|
|
129
|
+
* The label is judged first and whole; failing that, every word has to be
|
|
130
|
+
* found somewhere on the node — "van large" finds the large job about the
|
|
131
|
+
* van — and the why names the field that carried a word the label did not.
|
|
132
|
+
*/
|
|
133
|
+
export function matchNode(definition, node, words) {
|
|
134
|
+
const asked = tokensOf(words);
|
|
135
|
+
if (asked.length === 0)
|
|
136
|
+
return undefined;
|
|
137
|
+
const texts = textsOf(definition, node);
|
|
138
|
+
const [label, ...rest] = texts;
|
|
139
|
+
const onLabel = strengthOf(label.text, asked);
|
|
140
|
+
if (onLabel)
|
|
141
|
+
return { field: "label", reading: label.reading, fragment: fragmentOf(label.text, asked), strength: onLabel };
|
|
142
|
+
const everywhere = (word) => (text) => tokensOf(text.text).some((token) => token.startsWith(word));
|
|
143
|
+
if (!asked.every((word) => texts.some(everywhere(word))))
|
|
144
|
+
return undefined;
|
|
145
|
+
const labelTokens = tokensOf(label.text);
|
|
146
|
+
const carried = asked.find((word) => !labelTokens.some((token) => token.startsWith(word))) ?? asked[0];
|
|
147
|
+
const field = rest.find(everywhere(carried));
|
|
148
|
+
return { field: field.field, reading: field.reading, fragment: fragmentOf(field.text, [carried]), strength: "field" };
|
|
149
|
+
}
|
|
150
|
+
/*
|
|
151
|
+
* RECENTLY TOUCHED. What the op log wrote lately, decaying by log distance
|
|
152
|
+
* the way the menu's usage boost does, so a record worked on this morning
|
|
153
|
+
* ranks above its twin untouched since spring — a tiebreak, never a tier.
|
|
154
|
+
*/
|
|
155
|
+
const WINDOW = 512;
|
|
156
|
+
const HALF_LIFE = 64;
|
|
157
|
+
export function touchWeights(ops) {
|
|
158
|
+
const weights = new Map();
|
|
159
|
+
if (ops.length === 0)
|
|
160
|
+
return weights;
|
|
161
|
+
const tail = ops.slice(-WINDOW);
|
|
162
|
+
const last = ops[ops.length - 1].seq;
|
|
163
|
+
for (const op of tail) {
|
|
164
|
+
const weight = Math.pow(0.5, (last - op.seq) / HALF_LIFE);
|
|
165
|
+
for (const id of op.writes)
|
|
166
|
+
weights.set(id, (weights.get(id) ?? 0) + weight);
|
|
167
|
+
}
|
|
168
|
+
return weights;
|
|
169
|
+
}
|
|
170
|
+
const pluralOf = (definition, kind) => definition?.plural ?? `${humaniseField(kind)}s`;
|
|
171
|
+
/** Whether the words name this kind: its id, its singular in words, or its plural. */
|
|
172
|
+
function kindStrength(schema, kind, words) {
|
|
173
|
+
const definition = schema.tryDefinition(kind);
|
|
174
|
+
const candidates = [
|
|
175
|
+
{ field: "plural", text: pluralOf(definition, kind) },
|
|
176
|
+
{ field: "kind", text: humaniseField(kind) },
|
|
177
|
+
...(definition?.noun ? [{ field: "noun", text: definition.noun }] : []),
|
|
178
|
+
];
|
|
179
|
+
let best;
|
|
180
|
+
for (const candidate of candidates) {
|
|
181
|
+
const strength = strengthOf(candidate.text, words);
|
|
182
|
+
if (strength && (!best || TIER[strength] < TIER[best.strength]))
|
|
183
|
+
best = { strength, ...candidate };
|
|
184
|
+
}
|
|
185
|
+
return best;
|
|
186
|
+
}
|
|
187
|
+
/** The ranking, as one comparison: how it matched, the kind the person is in, near, current, touched, flagged, alphabetical, id. */
|
|
188
|
+
const byRank = (a, b) => a.tier - b.tier ||
|
|
189
|
+
a.inKind - b.inKind ||
|
|
190
|
+
a.near - b.near ||
|
|
191
|
+
a.past - b.past ||
|
|
192
|
+
a.touched - b.touched ||
|
|
193
|
+
a.flagged - b.flagged ||
|
|
194
|
+
a.name.localeCompare(b.name, undefined, { sensitivity: "base", numeric: true }) ||
|
|
195
|
+
(a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
196
|
+
/**
|
|
197
|
+
* Finds what the words name, anywhere in the graph, for one seat.
|
|
198
|
+
*
|
|
199
|
+
* Node hits are records whose label or readable fields carry every word;
|
|
200
|
+
* kind, place and rule hits are what the words name in the declaration;
|
|
201
|
+
* act hits are the acts the seat may run on `subject`, the highlighted
|
|
202
|
+
* node hit — never subjectless. A declaration hit leads only when the words
|
|
203
|
+
* name it exactly or as a prefix; otherwise the records lead.
|
|
204
|
+
*
|
|
205
|
+
* Ranking, stable: how the words matched (exact label, label prefix, whole
|
|
206
|
+
* words of the label, parts of it, a field), then near the subject, then
|
|
207
|
+
* current before past, then recently touched, then flagged before clear,
|
|
208
|
+
* then alphabetical, then by id.
|
|
209
|
+
*/
|
|
210
|
+
export function search(store, query, options = {}) {
|
|
211
|
+
const schema = store.schema;
|
|
212
|
+
const parsed = parseQuery(query);
|
|
213
|
+
const words = tokensOf(parsed.words);
|
|
214
|
+
const principal = options.principal ?? { kind: "human" };
|
|
215
|
+
const kept = store.kindsKeptFrom(principal);
|
|
216
|
+
const off = store.modules.disabledKinds;
|
|
217
|
+
const kindWords = parsed.conditions.filter((condition) => condition.key === "kind").map((condition) => condition.value);
|
|
218
|
+
const isWords = parsed.conditions.filter((condition) => condition.key === "is").map((condition) => condition.value);
|
|
219
|
+
const past = isWords.includes("any") || isWords.includes("past");
|
|
220
|
+
const onlyPast = isWords.includes("past") && !isWords.includes("any") && !isWords.includes("current");
|
|
221
|
+
const named = (kind, value) => {
|
|
222
|
+
const asked = squeeze(value);
|
|
223
|
+
const definition = schema.tryDefinition(kind);
|
|
224
|
+
return squeeze(kind) === asked || squeeze(pluralOf(definition, kind)) === asked;
|
|
225
|
+
};
|
|
226
|
+
const kinds = schema.kinds.filter((kind) => !kept.has(kind) &&
|
|
227
|
+
!off.has(kind) &&
|
|
228
|
+
(options.kinds === undefined || options.kinds.includes(kind)) &&
|
|
229
|
+
(kindWords.length === 0 || kindWords.some((value) => named(kind, value))));
|
|
230
|
+
// The rest of the conditions narrow the kinds whose declaration offers them.
|
|
231
|
+
const narrowing = parsed.conditions.filter((condition) => condition.key !== "kind");
|
|
232
|
+
const conditions = narrowing.map((condition) => ({
|
|
233
|
+
...condition,
|
|
234
|
+
admittedBy: kinds.filter((kind) => arrangeable(schema, kind).filters.some((offer) => offer.key === condition.key)),
|
|
235
|
+
}));
|
|
236
|
+
const fieldConditions = conditions.filter((condition) => condition.key !== "is");
|
|
237
|
+
// `is:flagged`, `is:clear` and `is:past` narrow on their own; `is:any` and `is:current` only say which horizon.
|
|
238
|
+
const narrowingIs = isWords.filter((word) => word === "flagged" || word === "clear" || word === "past");
|
|
239
|
+
const searched = { kinds, past };
|
|
240
|
+
const empty = { hits: [], words: parsed.words, conditions, searched, byKind: {}, total: 0, matched: [] };
|
|
241
|
+
if (words.length === 0 && fieldConditions.length === 0 && narrowingIs.length === 0)
|
|
242
|
+
return empty;
|
|
243
|
+
const today = options.today;
|
|
244
|
+
const flagged = options.flagged ?? new Set(store.violations().flatMap((violation) => violation.nodeIds));
|
|
245
|
+
const touched = options.touched ?? touchWeights(store.log.all());
|
|
246
|
+
const near = new Set();
|
|
247
|
+
for (const id of options.from ?? []) {
|
|
248
|
+
near.add(id);
|
|
249
|
+
for (const neighbour of store.graph.neighbors(id))
|
|
250
|
+
near.add(neighbour.id);
|
|
251
|
+
}
|
|
252
|
+
// The same words mean the same thing in a search box and in a list page's filter.
|
|
253
|
+
const ctx = { schema, graph: store.graph, flagged, ...(today ? { today } : {}) };
|
|
254
|
+
const ranked = [];
|
|
255
|
+
const byKind = {};
|
|
256
|
+
for (const kind of kinds) {
|
|
257
|
+
const definition = schema.tryDefinition(kind);
|
|
258
|
+
const own = conditions.filter((condition) => condition.key !== "is" && condition.admittedBy.includes(kind));
|
|
259
|
+
// Conditions alone find only in the kinds that can be narrowed by them.
|
|
260
|
+
if (words.length === 0 && own.length === 0 && narrowingIs.length === 0)
|
|
261
|
+
continue;
|
|
262
|
+
/*
|
|
263
|
+
* A CONDITION THAT NAMES A RETIRED STATE ASKS FOR THE PAST. `status:demo`
|
|
264
|
+
* on a discography whose demos are behind the horizon found nothing and
|
|
265
|
+
* said "add is:any" — to somebody who had just named the past by name.
|
|
266
|
+
*/
|
|
267
|
+
const namesThePast = asksForThePast(definition, own);
|
|
268
|
+
for (const node of store.graph.nodesOfKind(kind)) {
|
|
269
|
+
const current = isCurrent(definition, node, today);
|
|
270
|
+
if (!past && !namesThePast && !current)
|
|
271
|
+
continue;
|
|
272
|
+
if (onlyPast && current)
|
|
273
|
+
continue;
|
|
274
|
+
if (!own.every((condition) => conditionHolds(ctx, node, condition)))
|
|
275
|
+
continue;
|
|
276
|
+
if (isWords.includes("flagged") && !flagged.has(node.id))
|
|
277
|
+
continue;
|
|
278
|
+
if (isWords.includes("clear") && flagged.has(node.id))
|
|
279
|
+
continue;
|
|
280
|
+
const why = words.length > 0
|
|
281
|
+
? matchNode(definition, node, parsed.words)
|
|
282
|
+
: own[0]
|
|
283
|
+
? { field: own[0].key, reading: humaniseField(own[0].key), fragment: `${own[0].key}:${own[0].value}`, strength: "field" }
|
|
284
|
+
: { field: "is", reading: "Is", fragment: `is:${narrowingIs[0]}`, strength: "field" };
|
|
285
|
+
if (!why)
|
|
286
|
+
continue;
|
|
287
|
+
const label = labelOf(definition, node);
|
|
288
|
+
byKind[kind] = (byKind[kind] ?? 0) + 1;
|
|
289
|
+
ranked.push({
|
|
290
|
+
hit: { about: "node", id: node.id, kind, label, why, current, flagged: flagged.has(node.id) },
|
|
291
|
+
tier: TIER[why.strength],
|
|
292
|
+
inKind: kind === options.inKind ? 0 : 1,
|
|
293
|
+
near: near.has(node.id) ? 0 : 1,
|
|
294
|
+
past: current ? 0 : 1,
|
|
295
|
+
touched: -(touched.get(node.id) ?? 0),
|
|
296
|
+
flagged: flagged.has(node.id) ? 0 : 1,
|
|
297
|
+
name: label,
|
|
298
|
+
id: node.id,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
/*
|
|
303
|
+
* THE DECLARATION'S HITS: a kind, a place, a rule. They lead the records
|
|
304
|
+
* only when the words name them exactly or as a prefix — "tasks" is the
|
|
305
|
+
* district, "van" is a record that happens to share a letter with nothing.
|
|
306
|
+
*/
|
|
307
|
+
const lead = (strength) => (strength === "exact" || strength === "prefix" ? -1 : TIER[strength]);
|
|
308
|
+
const declared = (hit, strength, name, id) => ({
|
|
309
|
+
hit,
|
|
310
|
+
tier: lead(strength),
|
|
311
|
+
inKind: 1,
|
|
312
|
+
near: 1,
|
|
313
|
+
past: 0,
|
|
314
|
+
touched: 0,
|
|
315
|
+
flagged: 1,
|
|
316
|
+
name,
|
|
317
|
+
id,
|
|
318
|
+
});
|
|
319
|
+
if (words.length > 0) {
|
|
320
|
+
for (const kind of kinds) {
|
|
321
|
+
const match = kindStrength(schema, kind, words);
|
|
322
|
+
if (!match)
|
|
323
|
+
continue;
|
|
324
|
+
const label = pluralOf(schema.tryDefinition(kind), kind);
|
|
325
|
+
const why = { field: match.field, reading: match.field === "plural" ? "Plural" : "Kind", fragment: match.text, strength: match.strength };
|
|
326
|
+
ranked.push(declared({ about: "kind", kind, label, why, count: byKind[kind] ?? 0 }, match.strength, label, `kind:${kind}`));
|
|
327
|
+
}
|
|
328
|
+
for (const place of options.places ?? []) {
|
|
329
|
+
if (!kinds.includes(place.kind) || (place.across && kept.has(place.across)))
|
|
330
|
+
continue;
|
|
331
|
+
const strength = strengthOf(place.title, words);
|
|
332
|
+
if (!strength)
|
|
333
|
+
continue;
|
|
334
|
+
const why = { field: "title", reading: "Title", fragment: place.title, strength };
|
|
335
|
+
ranked.push(declared({ about: "place", kind: place.kind, title: place.title, as: place.as, why }, strength, place.title, `place:${place.kind}:${place.as}`));
|
|
336
|
+
}
|
|
337
|
+
for (const rule of store.allInvariants()) {
|
|
338
|
+
const scoped = rule.scope === "graph" ? undefined : rule.scope.kind;
|
|
339
|
+
if (scoped && !kinds.includes(scoped))
|
|
340
|
+
continue;
|
|
341
|
+
const label = rule.label ?? humaniseField(rule.name);
|
|
342
|
+
const onLabel = strengthOf(label, words);
|
|
343
|
+
const onName = onLabel ? undefined : strengthOf(humaniseField(rule.name), words);
|
|
344
|
+
const strength = onLabel ?? onName;
|
|
345
|
+
if (!strength)
|
|
346
|
+
continue;
|
|
347
|
+
const why = { field: onLabel ? "label" : "name", reading: onLabel ? "Rule" : "Name", fragment: onLabel ? label : rule.name, strength };
|
|
348
|
+
ranked.push(declared({ about: "rule", name: rule.name, label, ...(scoped ? { kind: scoped } : {}), why }, strength, label, `rule:${rule.name}`));
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
ranked.sort(byRank);
|
|
352
|
+
const limit = options.limit ?? 50;
|
|
353
|
+
const shown = ranked.slice(0, limit).map((entry) => entry.hit);
|
|
354
|
+
/*
|
|
355
|
+
* Two hits of one name say what tells them apart — a single and its
|
|
356
|
+
* album, both "Blue Hour", by the fact that differs; a song and an album,
|
|
357
|
+
* both "Gone Digital", by their nouns. Across kinds too: the strip heads
|
|
358
|
+
* each kind, but a row is read alone — by the pointer that lands on it,
|
|
359
|
+
* by the screen reader that speaks it — and "Gone Digital, Gone Digital"
|
|
360
|
+
* is a choice nobody can make.
|
|
361
|
+
*/
|
|
362
|
+
const alike = shown.flatMap((hit) => (hit.about === "node" ? [store.graph.getNode(hit.id)] : [])).filter(Boolean);
|
|
363
|
+
const apart = tellApart(alike, (named) => schema.tryDefinition(named));
|
|
364
|
+
const hits = shown.map((hit) => (hit.about === "node" && apart.has(hit.id) ? { ...hit, apart: apart.get(hit.id) } : hit));
|
|
365
|
+
const total = Object.values(byKind).reduce((sum, count) => sum + count, 0);
|
|
366
|
+
const matched = ranked.flatMap((entry) => (entry.hit.about === "node" ? [entry.hit.id] : []));
|
|
367
|
+
const acts = options.subject && kinds.includes(String(store.graph.getNode(options.subject)?.kind)) ? actsOn(store, options.subject, parsed.words, { principal }) : [];
|
|
368
|
+
return {
|
|
369
|
+
hits: [...hits, ...acts],
|
|
370
|
+
words: parsed.words,
|
|
371
|
+
conditions,
|
|
372
|
+
searched,
|
|
373
|
+
byKind,
|
|
374
|
+
total,
|
|
375
|
+
matched,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* WHAT CAN BE DONE ABOUT IT: the acts a seat may run on one record, as
|
|
380
|
+
* hits. An act is never a subjectless hit — it is offered on the thing the
|
|
381
|
+
* person highlighted, with its own title — and the words that name the
|
|
382
|
+
* record may name the act too ("van finish"), so each act is judged on the
|
|
383
|
+
* words its own title carries. The named first, the destructive last.
|
|
384
|
+
*
|
|
385
|
+
* Its own function because a Find box asks it for a highlighted row on
|
|
386
|
+
* every keystroke, and has no need to rescan the graph to get it.
|
|
387
|
+
*/
|
|
388
|
+
export function actsOn(store, subjectId, words, options = {}) {
|
|
389
|
+
const subject = store.graph.getNode(subjectId);
|
|
390
|
+
if (!subject)
|
|
391
|
+
return [];
|
|
392
|
+
const principal = options.principal ?? { kind: "human" };
|
|
393
|
+
if (store.kindsKeptFrom(principal).has(subject.kind))
|
|
394
|
+
return [];
|
|
395
|
+
const asked = tokensOf(words);
|
|
396
|
+
const said = labelOf(store.schema.tryDefinition(subject.kind), subject);
|
|
397
|
+
const ranked = [];
|
|
398
|
+
for (const mutation of store.allMutations()) {
|
|
399
|
+
const binding = mutation.subject;
|
|
400
|
+
if (!binding)
|
|
401
|
+
continue;
|
|
402
|
+
if (binding.kinds !== "*" && !binding.kinds.includes(subject.kind))
|
|
403
|
+
continue;
|
|
404
|
+
if (!store.permits({ name: mutation.name, args: { [binding.arg]: subject.id } }, principal).ok)
|
|
405
|
+
continue;
|
|
406
|
+
const title = mutation.title ?? humaniseField(mutation.name);
|
|
407
|
+
const titled = tokensOf(title);
|
|
408
|
+
const own = asked.filter((word) => titled.some((token) => token.startsWith(word)));
|
|
409
|
+
const strength = own.length > 0 ? strengthOf(title, own) : undefined;
|
|
410
|
+
const why = strength
|
|
411
|
+
? { field: "title", reading: "Act", fragment: title, strength }
|
|
412
|
+
: { field: "subject", reading: "On", fragment: said, strength: "field" };
|
|
413
|
+
ranked.push({
|
|
414
|
+
hit: { about: "act", name: mutation.name, title, subject: subject.id, why, destructive: mutation.destructive === true },
|
|
415
|
+
tier: strength ? TIER[strength] : TIER.field + 1,
|
|
416
|
+
inKind: 0,
|
|
417
|
+
near: 0,
|
|
418
|
+
past: mutation.destructive ? 1 : 0,
|
|
419
|
+
touched: 0,
|
|
420
|
+
flagged: mutation.pinned ? 0 : 1,
|
|
421
|
+
name: title,
|
|
422
|
+
id: `act:${mutation.name}`,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
ranked.sort(byRank);
|
|
426
|
+
const acts = ranked.map((entry) => entry.hit);
|
|
427
|
+
return options.limit === undefined ? acts : acts.slice(0, options.limit);
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* The sentence for what was searched, for an empty state or a strip's
|
|
431
|
+
* footer: "Tasks and lists, current ones; add is:any for past ones."
|
|
432
|
+
*/
|
|
433
|
+
export function describeSearched(schema, searched) {
|
|
434
|
+
const plurals = searched.kinds.map((kind, index) => {
|
|
435
|
+
const plural = pluralOf(schema.tryDefinition(kind), kind);
|
|
436
|
+
return index === 0 ? plural : plural.toLowerCase();
|
|
437
|
+
});
|
|
438
|
+
const names = plurals.length === 0
|
|
439
|
+
? "Nothing"
|
|
440
|
+
: plurals.length === 1
|
|
441
|
+
? plurals[0]
|
|
442
|
+
: `${plurals.slice(0, -1).join(", ")} and ${plurals[plurals.length - 1]}`;
|
|
443
|
+
const lifecycled = searched.kinds.some((kind) => schema.tryDefinition(kind)?.lifecycle);
|
|
444
|
+
if (!lifecycled)
|
|
445
|
+
return `${capitalFirst(names)}.`;
|
|
446
|
+
return searched.past ? `${capitalFirst(names)}, past ones included.` : `${capitalFirst(names)}, current ones; add is:any for past ones.`;
|
|
447
|
+
}
|
|
448
|
+
const capitalFirst = (text) => text.charAt(0).toUpperCase() + text.slice(1);
|
|
449
|
+
//# sourceMappingURL=search.js.map
|