ei-tui 0.6.0 → 0.6.2
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/package.json +1 -1
- package/src/core/handlers/human-matching.ts +13 -9
- package/src/core/orchestrators/human-extraction.ts +1 -0
- package/src/prompts/human/topic-update.ts +1 -1
- package/tui/src/commands/me.tsx +12 -6
- package/tui/src/util/yaml-context.ts +66 -0
- package/tui/src/util/yaml-human.ts +274 -0
- package/tui/src/util/yaml-persona.ts +479 -0
- package/tui/src/util/yaml-provider.ts +215 -0
- package/tui/src/util/yaml-queue.ts +81 -0
- package/tui/src/util/yaml-quotes.ts +46 -0
- package/tui/src/util/yaml-serializers.ts +9 -1510
- package/tui/src/util/yaml-settings.ts +223 -0
- package/tui/src/util/yaml-shared.ts +32 -0
- package/tui/src/util/yaml-toolkit.ts +55 -0
package/package.json
CHANGED
|
@@ -122,10 +122,7 @@ export async function handleTopicUpdate(response: LLMResponse, state: StateManag
|
|
|
122
122
|
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
123
123
|
const roomId = response.request.data.roomId as string | undefined;
|
|
124
124
|
const candidateCategory = response.request.data.candidateCategory as string | undefined;
|
|
125
|
-
|
|
126
|
-
if (!result.name || !result.description || result.sentiment === undefined) {
|
|
127
|
-
throw new Error(`[handleTopicUpdate] Missing required fields: name=${result.name}, description=${!!result.description}, sentiment=${result.sentiment}`);
|
|
128
|
-
}
|
|
125
|
+
const candidateName = response.request.data.candidateName as string | undefined;
|
|
129
126
|
|
|
130
127
|
const personaIds = personaId.split("|").filter(Boolean);
|
|
131
128
|
const primaryId = personaIds[0] ?? personaId;
|
|
@@ -147,14 +144,21 @@ export async function handleTopicUpdate(response: LLMResponse, state: StateManag
|
|
|
147
144
|
|
|
148
145
|
const existingTopic = isNewItem ? undefined : human.topics.find(t => t.id === existingItemId);
|
|
149
146
|
|
|
147
|
+
const resolvedName = result.name || existingTopic?.name || candidateName;
|
|
148
|
+
const resolvedDescription = typeof result.description === 'string' ? result.description : existingTopic?.description;
|
|
149
|
+
|
|
150
|
+
if (!resolvedName || !resolvedDescription || result.sentiment === undefined) {
|
|
151
|
+
throw new Error(`[handleTopicUpdate] Missing required fields: name=${resolvedName}, description=${!!resolvedDescription}, sentiment=${result.sentiment}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
150
154
|
let embedding: number[] | undefined;
|
|
151
155
|
try {
|
|
152
156
|
const embeddingService = getEmbeddingService();
|
|
153
157
|
const category = result.category ?? candidateCategory ?? existingTopic?.category;
|
|
154
|
-
const text = getTopicEmbeddingText({ name:
|
|
158
|
+
const text = getTopicEmbeddingText({ name: resolvedName, category, description: resolvedDescription });
|
|
155
159
|
embedding = await embeddingService.embed(text);
|
|
156
160
|
} catch (err) {
|
|
157
|
-
console.warn(`[handleTopicUpdate] Failed to compute embedding for topic "${
|
|
161
|
+
console.warn(`[handleTopicUpdate] Failed to compute embedding for topic "${resolvedName}":`, err);
|
|
158
162
|
}
|
|
159
163
|
|
|
160
164
|
const exposureImpact = result.exposure_impact as ExposureImpact | undefined;
|
|
@@ -167,8 +171,8 @@ export async function handleTopicUpdate(response: LLMResponse, state: StateManag
|
|
|
167
171
|
|
|
168
172
|
const topic: Topic = {
|
|
169
173
|
id: itemId,
|
|
170
|
-
name:
|
|
171
|
-
description:
|
|
174
|
+
name: resolvedName,
|
|
175
|
+
description: resolvedDescription,
|
|
172
176
|
sentiment: result.sentiment,
|
|
173
177
|
category: result.category ?? candidateCategory ?? existingTopic?.category,
|
|
174
178
|
exposure_current: calculateExposureCurrent(exposureImpact, existingTopic?.exposure_current ?? 0),
|
|
@@ -189,7 +193,7 @@ export async function handleTopicUpdate(response: LLMResponse, state: StateManag
|
|
|
189
193
|
: state.messages_get(personaId);
|
|
190
194
|
await validateAndStoreQuotes(result.quotes, allMessages, itemId, personaDisplayName, personaGroup, state);
|
|
191
195
|
|
|
192
|
-
console.log(`[handleTopicUpdate] ${isNewItem ? "Created" : "Updated"} topic "${
|
|
196
|
+
console.log(`[handleTopicUpdate] ${isNewItem ? "Created" : "Updated"} topic "${resolvedName}"`);
|
|
193
197
|
}
|
|
194
198
|
|
|
195
199
|
export async function handlePersonUpdate(response: LLMResponse, state: StateManager): Promise<void> {
|
|
@@ -422,6 +422,7 @@ export function queueTopicUpdate(
|
|
|
422
422
|
roomId: context.roomId,
|
|
423
423
|
isNewItem,
|
|
424
424
|
existingItemId: existingItem?.id,
|
|
425
|
+
candidateName: isNewItem ? context.candidateName : undefined,
|
|
425
426
|
candidateCategory: context.candidateCategory,
|
|
426
427
|
analyze_from_timestamp: getAnalyzeFromTimestamp(chunk),
|
|
427
428
|
},
|
|
@@ -250,7 +250,7 @@ ONLY ANALYZE the "Most Recent Messages". The "Earlier Conversation" is provided
|
|
|
250
250
|
${jsonTemplate}
|
|
251
251
|
\`\`\`
|
|
252
252
|
|
|
253
|
-
When returning a record,
|
|
253
|
+
When returning a record, always include \`sentiment\`. Include \`name\` only if you are changing it; omit it to keep the existing name. Always include \`description\` when returning a record.
|
|
254
254
|
|
|
255
255
|
If you find **NO EVIDENCE** of this TOPIC in the "Most Recent Messages", respond with: \`{}\`
|
|
256
256
|
|
package/tui/src/commands/me.tsx
CHANGED
|
@@ -80,21 +80,27 @@ export const meCommand: Command = {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
for (const fact of parsed.facts) {
|
|
83
|
-
|
|
83
|
+
if (parsed.changedFactIds.has(fact.id)) {
|
|
84
|
+
await ctx.ei.upsertFact(fact);
|
|
85
|
+
}
|
|
84
86
|
}
|
|
85
87
|
for (const topic of parsed.topics) {
|
|
86
|
-
|
|
88
|
+
if (parsed.changedTopicIds.has(topic.id)) {
|
|
89
|
+
await ctx.ei.upsertTopic(topic);
|
|
90
|
+
}
|
|
87
91
|
}
|
|
88
92
|
for (const person of parsed.people) {
|
|
89
|
-
|
|
93
|
+
if (parsed.changedPersonIds.has(person.id)) {
|
|
94
|
+
await ctx.ei.upsertPerson(person);
|
|
95
|
+
}
|
|
90
96
|
}
|
|
91
97
|
|
|
92
98
|
const deleteCount = parsed.deletedFactIds.length +
|
|
93
99
|
parsed.deletedTopicIds.length +
|
|
94
100
|
parsed.deletedPersonIds.length;
|
|
95
|
-
const updateCount = parsed.
|
|
96
|
-
parsed.
|
|
97
|
-
parsed.
|
|
101
|
+
const updateCount = parsed.changedFactIds.size +
|
|
102
|
+
parsed.changedTopicIds.size +
|
|
103
|
+
parsed.changedPersonIds.size;
|
|
98
104
|
|
|
99
105
|
ctx.showNotification(`Updated ${updateCount} items, deleted ${deleteCount}`, "info");
|
|
100
106
|
return;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import YAML from "yaml";
|
|
2
|
+
import type { Message } from "../../../src/core/types.js";
|
|
3
|
+
import { ContextStatus } from "../../../src/core/types.js";
|
|
4
|
+
|
|
5
|
+
interface EditableMessage {
|
|
6
|
+
id: string;
|
|
7
|
+
role: "human" | "system";
|
|
8
|
+
timestamp: string;
|
|
9
|
+
context_status: ContextStatus;
|
|
10
|
+
_delete?: boolean;
|
|
11
|
+
content?: string;
|
|
12
|
+
silence_reason?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getContent(m: { content?: string; verbal_response?: string; action_response?: string }): string {
|
|
16
|
+
if (m.content) return m.content;
|
|
17
|
+
const parts: string[] = [];
|
|
18
|
+
if (m.action_response) parts.push(`_${m.action_response}_`);
|
|
19
|
+
if (m.verbal_response) parts.push(m.verbal_response);
|
|
20
|
+
return parts.join('\n\n');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function contextToYAML(messages: Message[]): string {
|
|
24
|
+
const header = [
|
|
25
|
+
"# context_status: default | always | never",
|
|
26
|
+
"# _delete: true — permanently removes the message",
|
|
27
|
+
"# content | silence_reason",
|
|
28
|
+
].join("\n");
|
|
29
|
+
|
|
30
|
+
const data: EditableMessage[] = messages.map((m) => ({
|
|
31
|
+
id: m.id,
|
|
32
|
+
role: m.role,
|
|
33
|
+
timestamp: m.timestamp,
|
|
34
|
+
context_status: m.context_status,
|
|
35
|
+
_delete: false,
|
|
36
|
+
content: getContent(m) || undefined,
|
|
37
|
+
silence_reason: m.silence_reason,
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
return header + "\n" + YAML.stringify(data, { lineWidth: 0 });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ContextYAMLResult {
|
|
44
|
+
messages: Array<{ id: string; context_status: ContextStatus }>;
|
|
45
|
+
deletedMessageIds: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function contextFromYAML(yamlContent: string): ContextYAMLResult {
|
|
49
|
+
const data = YAML.parse(yamlContent) as EditableMessage[];
|
|
50
|
+
|
|
51
|
+
const deletedMessageIds: string[] = [];
|
|
52
|
+
const messages: Array<{ id: string; context_status: ContextStatus }> = [];
|
|
53
|
+
|
|
54
|
+
for (const m of data ?? []) {
|
|
55
|
+
if (m._delete) {
|
|
56
|
+
deletedMessageIds.push(m.id);
|
|
57
|
+
} else {
|
|
58
|
+
const normalized = (m.context_status ?? 'default').toString().toLowerCase() as ContextStatus;
|
|
59
|
+
messages.push({ id: m.id, context_status: normalized });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { messages, deletedMessageIds };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import YAML from "yaml";
|
|
2
|
+
import type {
|
|
3
|
+
HumanEntity,
|
|
4
|
+
Fact,
|
|
5
|
+
Topic,
|
|
6
|
+
Person,
|
|
7
|
+
PersonIdentifier,
|
|
8
|
+
} from "../../../src/core/types.js";
|
|
9
|
+
import { BUILT_IN_FACT_NAMES } from "../../../src/core/constants/built-in-facts.js";
|
|
10
|
+
import { BUILT_IN_IDENTIFIER_TYPES } from "../../../src/core/constants/built-in-identifier-types.js";
|
|
11
|
+
|
|
12
|
+
interface EditableTopic extends Omit<Topic, 'persona_groups'> {
|
|
13
|
+
_delete?: boolean;
|
|
14
|
+
persona_groups?: Record<string, boolean>[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface EditableFact extends Omit<Fact, 'persona_groups'> {
|
|
18
|
+
_delete?: boolean;
|
|
19
|
+
persona_groups?: Record<string, boolean>[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface YAMLPersonIdentifier {
|
|
23
|
+
type: string;
|
|
24
|
+
value: string;
|
|
25
|
+
primary?: true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface EditablePersonYAML extends Omit<Person, 'identifiers' | 'persona_groups'> {
|
|
29
|
+
identifiers: YAMLPersonIdentifier[];
|
|
30
|
+
_delete?: boolean;
|
|
31
|
+
persona_groups?: Record<string, boolean>[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface EditableHumanData {
|
|
35
|
+
facts: EditableFact[];
|
|
36
|
+
topics: EditableTopic[];
|
|
37
|
+
people: EditablePersonYAML[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type WithReadOnlyFields = {
|
|
41
|
+
learned_on?: string;
|
|
42
|
+
learned_by?: string;
|
|
43
|
+
validated_date?: string;
|
|
44
|
+
last_mentioned?: string;
|
|
45
|
+
last_updated: string;
|
|
46
|
+
last_changed_by?: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function readOnlyToEnd<T extends WithReadOnlyFields>(item: T): T {
|
|
50
|
+
const { learned_on, learned_by, validated_date, last_mentioned, last_updated, last_changed_by, ...rest } = item;
|
|
51
|
+
return { ...rest, learned_on, learned_by, validated_date, last_mentioned, last_updated, last_changed_by } as T;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function buildGroupCheckboxMap(itemGroups: string[], allGroups: string[]): Record<string, boolean>[] {
|
|
55
|
+
const activeSet = new Set(itemGroups);
|
|
56
|
+
return [...new Set([...allGroups, ...itemGroups])].map(g => ({ [g]: activeSet.has(g) }));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function toYAMLIdentifiers(identifiers: PersonIdentifier[], personaLookup?: Map<string, string>): YAMLPersonIdentifier[] {
|
|
60
|
+
return identifiers.map(({ type, value, is_primary }) => {
|
|
61
|
+
const resolvedValue = type === 'Ei Persona' ? (personaLookup?.get(value) ?? value) : value;
|
|
62
|
+
const entry: YAMLPersonIdentifier = { type, value: resolvedValue };
|
|
63
|
+
if (is_primary) entry.primary = true;
|
|
64
|
+
return entry;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function knownTypesComment(personaLookup?: Map<string, string>): string {
|
|
69
|
+
const lines = [`# Valid types: ${BUILT_IN_IDENTIFIER_TYPES.join(', ')}`];
|
|
70
|
+
if (personaLookup && personaLookup.size > 0) {
|
|
71
|
+
lines.push(`# Personas: ${Array.from(personaLookup.values()).join(', ')}`);
|
|
72
|
+
}
|
|
73
|
+
return lines.join('\n');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function parseGroupCheckboxMap(groups: Record<string, boolean>[] | undefined): string[] {
|
|
77
|
+
if (!groups) return [];
|
|
78
|
+
const result: string[] = [];
|
|
79
|
+
for (const record of groups) {
|
|
80
|
+
for (const [name, active] of Object.entries(record)) {
|
|
81
|
+
if (active) result.push(name);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function humanToYAML(human: HumanEntity, personaLookup?: Map<string, string>, allGroups: string[] = []): string {
|
|
88
|
+
const data: EditableHumanData = {
|
|
89
|
+
facts: human.facts.map(f => { const { interested_personas: _ip, persona_groups, ...rest } = readOnlyToEnd(f); return { ...rest, persona_groups: buildGroupCheckboxMap(persona_groups ?? [], allGroups), _delete: false }; }),
|
|
90
|
+
topics: human.topics.map(t => { const { interested_personas: _ip, persona_groups, ...rest } = readOnlyToEnd(t); return { ...rest, persona_groups: buildGroupCheckboxMap(persona_groups ?? [], allGroups), _delete: false }; }),
|
|
91
|
+
people: human.people.map(p => {
|
|
92
|
+
const { identifiers, interested_personas: _ip, persona_groups, ...rest } = readOnlyToEnd(p);
|
|
93
|
+
return {
|
|
94
|
+
...rest,
|
|
95
|
+
persona_groups: buildGroupCheckboxMap(persona_groups ?? [], allGroups),
|
|
96
|
+
identifiers: toYAMLIdentifiers(identifiers ?? [], personaLookup),
|
|
97
|
+
_delete: false as const,
|
|
98
|
+
};
|
|
99
|
+
}),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const personComment = knownTypesComment(personaLookup);
|
|
103
|
+
|
|
104
|
+
return YAML.stringify(data, {
|
|
105
|
+
lineWidth: 0,
|
|
106
|
+
})
|
|
107
|
+
.replace(/^(\s+)(learned_on: .+)$/mg, '$1# [read-only] $2')
|
|
108
|
+
.replace(/^(\s+)(learned_by: )(.+)$/mg, (_, indent, key, val) => {
|
|
109
|
+
const trimmed = val.trim();
|
|
110
|
+
const displayName = personaLookup?.get(trimmed) ?? trimmed;
|
|
111
|
+
return `${indent}# [read-only] ${key}${displayName}`;
|
|
112
|
+
})
|
|
113
|
+
.replace(/^(\s+)(validated_date: .+)$/mg, '$1# [read-only] $2')
|
|
114
|
+
.replace(/^(\s+)(last_mentioned: .+)$/mg, '$1# [read-only] $2')
|
|
115
|
+
.replace(/^(\s+)(last_updated: .+)$/mg, '$1# [read-only] $2')
|
|
116
|
+
.replace(/^(\s+)(last_changed_by: )(.+)$/mg, (_, indent, key, val) => {
|
|
117
|
+
const trimmed = val.trim();
|
|
118
|
+
const displayName = personaLookup?.get(trimmed) ?? trimmed;
|
|
119
|
+
return `${indent}# [read-only] ${key}${displayName}`;
|
|
120
|
+
})
|
|
121
|
+
.replace(/^(\s+)(identifiers:)/mg, (_, indent, _key) => {
|
|
122
|
+
return `${indent}${personComment}\n${indent}identifiers:`;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface HumanYAMLResult {
|
|
127
|
+
facts: Fact[];
|
|
128
|
+
topics: Topic[];
|
|
129
|
+
people: Person[];
|
|
130
|
+
deletedFactIds: string[];
|
|
131
|
+
deletedTopicIds: string[];
|
|
132
|
+
deletedPersonIds: string[];
|
|
133
|
+
changedFactIds: Set<string>;
|
|
134
|
+
changedTopicIds: Set<string>;
|
|
135
|
+
changedPersonIds: Set<string>;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function identifiersEqual(a: PersonIdentifier[] | undefined, b: PersonIdentifier[] | undefined): boolean {
|
|
139
|
+
const normalize = (ids: PersonIdentifier[] | undefined) =>
|
|
140
|
+
[...(ids ?? [])].sort((x, y) => `${x.type}:${x.value}`.localeCompare(`${y.type}:${y.value}`));
|
|
141
|
+
const na = normalize(a);
|
|
142
|
+
const nb = normalize(b);
|
|
143
|
+
if (na.length !== nb.length) return false;
|
|
144
|
+
return na.every((id, i) =>
|
|
145
|
+
id.type === nb[i].type &&
|
|
146
|
+
id.value === nb[i].value &&
|
|
147
|
+
Boolean(id.is_primary) === Boolean(nb[i].is_primary)
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function groupsEqual(a: string[] | undefined, b: string[] | undefined): boolean {
|
|
152
|
+
const sa = [...(a ?? [])].sort();
|
|
153
|
+
const sb = [...(b ?? [])].sort();
|
|
154
|
+
if (sa.length !== sb.length) return false;
|
|
155
|
+
return sa.every((v, i) => v === sb[i]);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function factChanged(parsed: Fact, original: Fact): boolean {
|
|
159
|
+
const scalarFields: (keyof Fact)[] = [
|
|
160
|
+
'name', 'description', 'sentiment',
|
|
161
|
+
];
|
|
162
|
+
for (const field of scalarFields) {
|
|
163
|
+
if (parsed[field] !== original[field]) return true;
|
|
164
|
+
}
|
|
165
|
+
return !groupsEqual(parsed.persona_groups, original.persona_groups);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function topicChanged(parsed: Topic, original: Topic): boolean {
|
|
169
|
+
const scalarFields: (keyof Topic)[] = [
|
|
170
|
+
'name', 'description', 'sentiment', 'exposure_current', 'exposure_desired', 'category',
|
|
171
|
+
];
|
|
172
|
+
for (const field of scalarFields) {
|
|
173
|
+
if (parsed[field] !== original[field]) return true;
|
|
174
|
+
}
|
|
175
|
+
return !groupsEqual(parsed.persona_groups, original.persona_groups);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function personChanged(parsed: Person, original: Person): boolean {
|
|
179
|
+
const scalarFields: (keyof Person)[] = [
|
|
180
|
+
'name', 'description', 'sentiment', 'relationship',
|
|
181
|
+
'exposure_current', 'exposure_desired',
|
|
182
|
+
];
|
|
183
|
+
for (const field of scalarFields) {
|
|
184
|
+
if (parsed[field] !== original[field]) return true;
|
|
185
|
+
}
|
|
186
|
+
if (!groupsEqual(parsed.persona_groups, original.persona_groups)) return true;
|
|
187
|
+
return !identifiersEqual(parsed.identifiers, original.identifiers);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function humanFromYAML(yamlContent: string, original?: HumanEntity): HumanYAMLResult {
|
|
191
|
+
const stripped = yamlContent
|
|
192
|
+
.split('\n')
|
|
193
|
+
.filter(line => !/^\s*#\s*\[read-only\]/.test(line))
|
|
194
|
+
.join('\n');
|
|
195
|
+
const data = YAML.parse(stripped) as EditableHumanData;
|
|
196
|
+
|
|
197
|
+
const deletedFactIds: string[] = [];
|
|
198
|
+
const deletedTopicIds: string[] = [];
|
|
199
|
+
const deletedPersonIds: string[] = [];
|
|
200
|
+
const changedFactIds = new Set<string>();
|
|
201
|
+
const changedTopicIds = new Set<string>();
|
|
202
|
+
const changedPersonIds = new Set<string>();
|
|
203
|
+
|
|
204
|
+
const facts: Fact[] = [];
|
|
205
|
+
for (const f of data.facts ?? []) {
|
|
206
|
+
if (f._delete && !BUILT_IN_FACT_NAMES.has(f.name)) {
|
|
207
|
+
deletedFactIds.push(f.id);
|
|
208
|
+
} else {
|
|
209
|
+
const { _delete, persona_groups: groupMap, ...parsed } = f;
|
|
210
|
+
const originalFact = original?.facts.find(of => of.id === parsed.id);
|
|
211
|
+
const fact: Fact = originalFact
|
|
212
|
+
? { ...originalFact, ...parsed, persona_groups: parseGroupCheckboxMap(groupMap) }
|
|
213
|
+
: { ...parsed, persona_groups: parseGroupCheckboxMap(groupMap) };
|
|
214
|
+
facts.push(fact);
|
|
215
|
+
if (!originalFact || factChanged(fact, originalFact)) {
|
|
216
|
+
if (fact.description && !originalFact?.validated_date) {
|
|
217
|
+
fact.validated_date = new Date().toISOString();
|
|
218
|
+
}
|
|
219
|
+
changedFactIds.add(fact.id);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const topics: Topic[] = [];
|
|
225
|
+
for (const t of data.topics ?? []) {
|
|
226
|
+
if (t._delete) {
|
|
227
|
+
deletedTopicIds.push(t.id);
|
|
228
|
+
} else {
|
|
229
|
+
const { _delete, persona_groups: groupMap, ...parsed } = t;
|
|
230
|
+
const originalTopic = original?.topics.find(ot => ot.id === parsed.id);
|
|
231
|
+
const topic: Topic = originalTopic
|
|
232
|
+
? { ...originalTopic, ...parsed, persona_groups: parseGroupCheckboxMap(groupMap) }
|
|
233
|
+
: { ...parsed, persona_groups: parseGroupCheckboxMap(groupMap) };
|
|
234
|
+
topics.push(topic);
|
|
235
|
+
if (!originalTopic || topicChanged(topic, originalTopic)) {
|
|
236
|
+
changedTopicIds.add(topic.id);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const people: Person[] = [];
|
|
242
|
+
for (const p of data.people ?? []) {
|
|
243
|
+
if (p._delete) {
|
|
244
|
+
deletedPersonIds.push(p.id);
|
|
245
|
+
} else {
|
|
246
|
+
const { _delete, identifiers: yamlIdentifiers, persona_groups: groupMap, ...parsed } = p;
|
|
247
|
+
const identifiers: PersonIdentifier[] = (yamlIdentifiers ?? []).map(({ type, value, primary }) => ({
|
|
248
|
+
type,
|
|
249
|
+
value,
|
|
250
|
+
...(primary ? { is_primary: true } : {}),
|
|
251
|
+
}));
|
|
252
|
+
const originalPerson = original?.people.find(op => op.id === parsed.id);
|
|
253
|
+
const person: Person = originalPerson
|
|
254
|
+
? { ...originalPerson, ...parsed, identifiers, persona_groups: parseGroupCheckboxMap(groupMap) }
|
|
255
|
+
: { ...parsed, identifiers, persona_groups: parseGroupCheckboxMap(groupMap) };
|
|
256
|
+
people.push(person);
|
|
257
|
+
if (!originalPerson || personChanged(person, originalPerson)) {
|
|
258
|
+
changedPersonIds.add(person.id);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
facts,
|
|
265
|
+
topics,
|
|
266
|
+
people,
|
|
267
|
+
deletedFactIds,
|
|
268
|
+
deletedTopicIds,
|
|
269
|
+
deletedPersonIds,
|
|
270
|
+
changedFactIds,
|
|
271
|
+
changedTopicIds,
|
|
272
|
+
changedPersonIds,
|
|
273
|
+
};
|
|
274
|
+
}
|