@graview/tools 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 +47 -0
- package/dist/agent/adapters.d.ts +43 -0
- package/dist/agent/adapters.d.ts.map +1 -0
- package/dist/agent/adapters.js +52 -0
- package/dist/agent/adapters.js.map +1 -0
- package/dist/agent/tools.d.ts +105 -0
- package/dist/agent/tools.d.ts.map +1 -0
- package/dist/agent/tools.js +311 -0
- package/dist/agent/tools.js.map +1 -0
- package/dist/cli.d.ts +37 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +306 -0
- package/dist/cli.js.map +1 -0
- package/dist/conversation.d.ts +108 -0
- package/dist/conversation.d.ts.map +1 -0
- package/dist/conversation.js +698 -0
- package/dist/conversation.js.map +1 -0
- package/dist/decide.d.ts +43 -0
- package/dist/decide.d.ts.map +1 -0
- package/dist/decide.js +125 -0
- package/dist/decide.js.map +1 -0
- package/dist/derive.d.ts +84 -0
- package/dist/derive.d.ts.map +1 -0
- package/dist/derive.js +302 -0
- package/dist/derive.js.map +1 -0
- package/dist/edit.d.ts +70 -0
- package/dist/edit.d.ts.map +1 -0
- package/dist/edit.js +97 -0
- package/dist/edit.js.map +1 -0
- package/dist/figure.d.ts +53 -0
- package/dist/figure.d.ts.map +1 -0
- package/dist/figure.js +86 -0
- package/dist/figure.js.map +1 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/intelligence.d.ts +132 -0
- package/dist/intelligence.d.ts.map +1 -0
- package/dist/intelligence.js +463 -0
- package/dist/intelligence.js.map +1 -0
- package/dist/local.d.ts +192 -0
- package/dist/local.d.ts.map +1 -0
- package/dist/local.js +365 -0
- package/dist/local.js.map +1 -0
- package/dist/loop.d.ts +84 -0
- package/dist/loop.d.ts.map +1 -0
- package/dist/loop.js +173 -0
- package/dist/loop.js.map +1 -0
- package/dist/mcp-stdio.d.ts +38 -0
- package/dist/mcp-stdio.d.ts.map +1 -0
- package/dist/mcp-stdio.js +83 -0
- package/dist/mcp-stdio.js.map +1 -0
- package/dist/pins.d.ts +29 -0
- package/dist/pins.d.ts.map +1 -0
- package/dist/pins.js +60 -0
- package/dist/pins.js.map +1 -0
- package/dist/plan.d.ts +150 -0
- package/dist/plan.d.ts.map +1 -0
- package/dist/plan.js +303 -0
- package/dist/plan.js.map +1 -0
- package/dist/providers/insight.d.ts +14 -0
- package/dist/providers/insight.d.ts.map +1 -0
- package/dist/providers/insight.js +81 -0
- package/dist/providers/insight.js.map +1 -0
- package/dist/providers/invariant.d.ts +14 -0
- package/dist/providers/invariant.d.ts.map +1 -0
- package/dist/providers/invariant.js +123 -0
- package/dist/providers/invariant.js.map +1 -0
- package/dist/providers/jev.d.ts +115 -0
- package/dist/providers/jev.d.ts.map +1 -0
- package/dist/providers/jev.js +148 -0
- package/dist/providers/jev.js.map +1 -0
- package/dist/providers/lens.d.ts +24 -0
- package/dist/providers/lens.d.ts.map +1 -0
- package/dist/providers/lens.js +36 -0
- package/dist/providers/lens.js.map +1 -0
- package/dist/providers/llm.d.ts +25 -0
- package/dist/providers/llm.d.ts.map +1 -0
- package/dist/providers/llm.js +44 -0
- package/dist/providers/llm.js.map +1 -0
- package/dist/providers/schema.d.ts +12 -0
- package/dist/providers/schema.d.ts.map +1 -0
- package/dist/providers/schema.js +508 -0
- package/dist/providers/schema.js.map +1 -0
- package/dist/providers/structure.d.ts +14 -0
- package/dist/providers/structure.d.ts.map +1 -0
- package/dist/providers/structure.js +211 -0
- package/dist/providers/structure.js.map +1 -0
- package/dist/questions.d.ts +162 -0
- package/dist/questions.d.ts.map +1 -0
- package/dist/questions.js +316 -0
- package/dist/questions.js.map +1 -0
- package/dist/run.d.ts +202 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +453 -0
- package/dist/run.js.map +1 -0
- package/dist/space.d.ts +50 -0
- package/dist/space.d.ts.map +1 -0
- package/dist/space.js +55 -0
- package/dist/space.js.map +1 -0
- package/dist/types.d.ts +122 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/usage.d.ts +10 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +59 -0
- package/dist/usage.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
import { formFields, humaniseField, nounOf, tellApart, labelOf, readableFields, search, violationsTouching, withArticle, } from "@graview/core";
|
|
2
|
+
import { droppedProposals, firstJsonObject, resolveProposal, validateProposals, } from "./intelligence.js";
|
|
3
|
+
const sentence = (parts) => parts.filter(Boolean).join(" ");
|
|
4
|
+
/** The words around a bare name that still only ask about it: "tell me about the School run". */
|
|
5
|
+
const ASKING_WORDS = new Set(["tell", "me", "about", "what", "how", "show", "describe", "the", "a", "an", "and", "please"]);
|
|
6
|
+
/*
|
|
7
|
+
* The words around what is being looked for: "where is the van", "anything
|
|
8
|
+
* about moving". Dropped before the search, so the matcher is handed the
|
|
9
|
+
* thing and not the asking.
|
|
10
|
+
*/
|
|
11
|
+
const LOOKING_WORDS = new Set([
|
|
12
|
+
...ASKING_WORDS,
|
|
13
|
+
"where", "is", "are", "was", "find", "any", "anything", "something", "things", "called", "named",
|
|
14
|
+
"which", "who", "of", "for", "with", "to", "in", "on", "i", "my", "do", "does", "have", "has",
|
|
15
|
+
"can", "you", "it", "that", "this", "there", "we", "our", "got", "be",
|
|
16
|
+
]);
|
|
17
|
+
/** "a person", "a person and a date" — a list a person would say out loud. */
|
|
18
|
+
const withList = (words) => words.length <= 1
|
|
19
|
+
? withArticle(words[0] ?? "something")
|
|
20
|
+
: `${words.slice(0, -1).map(withArticle).join(", ")} and ${withArticle(words[words.length - 1])}`;
|
|
21
|
+
/**
|
|
22
|
+
* The graph answers for itself. Deterministic, keyless, derived:
|
|
23
|
+
* - "what's wrong / broken / problems" → the standing, with ready repairs;
|
|
24
|
+
* - a node named in the message (or selected) → its facts, its trouble,
|
|
25
|
+
* and that trouble's repairs;
|
|
26
|
+
* - a mutation's own title phrased in the message → that call, proposed
|
|
27
|
+
* with what can honestly be filled and nothing guessed;
|
|
28
|
+
* - anything else → the shape of the graph and how to ask.
|
|
29
|
+
*/
|
|
30
|
+
export function graphResponder(options = {}) {
|
|
31
|
+
return async (store, text, context = {}) => {
|
|
32
|
+
const asked = text.toLowerCase();
|
|
33
|
+
const name = (node) => labelOf(store.schema.tryDefinition(node.kind), node);
|
|
34
|
+
/*
|
|
35
|
+
* REFERENTS: the selection first — "this" means what is selected — then
|
|
36
|
+
* any node whose label appears in the message, longest label first so
|
|
37
|
+
* "Morning school run" wins over "run".
|
|
38
|
+
*/
|
|
39
|
+
const referents = [];
|
|
40
|
+
/*
|
|
41
|
+
* Matching is TOKEN-ALIGNED and punctuation-blind, or names fail for
|
|
42
|
+
* the dumbest reasons: a child stored as "child2" must be found by
|
|
43
|
+
* "child 2", and "child2's nap" by "child 2 nap". Both sides tokenize
|
|
44
|
+
* on non-alphanumerics; a label matches when its squeezed form equals
|
|
45
|
+
* some run of adjacent message tokens joined — token alignment is what
|
|
46
|
+
* keeps "Bo" out of "elbow".
|
|
47
|
+
*/
|
|
48
|
+
const tokens = asked.split(/[^a-z0-9]+/).filter(Boolean);
|
|
49
|
+
const runs = new Set();
|
|
50
|
+
for (let start = 0; start < tokens.length; start++) {
|
|
51
|
+
let joined = "";
|
|
52
|
+
for (let end = start; end < Math.min(tokens.length, start + 6); end++) {
|
|
53
|
+
joined += tokens[end];
|
|
54
|
+
runs.add(joined);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const squeeze = (text) => text.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
58
|
+
const byLabel = [...store.graph.allNodes()]
|
|
59
|
+
.map((node) => ({ node, label: squeeze(name(node)) }))
|
|
60
|
+
.filter(({ label }) => label.length >= 2 && runs.has(label))
|
|
61
|
+
.sort((a, b) => b.label.length - a.label.length);
|
|
62
|
+
/*
|
|
63
|
+
* A NAME SEVERAL THINGS SHARE NAMES NONE OF THEM. "Tell me about the
|
|
64
|
+
* 2026 Tesla Model Y Performance" on a lot with three was answered
|
|
65
|
+
* with the first one found and said nothing about the other two; the
|
|
66
|
+
* words find them all below, each told apart.
|
|
67
|
+
*/
|
|
68
|
+
const shared = new Map();
|
|
69
|
+
for (const { label } of byLabel)
|
|
70
|
+
shared.set(label, (shared.get(label) ?? 0) + 1);
|
|
71
|
+
for (const { node, label } of byLabel) {
|
|
72
|
+
if ((shared.get(label) ?? 0) > 1)
|
|
73
|
+
continue;
|
|
74
|
+
if (!referents.some((held) => held.id === node.id))
|
|
75
|
+
referents.push(node);
|
|
76
|
+
}
|
|
77
|
+
/*
|
|
78
|
+
* The SELECTION comes after anything the message NAMED. "This" means
|
|
79
|
+
* what is selected — but a question that says "left back" out loud is
|
|
80
|
+
* about left back, and a standing selection answering it instead was
|
|
81
|
+
* the seat confidently describing the wrong thing.
|
|
82
|
+
*/
|
|
83
|
+
for (const id of context.selection ?? []) {
|
|
84
|
+
const node = store.graph.getNode(id);
|
|
85
|
+
if (node && !referents.some((held) => held.id === node.id))
|
|
86
|
+
referents.push(node);
|
|
87
|
+
}
|
|
88
|
+
const violations = store.violations();
|
|
89
|
+
const readyRepairs = (subset = violations) => subset.flatMap((violation) => {
|
|
90
|
+
const repair = violation.repairs.find((candidate) => !candidate.missing?.length);
|
|
91
|
+
return repair
|
|
92
|
+
? [{ mutation: repair.mutation, args: { ...repair.args }, why: violation.message }]
|
|
93
|
+
: [];
|
|
94
|
+
});
|
|
95
|
+
// ------------------------------------------------------- the standing
|
|
96
|
+
if (/\b(wrong|broken|problem|violat|standing)\b/.test(asked)) {
|
|
97
|
+
if (violations.length === 0) {
|
|
98
|
+
return { say: "Nothing is broken — every declared rule holds.", proposals: [], grounded: true };
|
|
99
|
+
}
|
|
100
|
+
/*
|
|
101
|
+
* THE SENTENCE MATCHES WHAT IS ACTUALLY BELOW IT.
|
|
102
|
+
*
|
|
103
|
+
* "The repairs below come from the rules themselves" was said
|
|
104
|
+
* unconditionally, and `readyRepairs` drops every repair that still
|
|
105
|
+
* needs an argument chosen — so a rule whose repair asks for one thing
|
|
106
|
+
* ("hand it to someone": which someone is the decision the rule cannot
|
|
107
|
+
* make) produced a seat promising repairs under an empty list. Naming
|
|
108
|
+
* what the repair still wants is the honest answer, and it is the same
|
|
109
|
+
* answer the phrased-mutation branch below already gives.
|
|
110
|
+
*/
|
|
111
|
+
const offered = validateProposals(store, readyRepairs().slice(0, 4));
|
|
112
|
+
/*
|
|
113
|
+
* And what it wants is said the way a picker is named — by the KIND it
|
|
114
|
+
* would pick, not by the argument's identifier. Otherwise the seat
|
|
115
|
+
* asks for "a handler" where the strip and the routed face both say
|
|
116
|
+
* "a person".
|
|
117
|
+
*/
|
|
118
|
+
const asked = (mutation, field) => {
|
|
119
|
+
const declared = store.allMutations().find((candidate) => candidate.name === mutation);
|
|
120
|
+
const spec = declared
|
|
121
|
+
? formFields(declared.input).find((candidate) => candidate.name === field)
|
|
122
|
+
: undefined;
|
|
123
|
+
return spec?.control === "node" && !spec.kinds.includes("*")
|
|
124
|
+
? spec.kinds.map((kind) => nounOf(store.schema.tryDefinition(kind), kind)).join(" or ")
|
|
125
|
+
: humaniseField(field).toLowerCase();
|
|
126
|
+
};
|
|
127
|
+
const wants = [
|
|
128
|
+
...new Set(violations
|
|
129
|
+
.flatMap((violation) => violation.repairs)
|
|
130
|
+
.flatMap((repair) => (repair.missing ?? []).map((field) => asked(repair.mutation, field)))),
|
|
131
|
+
];
|
|
132
|
+
return {
|
|
133
|
+
say: sentence([
|
|
134
|
+
`${violations.length} ${violations.length === 1 ? "problem" : "problems"}:`,
|
|
135
|
+
violations
|
|
136
|
+
.slice(0, 4)
|
|
137
|
+
.map((violation) => violation.message)
|
|
138
|
+
.join("; ") + (violations.length > 4 ? "…" : "."),
|
|
139
|
+
offered.length > 0
|
|
140
|
+
? "The repairs below come from the rules themselves."
|
|
141
|
+
: wants.length > 0
|
|
142
|
+
? `The rules name a way to fix ${violations.length === 1 ? "it" : "these"}, but it needs ${withList(wants)} chosen — select the record and its own actions will ask.`
|
|
143
|
+
: "No rule here names a way to fix it.",
|
|
144
|
+
]),
|
|
145
|
+
proposals: offered,
|
|
146
|
+
grounded: true,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
// -------------------------------------------- a mutation, said in words
|
|
150
|
+
/*
|
|
151
|
+
* A QUESTION IS NEVER A CHANGE. "what depends on Pay the deposit?"
|
|
152
|
+
* carries the title of an act ("Depends on") and the name of a record,
|
|
153
|
+
* and was answered with a proposal to RUN that act on the record — an
|
|
154
|
+
* apply button under a question, which is the graph answering wrongly
|
|
155
|
+
* rather than not at all. A sentence that asks is answered from the
|
|
156
|
+
* graph below; only a sentence that says does anything.
|
|
157
|
+
*/
|
|
158
|
+
const question = /\?\s*$/.test(text) ||
|
|
159
|
+
/^\s*(what|who|whom|whose|which|when|where|why|how|is|are|was|were|does|do|did|can|could|should|would|will|has|have)\b/.test(asked);
|
|
160
|
+
const phrased = question
|
|
161
|
+
? undefined
|
|
162
|
+
: store.allMutations().find((mutation) => {
|
|
163
|
+
const title = (mutation.title ?? mutation.name).toLowerCase();
|
|
164
|
+
return title.length > 3 && asked.includes(title);
|
|
165
|
+
});
|
|
166
|
+
if (phrased) {
|
|
167
|
+
const args = {};
|
|
168
|
+
const missing = [];
|
|
169
|
+
const quoted = text.match(/"([^"]+)"/)?.[1];
|
|
170
|
+
// Each thing named fills ONE blank: the same record in both ends of a
|
|
171
|
+
// tie is an act that cannot act, and never what was said.
|
|
172
|
+
const unused = [...referents];
|
|
173
|
+
for (const field of formFields(phrased.input)) {
|
|
174
|
+
const value = answerFrom(field, unused, quoted, options.today, asked);
|
|
175
|
+
if (value !== undefined) {
|
|
176
|
+
args[field.name] = value;
|
|
177
|
+
const at = unused.findIndex((node) => node.id === value);
|
|
178
|
+
if (at >= 0)
|
|
179
|
+
unused.splice(at, 1);
|
|
180
|
+
}
|
|
181
|
+
else if (!field.optional)
|
|
182
|
+
missing.push(field.name);
|
|
183
|
+
}
|
|
184
|
+
/*
|
|
185
|
+
* A READING, NOT A FACT. This branch matched an act's title in a
|
|
186
|
+
* sentence and filled what it honestly could — right often enough to
|
|
187
|
+
* be the floor, and not so right that a model should be kept out of
|
|
188
|
+
* it. Marked grounded, it stopped the ladder dead: a person with a
|
|
189
|
+
* model chosen still got the pattern-matcher's single act out of a
|
|
190
|
+
* sentence that described three.
|
|
191
|
+
*/
|
|
192
|
+
if (missing.length === 0) {
|
|
193
|
+
return {
|
|
194
|
+
say: `I can do that. Review it below.`,
|
|
195
|
+
proposals: validateProposals(store, [
|
|
196
|
+
{ mutation: phrased.name, args, why: `you asked in words` },
|
|
197
|
+
]),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
say: `"${phrased.title ?? phrased.name}" needs ${missing.join(", ")} — name the ${missing.length === 1 ? "thing" : "things"} (or select ${missing.length === 1 ? "it" : "them"}) and ask again.`,
|
|
202
|
+
proposals: [],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/*
|
|
206
|
+
* ------------------------------------------- when / who, from the graph
|
|
207
|
+
*
|
|
208
|
+
* The declarations already answer these. WHEN: field roles name which
|
|
209
|
+
* fields are a thing's start, end and day, and display.format says how
|
|
210
|
+
* to speak them. WHO: an edge whose own description says "who…" IS the
|
|
211
|
+
* who-relation — "who does the run", "who is there" — so following it
|
|
212
|
+
* from the right node answers without knowing what a run is. The right
|
|
213
|
+
* node is chosen honestly: the referent itself when it is timed, else
|
|
214
|
+
* the referent's neighbour that best matches the question's remaining
|
|
215
|
+
* words and any day named.
|
|
216
|
+
*/
|
|
217
|
+
const DAY_WORDS = {
|
|
218
|
+
mon: "mon", monday: "mon", tue: "tue", tues: "tue", tuesday: "tue",
|
|
219
|
+
wed: "wed", wednesday: "wed", thu: "thu", thur: "thu", thurs: "thu", thursday: "thu",
|
|
220
|
+
fri: "fri", friday: "fri", sat: "sat", saturday: "sat", sun: "sun", sunday: "sun",
|
|
221
|
+
};
|
|
222
|
+
const askedDay = tokens.map((token) => DAY_WORDS[token]).find(Boolean);
|
|
223
|
+
const formatOf = (kind, field, value) => {
|
|
224
|
+
const format = store.schema.tryDefinition(kind)?.display?.format?.[field];
|
|
225
|
+
return format ? format(value) : String(value);
|
|
226
|
+
};
|
|
227
|
+
const timing = (node) => {
|
|
228
|
+
const roles = store.schema.tryDefinition(node.kind)?.fieldRoles;
|
|
229
|
+
if (!roles?.["start"])
|
|
230
|
+
return null;
|
|
231
|
+
const start = node[roles["start"]];
|
|
232
|
+
const end = roles["end"] ? node[roles["end"]] : undefined;
|
|
233
|
+
const day = roles["day"] ? node[roles["day"]] : roles["days"] ? node[roles["days"]] : undefined;
|
|
234
|
+
if (start === undefined && end === undefined)
|
|
235
|
+
return null;
|
|
236
|
+
const said = [
|
|
237
|
+
start !== undefined ? formatOf(node.kind, roles["start"], start) : null,
|
|
238
|
+
end !== undefined && roles["end"] ? `–${formatOf(node.kind, roles["end"], end)}` : null,
|
|
239
|
+
Array.isArray(day) ? ` on ${day.join(", ")}` : day !== undefined ? ` on ${String(day)}` : null,
|
|
240
|
+
]
|
|
241
|
+
.filter(Boolean)
|
|
242
|
+
.join("");
|
|
243
|
+
return said || null;
|
|
244
|
+
};
|
|
245
|
+
const neighboursOf = (id) => {
|
|
246
|
+
const out = [];
|
|
247
|
+
for (const edge of store.graph.allEdges()) {
|
|
248
|
+
const other = edge.from === id ? edge.to : edge.to === id ? edge.from : null;
|
|
249
|
+
if (!other)
|
|
250
|
+
continue;
|
|
251
|
+
const found = store.graph.getNode(other);
|
|
252
|
+
if (found && !out.some((held) => held.id === found.id))
|
|
253
|
+
out.push(found);
|
|
254
|
+
}
|
|
255
|
+
return out;
|
|
256
|
+
};
|
|
257
|
+
/**
|
|
258
|
+
* The question's subjects, best first: the referent's neighbours ranked
|
|
259
|
+
* by how many of the question's words their name shares and — when a
|
|
260
|
+
* day is named — how exactly they sit on that day. An exact day FIELD
|
|
261
|
+
* outranks a days array that merely contains it: "tuesday" means the
|
|
262
|
+
* Tuesday one, not everything that also happens on Tuesdays.
|
|
263
|
+
*/
|
|
264
|
+
const subjectsFor = (wantTimed) => {
|
|
265
|
+
const first = referents[0];
|
|
266
|
+
if (!first)
|
|
267
|
+
return [];
|
|
268
|
+
const scored = [];
|
|
269
|
+
for (const candidate of neighboursOf(first.id)) {
|
|
270
|
+
let score = 0;
|
|
271
|
+
const squeezed = squeeze(name(candidate));
|
|
272
|
+
for (const token of tokens) {
|
|
273
|
+
if (token.length > 2 && squeezed.includes(token))
|
|
274
|
+
score += 1;
|
|
275
|
+
}
|
|
276
|
+
if (askedDay) {
|
|
277
|
+
const roles = store.schema.tryDefinition(candidate.kind)?.fieldRoles;
|
|
278
|
+
const day = roles?.["day"] ? candidate[roles["day"]] : undefined;
|
|
279
|
+
const days = roles?.["days"] ? candidate[roles["days"]] : undefined;
|
|
280
|
+
if (day === askedDay)
|
|
281
|
+
score += 4;
|
|
282
|
+
else if (Array.isArray(days) && days.includes(askedDay))
|
|
283
|
+
score += 1;
|
|
284
|
+
}
|
|
285
|
+
if (wantTimed && !timing(candidate))
|
|
286
|
+
continue;
|
|
287
|
+
if (score > 0)
|
|
288
|
+
scored.push({ node: candidate, score });
|
|
289
|
+
}
|
|
290
|
+
const ranked = scored.sort((a, b) => b.score - a.score).map((held) => held.node);
|
|
291
|
+
if (wantTimed && timing(first))
|
|
292
|
+
ranked.unshift(first);
|
|
293
|
+
else if (!wantTimed)
|
|
294
|
+
ranked.push(first);
|
|
295
|
+
return ranked;
|
|
296
|
+
};
|
|
297
|
+
if (referents.length > 0 && /\bwhen\b/.test(asked)) {
|
|
298
|
+
const subject = subjectsFor(true)[0] ?? null;
|
|
299
|
+
const said = subject ? timing(subject) : null;
|
|
300
|
+
if (subject && said) {
|
|
301
|
+
return {
|
|
302
|
+
say: `${name(subject)} runs ${said}.`,
|
|
303
|
+
proposals: validateProposals(store, readyRepairs(violationsTouching(violations, [subject.id])).slice(0, 3)),
|
|
304
|
+
grounded: true,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (referents.length > 0 && /\bwho(m|se)?\b/.test(asked)) {
|
|
309
|
+
/*
|
|
310
|
+
* A subject that cannot answer "who" is the wrong subject: walk the
|
|
311
|
+
* ranked candidates until one actually has who-edges. The edge's own
|
|
312
|
+
* description is the sentence — and its declared `inverse` ("who is
|
|
313
|
+
* there") counts as a who-sentence read from the other end.
|
|
314
|
+
*/
|
|
315
|
+
for (const subject of subjectsFor(false)) {
|
|
316
|
+
const parts = [];
|
|
317
|
+
const said = new Set();
|
|
318
|
+
for (const definition of store.schema.definitions) {
|
|
319
|
+
for (const [edgeKind, spec] of Object.entries(definition.edges)) {
|
|
320
|
+
/*
|
|
321
|
+
* Whoever declared the edge, the who-things are the nodes on
|
|
322
|
+
* the OTHER side of the subject — in-neighbours when the edge
|
|
323
|
+
* points at the subject, out-neighbours when it points away —
|
|
324
|
+
* and the sentence is whichever declared line says "who".
|
|
325
|
+
*/
|
|
326
|
+
const forward = /\bwho\b/i.test(spec.description ?? "");
|
|
327
|
+
const backward = /\bwho\b/i.test(spec.inverse ?? "");
|
|
328
|
+
if ((!forward && !backward) || said.has(edgeKind))
|
|
329
|
+
continue;
|
|
330
|
+
said.add(edgeKind);
|
|
331
|
+
const others = [
|
|
332
|
+
...store.graph.in(subject.id, edgeKind),
|
|
333
|
+
...store.graph.out(subject.id, edgeKind),
|
|
334
|
+
];
|
|
335
|
+
if (others.length === 0)
|
|
336
|
+
continue;
|
|
337
|
+
const sentence = forward ? spec.description : spec.inverse;
|
|
338
|
+
parts.push(`${sentence}: ${others.map((other) => name(other)).join(", ")}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (parts.length > 0) {
|
|
342
|
+
return {
|
|
343
|
+
say: `${name(subject)} — ${parts.join("; ")}.`,
|
|
344
|
+
proposals: validateProposals(store, readyRepairs(violationsTouching(violations, [subject.id])).slice(0, 3)),
|
|
345
|
+
grounded: true,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// ------------------------------------------------------ a named thing
|
|
351
|
+
if (referents.length > 0) {
|
|
352
|
+
const node = referents[0];
|
|
353
|
+
/*
|
|
354
|
+
* NAMING A THING IS NOT ASKING ABOUT IT. "Erin tends that plot" names
|
|
355
|
+
* Erin and says a change; answered as a fact — "Erin — a gardener.
|
|
356
|
+
* Connected to nothing yet." — and marked grounded, it kept the model
|
|
357
|
+
* from ever reading it, and the seat described the gardener it had
|
|
358
|
+
* just been told to connect. The description is still the floor's
|
|
359
|
+
* best answer, but only a question, or the bare name, is a FACT.
|
|
360
|
+
*/
|
|
361
|
+
const own = new Set(name(node).toLowerCase().split(/[^a-z0-9]+/).filter(Boolean));
|
|
362
|
+
const bare = tokens.every((token) => own.has(token) || ASKING_WORDS.has(token));
|
|
363
|
+
const asking = question || bare;
|
|
364
|
+
const definition = store.schema.tryDefinition(node.kind);
|
|
365
|
+
/*
|
|
366
|
+
* Each fact as it reads on its own, its LABEL lower-cased to sit in
|
|
367
|
+
* brackets — "explicit: no", "year 2026" — and never its value: "tesla"
|
|
368
|
+
* is a make somebody spelt with a capital. A bare value says what it
|
|
369
|
+
* is, "make Tesla", "phone (555) 298-1878", or a VIN reads as noise.
|
|
370
|
+
*/
|
|
371
|
+
const quietly = (label) => (label === label.toUpperCase() ? label : label.charAt(0).toLowerCase() + label.slice(1));
|
|
372
|
+
const facts = readableFields(node, definition, { limit: 3 })
|
|
373
|
+
.map((field) => field.alone.startsWith(field.label) ? quietly(field.label) + field.alone.slice(field.label.length) : `${quietly(field.label)} ${field.alone}`)
|
|
374
|
+
.join(", ");
|
|
375
|
+
const touching = violationsTouching(violations, [node.id]);
|
|
376
|
+
/*
|
|
377
|
+
* The relations, in the declarations' own words — "where they can
|
|
378
|
+
* play: Goalkeeper, Left back" answers "what about Bo" the way a
|
|
379
|
+
* person would, instead of a degree count.
|
|
380
|
+
*/
|
|
381
|
+
const groups = new Map();
|
|
382
|
+
for (const edge of [...store.graph.allEdges()]) {
|
|
383
|
+
const direction = edge.from === node.id ? "out" : edge.to === node.id ? "in" : null;
|
|
384
|
+
if (!direction)
|
|
385
|
+
continue;
|
|
386
|
+
const other = store.graph.getNode(direction === "out" ? edge.to : edge.from);
|
|
387
|
+
if (!other)
|
|
388
|
+
continue;
|
|
389
|
+
const key = `${edge.kind}|${direction}`;
|
|
390
|
+
if (!groups.has(key)) {
|
|
391
|
+
/*
|
|
392
|
+
* Read from the end you are standing on. An edge is declared on
|
|
393
|
+
* the kind at its `from` end, so its `description` is the reading
|
|
394
|
+
* from there and its `inverse` the reading from the `to` end —
|
|
395
|
+
* whichever kind this node is. "What is Ada seeing to?" was
|
|
396
|
+
* answered "who is seeing to it: Pay the deposit", the item's
|
|
397
|
+
* words in the person's mouth, because the caption was chosen by
|
|
398
|
+
* which KIND declared the edge rather than by which END the node
|
|
399
|
+
* is at.
|
|
400
|
+
*/
|
|
401
|
+
let said;
|
|
402
|
+
for (const definition of store.schema.definitions) {
|
|
403
|
+
const spec = definition.edges[edge.kind];
|
|
404
|
+
if (!spec)
|
|
405
|
+
continue;
|
|
406
|
+
said = direction === "out" ? spec.description : (spec.inverse ?? spec.description);
|
|
407
|
+
if (said)
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
groups.set(key, { sentence: said ?? edge.kind.replace(/-/g, " "), names: [] });
|
|
411
|
+
}
|
|
412
|
+
const group = groups.get(key);
|
|
413
|
+
if (group.names.length < 6)
|
|
414
|
+
group.names.push(name(other));
|
|
415
|
+
}
|
|
416
|
+
const related = [...groups.values()]
|
|
417
|
+
.slice(0, 4)
|
|
418
|
+
// Each is a sentence of its own, so it starts like one: "The releases it is on: Blue Hour."
|
|
419
|
+
.map((group) => `${group.sentence.charAt(0).toUpperCase()}${group.sentence.slice(1)}: ${group.names.join(", ")}`)
|
|
420
|
+
.join(". ");
|
|
421
|
+
return {
|
|
422
|
+
say: sentence([
|
|
423
|
+
`${name(node)} — ${withArticle(nounOf(store.schema.tryDefinition(node.kind), node.kind))}${facts ? ` (${facts})` : ""}.`,
|
|
424
|
+
related ? `${related}.` : "Connected to nothing yet.",
|
|
425
|
+
touching.length > 0
|
|
426
|
+
? `Trouble: ${touching.map((violation) => violation.message).join("; ")}.`
|
|
427
|
+
: "Nothing about it is broken.",
|
|
428
|
+
]),
|
|
429
|
+
proposals: validateProposals(store, readyRepairs(touching).slice(0, 3)),
|
|
430
|
+
...(asking ? { grounded: true } : {}),
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
// --------------------------------------------------- what the words find
|
|
434
|
+
/*
|
|
435
|
+
* NO ACT AND NO FACT, BUT THE WORDS FIND THINGS. "Where is the van" names
|
|
436
|
+
* nothing by its whole name and asks for no change — and the Find box
|
|
437
|
+
* would have answered it at once. The same matcher, the same seat's
|
|
438
|
+
* view: the reply is the strip in prose, each thing a press. Not a
|
|
439
|
+
* grounded answer — a model may yet read the sentence better.
|
|
440
|
+
*/
|
|
441
|
+
{
|
|
442
|
+
const words = asked.split(/[^\p{L}\p{N}]+/u).filter((word) => word.length > 0 && !LOOKING_WORDS.has(word));
|
|
443
|
+
if (words.length > 0) {
|
|
444
|
+
const found = search(store, words.join(" "), {
|
|
445
|
+
...(context.principal ? { principal: context.principal } : {}),
|
|
446
|
+
...(context.selection ? { from: context.selection } : {}),
|
|
447
|
+
...(options.today ? { today: options.today } : {}),
|
|
448
|
+
limit: 6,
|
|
449
|
+
});
|
|
450
|
+
const picks = found.hits.filter((hit) => hit.about === "node");
|
|
451
|
+
if (picks.length > 0) {
|
|
452
|
+
// Two of one name are told apart: "2025 Subaru Outback Base · VIN 3VP1…" (the W-095 rule, in prose).
|
|
453
|
+
const apart = tellApart(picks.flatMap((hit) => {
|
|
454
|
+
const node = store.graph.getNode(hit.id);
|
|
455
|
+
return node ? [node] : [];
|
|
456
|
+
}), (kind) => store.schema.tryDefinition(kind));
|
|
457
|
+
const said = picks.map((hit) => `${hit.label}${apart.has(hit.id) ? ` · ${apart.get(hit.id)}` : ""} (${nounOf(store.schema.tryDefinition(hit.kind), hit.kind)}${hit.why.field === "label" ? "" : `, ${hit.why.reading.toLowerCase()}: ${hit.why.fragment}`})`);
|
|
458
|
+
const how = found.total === 1 ? "One thing is" : `${numberWord(found.total)} things are`;
|
|
459
|
+
return {
|
|
460
|
+
say: `${how} called “${found.words}” — ${said.join("; ")}${found.total > picks.length ? `; and ${found.total - picks.length} more` : ""}.`,
|
|
461
|
+
proposals: [],
|
|
462
|
+
picks,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
// ------------------------------------------------------------ the shape
|
|
468
|
+
const counts = store.schema.kinds
|
|
469
|
+
.filter((kind) => !store.modules.disabledKinds.has(kind))
|
|
470
|
+
.map((kind) => {
|
|
471
|
+
const plural = store.schema.tryDefinition(kind)?.plural ?? `${kind}s`;
|
|
472
|
+
return `${store.graph.nodesOfKind(kind).length} ${plural}`;
|
|
473
|
+
})
|
|
474
|
+
.join(", ");
|
|
475
|
+
return {
|
|
476
|
+
say: sentence([
|
|
477
|
+
`This graph holds ${counts}.`,
|
|
478
|
+
`Ask about anything by name, ask what's wrong, or say a change in its own words —`,
|
|
479
|
+
`like "${store.allMutations()[0]?.title ?? "an action"}".`,
|
|
480
|
+
]),
|
|
481
|
+
proposals: [],
|
|
482
|
+
};
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
const NUMBER_WORDS = ["No", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten"];
|
|
486
|
+
const numberWord = (count) => NUMBER_WORDS[count] ?? String(count);
|
|
487
|
+
/** One honest answer for one form field, or undefined — never a guess. */
|
|
488
|
+
function answerFrom(field, referents, quoted, today,
|
|
489
|
+
/** The sentence, lower-cased: a choice is answered by its own word in it. */
|
|
490
|
+
asked = "") {
|
|
491
|
+
switch (field.control) {
|
|
492
|
+
case "node": {
|
|
493
|
+
const match = referents.find((node) => field.kinds.includes("*") || field.kinds.includes(node.kind));
|
|
494
|
+
return match?.id;
|
|
495
|
+
}
|
|
496
|
+
case "text":
|
|
497
|
+
return quoted;
|
|
498
|
+
case "date": {
|
|
499
|
+
const day = today ?? new Date().toISOString().slice(0, 10);
|
|
500
|
+
// A sentence that names no hour still has to give one when the act asks for a time of day.
|
|
501
|
+
return field.time ? `${day}T09:00` : day;
|
|
502
|
+
}
|
|
503
|
+
/*
|
|
504
|
+
* A CHOICE IS ITS OWN WORD. "Give a role keeper to Sam" names the role
|
|
505
|
+
* the way a person would, and a closed set of options is the one kind
|
|
506
|
+
* of argument a sentence can settle exactly: the option that appears
|
|
507
|
+
* as a whole word is the answer; two of them, or none, is no answer.
|
|
508
|
+
*/
|
|
509
|
+
case "choice": {
|
|
510
|
+
const words = new Set(asked.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean));
|
|
511
|
+
const named = (field.options ?? []).filter((option) => option
|
|
512
|
+
.toLowerCase()
|
|
513
|
+
.split(/[^a-z0-9]+/)
|
|
514
|
+
.filter(Boolean)
|
|
515
|
+
.every((part) => words.has(part)));
|
|
516
|
+
return named.length === 1 ? named[0] : undefined;
|
|
517
|
+
}
|
|
518
|
+
default:
|
|
519
|
+
return undefined;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* A model holds the conversation, through the same one-function seam and
|
|
524
|
+
* the same validation gate as every other model use. The reply contract is
|
|
525
|
+
* JSON — something to say, calls to propose — and an unparseable answer
|
|
526
|
+
* degrades to words with no proposals rather than to guesses.
|
|
527
|
+
*/
|
|
528
|
+
export function llmResponder(options) {
|
|
529
|
+
return async (store, text, context = {}) => {
|
|
530
|
+
/*
|
|
531
|
+
* EACH ACT WITH WHAT IT TAKES. Listed by name and description alone, a
|
|
532
|
+
* model proposed `add-plot` with no label and no beds, and the person
|
|
533
|
+
* was handed a form for what they had just said in words. The same
|
|
534
|
+
* form fields the menu asks with, said as a signature.
|
|
535
|
+
*/
|
|
536
|
+
const argument = (field) => {
|
|
537
|
+
const type = field.control === "node"
|
|
538
|
+
? `name of ${field.kinds.includes("*") ? "anything" : field.kinds.join(" or ")}`
|
|
539
|
+
: field.control === "choice"
|
|
540
|
+
? (field.options ?? []).map((option) => JSON.stringify(option)).join(" | ")
|
|
541
|
+
: field.control === "date"
|
|
542
|
+
? field.time
|
|
543
|
+
? "YYYY-MM-DDTHH:MM"
|
|
544
|
+
: "YYYY-MM-DD"
|
|
545
|
+
: field.control === "text" || field.control === "number" || field.control === "boolean"
|
|
546
|
+
? field.control
|
|
547
|
+
: "value";
|
|
548
|
+
return `${field.name}${field.optional ? "?" : ""}: ${type}`;
|
|
549
|
+
};
|
|
550
|
+
const mutations = store
|
|
551
|
+
.allMutations()
|
|
552
|
+
.map((mutation) => `- ${mutation.name}(${formFields(mutation.input).map(argument).join(", ")}): ${mutation.description ?? mutation.title ?? ""}`)
|
|
553
|
+
.join("\n");
|
|
554
|
+
/*
|
|
555
|
+
* WHAT IS ACTUALLY IN HERE, BY NAME.
|
|
556
|
+
*
|
|
557
|
+
* A model that cannot see the names cannot use them: asked to add a
|
|
558
|
+
* field to Meal it answers `{"kind": "Meal"}` and hopes, because the
|
|
559
|
+
* prompt listed the acts and never the things. A bounded sample per
|
|
560
|
+
* kind is enough to name anything a sentence is likely to mention, and
|
|
561
|
+
* `resolveProposal` turns whichever name it picks into the id.
|
|
562
|
+
*/
|
|
563
|
+
const shape = store.schema.kinds
|
|
564
|
+
.filter((kind) => !store.modules.disabledKinds.has(kind))
|
|
565
|
+
.map((kind) => {
|
|
566
|
+
const definition = store.schema.tryDefinition(kind);
|
|
567
|
+
const members = store.graph.nodesOfKind(kind);
|
|
568
|
+
const names = members
|
|
569
|
+
.slice(0, 12)
|
|
570
|
+
.map((node) => labelOf(definition, node))
|
|
571
|
+
.join(", ");
|
|
572
|
+
return `- ${kind} (${definition?.plural ?? `${kind}s`}, ${members.length}): ${names}${members.length > 12 ? ", …" : ""}`;
|
|
573
|
+
})
|
|
574
|
+
.join("\n");
|
|
575
|
+
/*
|
|
576
|
+
* HOW THINGS ARE CONNECTED. A model told "move it to Erin" can only
|
|
577
|
+
* answer from connections it can see; a bounded list is enough for the
|
|
578
|
+
* graphs a conversation is held over.
|
|
579
|
+
*/
|
|
580
|
+
const nameOf = (id) => {
|
|
581
|
+
const node = store.graph.getNode(id);
|
|
582
|
+
return node ? labelOf(store.schema.tryDefinition(node.kind), node) : id;
|
|
583
|
+
};
|
|
584
|
+
const edges = [...store.graph.allEdges()];
|
|
585
|
+
const connections = edges
|
|
586
|
+
.slice(0, 40)
|
|
587
|
+
.map((edge) => `- ${nameOf(edge.from)} —${edge.kind}→ ${nameOf(edge.to)}`)
|
|
588
|
+
.join("\n");
|
|
589
|
+
const selected = (context.selection ?? [])
|
|
590
|
+
.map((id) => {
|
|
591
|
+
const node = store.graph.getNode(id);
|
|
592
|
+
return node ? `${id} (${labelOf(store.schema.tryDefinition(node.kind), node)})` : id;
|
|
593
|
+
})
|
|
594
|
+
.join(", ");
|
|
595
|
+
const history = (context.history ?? [])
|
|
596
|
+
.slice(-8)
|
|
597
|
+
.map((turn) => `${turn.role === "person" ? "Person" : "You"}: ${turn.text}`)
|
|
598
|
+
.join("\n");
|
|
599
|
+
const trouble = store
|
|
600
|
+
.violations()
|
|
601
|
+
.map((violation) => `- ${violation.message}`)
|
|
602
|
+
.join("\n");
|
|
603
|
+
/*
|
|
604
|
+
* The floor's reading, offered as a starting point rather than a rule.
|
|
605
|
+
* "Keep it, correct it, or split it" is the instruction that turns one
|
|
606
|
+
* loose sentence into the two or three acts it actually described.
|
|
607
|
+
*/
|
|
608
|
+
const reading = (context.reading ?? [])
|
|
609
|
+
.map((proposal) => `- ${proposal.mutation} ${JSON.stringify(proposal.args)}`)
|
|
610
|
+
.join("\n");
|
|
611
|
+
const prompt = [
|
|
612
|
+
[
|
|
613
|
+
"You are the seat of a typed context graph. You answer questions about it and turn requests for change into proposals of its declared mutations, which the person reviews and applies.",
|
|
614
|
+
"- Fill every argument a mutation takes. Refer to things by their exact name as listed below.",
|
|
615
|
+
"- When a request needs something that does not exist yet, propose creating it first, then refer to it by the name you gave it. Choose sensible names and numbers rather than leaving them out.",
|
|
616
|
+
'- "it", "that" and "this" mean what the conversation or the selection points at.',
|
|
617
|
+
'- "say" is one short sentence in plain words. Do not restate the proposals in it; they are shown beneath it.',
|
|
618
|
+
"- A question gets an answer and no proposals.",
|
|
619
|
+
].join("\n"),
|
|
620
|
+
`Today is ${new Date().toISOString().slice(0, 10)}.`,
|
|
621
|
+
`Mutations:\n${mutations}`,
|
|
622
|
+
shape ? `What is in the graph now:\n${shape}` : "",
|
|
623
|
+
connections ? `How they are connected:\n${connections}${edges.length > 40 ? "\n- …" : ""}` : "",
|
|
624
|
+
reading
|
|
625
|
+
? `A first reading of this request, worked out from the graph:\n${reading}\nKeep it, correct it, or split it into several — one proposal per distinct change the person described.`
|
|
626
|
+
: "Where a request describes several changes, answer with several proposals — one per distinct change.",
|
|
627
|
+
trouble ? `Currently broken:\n${trouble}` : "Nothing is broken.",
|
|
628
|
+
selected ? `Selected right now (what "this" means): ${selected}` : "Nothing is selected.",
|
|
629
|
+
history ? `Conversation so far:\n${history}` : "",
|
|
630
|
+
`Person: ${text}`,
|
|
631
|
+
'Answer ONLY JSON: {"say": string, "proposals": [{"mutation": string, "args": object, "why": string}]}.',
|
|
632
|
+
]
|
|
633
|
+
.filter(Boolean)
|
|
634
|
+
.join("\n\n");
|
|
635
|
+
const answer = await options.complete(prompt);
|
|
636
|
+
const read = firstJsonObject(answer);
|
|
637
|
+
/*
|
|
638
|
+
* A bare array IS the list of proposals — the shape a model reaches for
|
|
639
|
+
* about as often as the one it was asked for.
|
|
640
|
+
*/
|
|
641
|
+
const parsed = (Array.isArray(read) ? { proposals: read } : read);
|
|
642
|
+
/*
|
|
643
|
+
* A PERSON IS NEVER SHOWN THE PLUMBING.
|
|
644
|
+
*
|
|
645
|
+
* The contract is JSON, and the old fallback put the raw answer in the
|
|
646
|
+
* bubble when it would not parse — so a model that closed one brace too
|
|
647
|
+
* many produced a chat message reading `{"say": "Yes", "proposals":
|
|
648
|
+
* [{"mutation": "add-field", …}]}}`. `firstJsonObject` reads the object
|
|
649
|
+
* the model meant; where there is no object at all, a model that was
|
|
650
|
+
* asked for JSON and wrote prose is answering in the wrong shape, and
|
|
651
|
+
* saying so is better than pasting it.
|
|
652
|
+
*/
|
|
653
|
+
if (!parsed || typeof parsed !== "object") {
|
|
654
|
+
const prose = answer.trim();
|
|
655
|
+
const looksLikeJson = prose.startsWith("{") || prose.startsWith("[");
|
|
656
|
+
return {
|
|
657
|
+
say: prose.length > 0 && !looksLikeJson
|
|
658
|
+
? prose
|
|
659
|
+
: "The model answered in a shape I could not read. Ask again, or try a different one from the gear.",
|
|
660
|
+
proposals: [],
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
/*
|
|
664
|
+
* And a model names things the way a person does — "Meal", not
|
|
665
|
+
* `declared:meal` — so a label that means exactly one node is read as
|
|
666
|
+
* that node before the gate sees it.
|
|
667
|
+
*/
|
|
668
|
+
const offered = (parsed.proposals ?? []).map((proposal) => resolveProposal(store, proposal));
|
|
669
|
+
const kept = validateProposals(store, offered, options.may);
|
|
670
|
+
/*
|
|
671
|
+
* WHAT THE GATE TOOK OUT IS SAID OUT LOUD. A model that answers "Sure"
|
|
672
|
+
* and names an act this app does not have used to leave a sentence with
|
|
673
|
+
* nothing under it: no proposal, no refusal, no way to tell whether the
|
|
674
|
+
* seat had understood. Silence is the one answer that cannot be acted
|
|
675
|
+
* on.
|
|
676
|
+
*/
|
|
677
|
+
const dropped = droppedProposals(store, offered, options.may);
|
|
678
|
+
const unknown = dropped.filter((one) => one.why === "unknown").map((one) => one.proposal.mutation);
|
|
679
|
+
const barred = dropped.filter((one) => one.why === "not-allowed").map((one) => one.proposal.mutation);
|
|
680
|
+
const aside = [
|
|
681
|
+
unknown.length > 0
|
|
682
|
+
? `It also suggested ${unknown.map((name) => `"${name}"`).join(", ")}, which this app has no act for.`
|
|
683
|
+
: "",
|
|
684
|
+
barred.length > 0
|
|
685
|
+
? `${barred.map((name) => `"${name}"`).join(", ")} is not something this seat may run.`
|
|
686
|
+
: "",
|
|
687
|
+
kept.length === 0 && offered.length > 0 ? "Nothing it suggested can be applied here." : "",
|
|
688
|
+
]
|
|
689
|
+
.filter(Boolean)
|
|
690
|
+
.join(" ");
|
|
691
|
+
const said = typeof parsed.say === "string" && parsed.say.length > 0 ? parsed.say : "";
|
|
692
|
+
return {
|
|
693
|
+
say: [said, aside].filter(Boolean).join(" ") || "…",
|
|
694
|
+
proposals: kept,
|
|
695
|
+
};
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
//# sourceMappingURL=conversation.js.map
|