synomem 0.6.10 → 0.7.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/dist/backend.d.ts.map +1 -1
- package/dist/backend.js +1 -0
- package/dist/backend.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +139 -43
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +22 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +164 -3
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -0
- package/dist/errors.js.map +1 -1
- package/dist/import.d.ts +83 -0
- package/dist/import.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +110 -2
- package/dist/mcp/index.js.map +1 -1
- package/dist/ports/repository.d.ts +10 -1
- package/dist/ports/repository.d.ts.map +1 -1
- package/dist/projections.d.ts.map +1 -1
- package/dist/projections.js +6 -0
- package/dist/projections.js.map +1 -1
- package/dist/remote.d.ts +22 -1
- package/dist/remote.d.ts.map +1 -1
- package/dist/remote.js +18 -4
- package/dist/remote.js.map +1 -1
- package/dist/schemas.d.ts +130 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +77 -0
- package/dist/schemas.js.map +1 -1
- package/dist/service.d.ts +14 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/storage.d.ts +12 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +136 -19
- package/dist/storage.js.map +1 -1
- package/dist/types.d.ts +80 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/backend.ts +1 -0
- package/src/cli.ts +305 -91
- package/src/client.ts +184 -2
- package/src/errors.ts +2 -0
- package/src/mcp/index.ts +140 -1
- package/src/ports/repository.ts +8 -0
- package/src/projections.ts +6 -0
- package/src/remote.ts +65 -4
- package/src/schemas.ts +85 -0
- package/src/service.ts +18 -0
- package/src/storage.ts +167 -13
- package/src/types.ts +90 -2
package/src/storage.ts
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
readJsonFile,
|
|
24
24
|
} from './fs-utils.js';
|
|
25
25
|
import { dueInstant } from './projections.js';
|
|
26
|
-
import { actorSchema, eventSchema, profileSchema } from './schemas.js';
|
|
26
|
+
import { actorSchema, eventSchema, profileSchema, topicProfileSchema } from './schemas.js';
|
|
27
27
|
import type {
|
|
28
28
|
ActorIdentity,
|
|
29
29
|
SynomemConfig,
|
|
@@ -39,6 +39,7 @@ import type {
|
|
|
39
39
|
ItemChange,
|
|
40
40
|
ItemListInput,
|
|
41
41
|
ItemSummary,
|
|
42
|
+
Topic,
|
|
42
43
|
RecordKind,
|
|
43
44
|
KudosListInput,
|
|
44
45
|
KudosSummary,
|
|
@@ -62,6 +63,7 @@ interface CurrentRow {
|
|
|
62
63
|
actor_display_name: string | null;
|
|
63
64
|
title: string;
|
|
64
65
|
tags_json: string;
|
|
66
|
+
topic_ids_json: string;
|
|
65
67
|
visibility: KudosSummary['visibility'];
|
|
66
68
|
status: KudosSummary['status'];
|
|
67
69
|
revocation_status: KudosSummary['revocationStatus'];
|
|
@@ -80,6 +82,7 @@ interface ItemRow {
|
|
|
80
82
|
actor_display_name: string | null;
|
|
81
83
|
title: string;
|
|
82
84
|
tags_json: string;
|
|
85
|
+
topic_ids_json: string;
|
|
83
86
|
visibility: ItemSummary['visibility'];
|
|
84
87
|
status: string;
|
|
85
88
|
recipient_agent_id: string | null;
|
|
@@ -99,6 +102,16 @@ interface ProfileRow {
|
|
|
99
102
|
profile_json: string;
|
|
100
103
|
}
|
|
101
104
|
|
|
105
|
+
interface TopicRow {
|
|
106
|
+
id: string;
|
|
107
|
+
display_name: string;
|
|
108
|
+
status: 'active' | 'archived';
|
|
109
|
+
created_at: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The schema version this file writes and expects. */
|
|
113
|
+
const SUPPORTED_SCHEMA_VERSION = 8;
|
|
114
|
+
|
|
102
115
|
const migrationV1 = `
|
|
103
116
|
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
104
117
|
version INTEGER PRIMARY KEY,
|
|
@@ -181,6 +194,7 @@ CREATE TABLE kudos_current (
|
|
|
181
194
|
actor_display_name TEXT,
|
|
182
195
|
title TEXT NOT NULL,
|
|
183
196
|
tags_json TEXT NOT NULL CHECK (json_valid(tags_json)),
|
|
197
|
+
topic_ids_json TEXT NOT NULL DEFAULT '[]' CHECK (json_valid(topic_ids_json)),
|
|
184
198
|
visibility TEXT NOT NULL,
|
|
185
199
|
status TEXT NOT NULL,
|
|
186
200
|
revocation_status TEXT NOT NULL,
|
|
@@ -270,6 +284,25 @@ CREATE UNIQUE INDEX agents_handle ON agents(handle);
|
|
|
270
284
|
ALTER TABLE agents ADD COLUMN status TEXT NOT NULL DEFAULT 'active';
|
|
271
285
|
`;
|
|
272
286
|
|
|
287
|
+
const migrationV8 = `
|
|
288
|
+
-- Topics: a controlled, reusable subject a record can be filed under,
|
|
289
|
+
-- distinct from a free-text tag (§ Topic in types.ts). One canonical display
|
|
290
|
+
-- name and a set of case-folded aliases, so a topic can be renamed without
|
|
291
|
+
-- retagging every record that already carries it.
|
|
292
|
+
CREATE TABLE topics (
|
|
293
|
+
id TEXT PRIMARY KEY,
|
|
294
|
+
display_name TEXT NOT NULL,
|
|
295
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
296
|
+
created_at TEXT NOT NULL,
|
|
297
|
+
updated_at TEXT NOT NULL
|
|
298
|
+
) STRICT;
|
|
299
|
+
CREATE TABLE topic_aliases (
|
|
300
|
+
alias TEXT PRIMARY KEY,
|
|
301
|
+
topic_id TEXT NOT NULL REFERENCES topics(id)
|
|
302
|
+
) STRICT;
|
|
303
|
+
CREATE INDEX topic_aliases_topic ON topic_aliases(topic_id);
|
|
304
|
+
`;
|
|
305
|
+
|
|
273
306
|
const migrationV3 = `
|
|
274
307
|
DROP TRIGGER IF EXISTS events_append_only_update;
|
|
275
308
|
DROP TRIGGER IF EXISTS events_append_only_delete;
|
|
@@ -317,6 +350,7 @@ CREATE TABLE items_current (
|
|
|
317
350
|
actor_display_name TEXT,
|
|
318
351
|
title TEXT NOT NULL,
|
|
319
352
|
tags_json TEXT NOT NULL CHECK (json_valid(tags_json)),
|
|
353
|
+
topic_ids_json TEXT NOT NULL DEFAULT '[]' CHECK (json_valid(topic_ids_json)),
|
|
320
354
|
visibility TEXT NOT NULL,
|
|
321
355
|
status TEXT NOT NULL,
|
|
322
356
|
recipient_agent_id TEXT,
|
|
@@ -379,6 +413,7 @@ function itemSummaryFromRow(row: ItemRow): ItemSummary {
|
|
|
379
413
|
},
|
|
380
414
|
title: row.title,
|
|
381
415
|
tags: JSON.parse(row.tags_json) as string[],
|
|
416
|
+
topicIds: JSON.parse(row.topic_ids_json) as string[],
|
|
382
417
|
visibility: row.visibility,
|
|
383
418
|
status: row.status,
|
|
384
419
|
...(row.recipient_agent_id ? { recipientAgentId: row.recipient_agent_id } : {}),
|
|
@@ -419,6 +454,7 @@ function summaryFromRow(row: CurrentRow): KudosSummary {
|
|
|
419
454
|
},
|
|
420
455
|
title: row.title,
|
|
421
456
|
tags: JSON.parse(row.tags_json) as string[],
|
|
457
|
+
topicIds: JSON.parse(row.topic_ids_json) as string[],
|
|
422
458
|
visibility: row.visibility,
|
|
423
459
|
status: row.status,
|
|
424
460
|
revocationStatus: row.revocation_status,
|
|
@@ -534,7 +570,7 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
534
570
|
const version = Number(
|
|
535
571
|
(db.prepare('PRAGMA user_version').get() as { user_version: number }).user_version,
|
|
536
572
|
);
|
|
537
|
-
if (version >
|
|
573
|
+
if (version > SUPPORTED_SCHEMA_VERSION) {
|
|
538
574
|
throw new SynomemError(
|
|
539
575
|
'UNSUPPORTED_SCHEMA',
|
|
540
576
|
`Database schema version ${version} is newer than this package supports.`,
|
|
@@ -626,14 +662,38 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
626
662
|
db.exec('PRAGMA user_version = 7');
|
|
627
663
|
});
|
|
628
664
|
}
|
|
665
|
+
const afterV7 = Number(
|
|
666
|
+
(db.prepare('PRAGMA user_version').get() as { user_version: number }).user_version,
|
|
667
|
+
);
|
|
668
|
+
if (afterV7 === 7) {
|
|
669
|
+
this.transactionSync(() => {
|
|
670
|
+
db.exec(migrationV8);
|
|
671
|
+
// A database bootstrapped fresh today already gets topic_ids_json
|
|
672
|
+
// from V1/V3's own CREATE TABLE text, so this only fires for a
|
|
673
|
+
// database that reached v7 before those columns existed — adding a
|
|
674
|
+
// column SQLite has no "IF NOT EXISTS" form for.
|
|
675
|
+
for (const table of ['kudos_current', 'items_current']) {
|
|
676
|
+
const hasColumn = (
|
|
677
|
+
db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>
|
|
678
|
+
).some((column) => column.name === 'topic_ids_json');
|
|
679
|
+
if (!hasColumn) {
|
|
680
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN topic_ids_json TEXT NOT NULL DEFAULT '[]'`);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
db.prepare(
|
|
684
|
+
'INSERT OR IGNORE INTO schema_migrations(version, applied_at) VALUES (?, ?)',
|
|
685
|
+
).run(8, new Date().toISOString());
|
|
686
|
+
db.exec('PRAGMA user_version = 8');
|
|
687
|
+
});
|
|
688
|
+
}
|
|
629
689
|
}
|
|
630
690
|
|
|
631
691
|
private assertSchemaSupported(): void {
|
|
632
692
|
const version = Number(
|
|
633
693
|
(this.db().prepare('PRAGMA user_version').get() as { user_version: number }).user_version,
|
|
634
694
|
);
|
|
635
|
-
if (version !==
|
|
636
|
-
if (version >= 1 && version
|
|
695
|
+
if (version !== SUPPORTED_SCHEMA_VERSION) {
|
|
696
|
+
if (version >= 1 && version < SUPPORTED_SCHEMA_VERSION) {
|
|
637
697
|
throw new SynomemError(
|
|
638
698
|
'UNSUPPORTED_SCHEMA',
|
|
639
699
|
`Database schema version ${version} requires migration. Open this home once with readOnly: false, then retry the read-only client.`,
|
|
@@ -641,7 +701,7 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
641
701
|
}
|
|
642
702
|
throw new SynomemError(
|
|
643
703
|
'UNSUPPORTED_SCHEMA',
|
|
644
|
-
`Expected database schema version
|
|
704
|
+
`Expected database schema version ${SUPPORTED_SCHEMA_VERSION}; found ${version}.`,
|
|
645
705
|
);
|
|
646
706
|
}
|
|
647
707
|
}
|
|
@@ -772,9 +832,9 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
772
832
|
.prepare(
|
|
773
833
|
`INSERT INTO kudos_current(
|
|
774
834
|
kudos_id, given_sequence, created_at, recipient_agent_id, recipient_display_name,
|
|
775
|
-
actor_kind, actor_id, actor_display_name, title, tags_json, visibility,
|
|
835
|
+
actor_kind, actor_id, actor_display_name, title, tags_json, topic_ids_json, visibility,
|
|
776
836
|
status, revocation_status, updated_sequence
|
|
777
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'unacknowledged', 'active', ?)`,
|
|
837
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'unacknowledged', 'active', ?)`,
|
|
778
838
|
)
|
|
779
839
|
.run(
|
|
780
840
|
event.id,
|
|
@@ -787,6 +847,7 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
787
847
|
event.actor.displayName ?? null,
|
|
788
848
|
event.title,
|
|
789
849
|
JSON.stringify(event.tags ?? []),
|
|
850
|
+
JSON.stringify(event.topicIds ?? []),
|
|
790
851
|
event.visibility,
|
|
791
852
|
sequence,
|
|
792
853
|
);
|
|
@@ -810,6 +871,7 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
810
871
|
kind: RecordKind;
|
|
811
872
|
title: string;
|
|
812
873
|
tags?: string[];
|
|
874
|
+
topicIds?: string[];
|
|
813
875
|
visibility: ItemSummary['visibility'];
|
|
814
876
|
status: string;
|
|
815
877
|
recipientAgentId?: string;
|
|
@@ -824,10 +886,10 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
824
886
|
.prepare(
|
|
825
887
|
`INSERT INTO items_current(
|
|
826
888
|
item_id, kind, created_sequence, updated_sequence, created_at, updated_at,
|
|
827
|
-
actor_kind, actor_id, actor_display_name, title, tags_json, visibility, status,
|
|
889
|
+
actor_kind, actor_id, actor_display_name, title, tags_json, topic_ids_json, visibility, status,
|
|
828
890
|
recipient_agent_id, recipient_display_name, owner_agent_id, owner_display_name,
|
|
829
891
|
assignee_agent_id, assignee_display_name, due_at
|
|
830
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
892
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
831
893
|
)
|
|
832
894
|
.run(
|
|
833
895
|
event.aggregateId,
|
|
@@ -841,6 +903,7 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
841
903
|
event.actor.displayName ?? null,
|
|
842
904
|
values.title,
|
|
843
905
|
JSON.stringify(values.tags ?? []),
|
|
906
|
+
JSON.stringify(values.topicIds ?? []),
|
|
844
907
|
values.visibility,
|
|
845
908
|
values.status,
|
|
846
909
|
values.recipientAgentId ?? null,
|
|
@@ -865,6 +928,7 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
865
928
|
kind: 'kudos',
|
|
866
929
|
title: event.title,
|
|
867
930
|
tags: event.tags,
|
|
931
|
+
topicIds: event.topicIds,
|
|
868
932
|
visibility: event.visibility,
|
|
869
933
|
status: 'unacknowledged',
|
|
870
934
|
recipientAgentId: event.recipientAgentId,
|
|
@@ -877,6 +941,7 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
877
941
|
kind: 'memo',
|
|
878
942
|
title: event.subject,
|
|
879
943
|
tags: event.tags,
|
|
944
|
+
topicIds: event.topicIds,
|
|
880
945
|
visibility: event.visibility,
|
|
881
946
|
status: 'unread',
|
|
882
947
|
recipientAgentId: event.recipientAgentId,
|
|
@@ -889,6 +954,7 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
889
954
|
kind: 'note',
|
|
890
955
|
title: event.title,
|
|
891
956
|
tags: event.tags,
|
|
957
|
+
topicIds: event.topicIds,
|
|
892
958
|
visibility: event.visibility,
|
|
893
959
|
status: 'active',
|
|
894
960
|
ownerAgentId: event.ownerAgentId,
|
|
@@ -905,18 +971,20 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
905
971
|
kind: 'post',
|
|
906
972
|
title: event.title,
|
|
907
973
|
tags: event.tags,
|
|
974
|
+
topicIds: event.topicIds,
|
|
908
975
|
visibility: 'workspace',
|
|
909
976
|
status: 'active',
|
|
910
977
|
});
|
|
911
978
|
} else if (event.type === 'post.edited') {
|
|
912
979
|
this.db()
|
|
913
980
|
.prepare(
|
|
914
|
-
`UPDATE items_current SET title = ?, tags_json = ?,
|
|
981
|
+
`UPDATE items_current SET title = ?, tags_json = ?, topic_ids_json = ?,
|
|
915
982
|
updated_sequence = ?, updated_at = ? WHERE item_id = ?`,
|
|
916
983
|
)
|
|
917
984
|
.run(
|
|
918
985
|
event.title,
|
|
919
986
|
JSON.stringify(event.tags ?? []),
|
|
987
|
+
JSON.stringify(event.topicIds ?? []),
|
|
920
988
|
sequence,
|
|
921
989
|
event.createdAt,
|
|
922
990
|
event.postId,
|
|
@@ -952,12 +1020,13 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
952
1020
|
} else if (event.type === 'note.revised') {
|
|
953
1021
|
this.db()
|
|
954
1022
|
.prepare(
|
|
955
|
-
`UPDATE items_current SET title = ?, tags_json = ?, visibility = ?,
|
|
1023
|
+
`UPDATE items_current SET title = ?, tags_json = ?, topic_ids_json = ?, visibility = ?,
|
|
956
1024
|
updated_sequence = ?, updated_at = ? WHERE item_id = ?`,
|
|
957
1025
|
)
|
|
958
1026
|
.run(
|
|
959
1027
|
event.title,
|
|
960
1028
|
JSON.stringify(event.tags ?? []),
|
|
1029
|
+
JSON.stringify(event.topicIds ?? []),
|
|
961
1030
|
event.visibility,
|
|
962
1031
|
sequence,
|
|
963
1032
|
event.createdAt,
|
|
@@ -969,6 +1038,7 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
969
1038
|
kind: 'task',
|
|
970
1039
|
title: event.title,
|
|
971
1040
|
tags: event.tags,
|
|
1041
|
+
topicIds: event.topicIds,
|
|
972
1042
|
visibility: event.visibility,
|
|
973
1043
|
status: event.requiresAcceptance ? 'assigned' : 'open',
|
|
974
1044
|
assigneeAgentId: event.assigneeAgentId,
|
|
@@ -978,12 +1048,13 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
978
1048
|
} else if (event.type === 'task.updated') {
|
|
979
1049
|
this.db()
|
|
980
1050
|
.prepare(
|
|
981
|
-
`UPDATE items_current SET title = ?, tags_json = ?, visibility = ?, due_at = ?,
|
|
1051
|
+
`UPDATE items_current SET title = ?, tags_json = ?, topic_ids_json = ?, visibility = ?, due_at = ?,
|
|
982
1052
|
updated_sequence = ?, updated_at = ? WHERE item_id = ?`,
|
|
983
1053
|
)
|
|
984
1054
|
.run(
|
|
985
1055
|
event.title,
|
|
986
1056
|
JSON.stringify(event.tags ?? []),
|
|
1057
|
+
JSON.stringify(event.topicIds ?? []),
|
|
987
1058
|
event.visibility,
|
|
988
1059
|
dueInstant(event.due) ?? null,
|
|
989
1060
|
sequence,
|
|
@@ -1003,6 +1074,7 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
1003
1074
|
kind: 'todo',
|
|
1004
1075
|
title: event.title,
|
|
1005
1076
|
tags: event.tags,
|
|
1077
|
+
topicIds: event.topicIds,
|
|
1006
1078
|
visibility: 'private',
|
|
1007
1079
|
status: 'open',
|
|
1008
1080
|
ownerAgentId: event.actor.id,
|
|
@@ -1012,12 +1084,13 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
1012
1084
|
} else if (event.type === 'todo.updated') {
|
|
1013
1085
|
this.db()
|
|
1014
1086
|
.prepare(
|
|
1015
|
-
`UPDATE items_current SET title = ?, tags_json = ?, due_at = ?,
|
|
1087
|
+
`UPDATE items_current SET title = ?, tags_json = ?, topic_ids_json = ?, due_at = ?,
|
|
1016
1088
|
updated_sequence = ?, updated_at = ? WHERE item_id = ?`,
|
|
1017
1089
|
)
|
|
1018
1090
|
.run(
|
|
1019
1091
|
event.title,
|
|
1020
1092
|
JSON.stringify(event.tags ?? []),
|
|
1093
|
+
JSON.stringify(event.topicIds ?? []),
|
|
1021
1094
|
dueInstant(event.due) ?? null,
|
|
1022
1095
|
sequence,
|
|
1023
1096
|
event.createdAt,
|
|
@@ -1084,6 +1157,8 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
1084
1157
|
if (input.actorId) add('actor_id = ?', input.actorId);
|
|
1085
1158
|
if (input.actorKind) add('actor_kind = ?', input.actorKind);
|
|
1086
1159
|
if (input.tag) add('EXISTS (SELECT 1 FROM json_each(tags_json) WHERE value = ?)', input.tag);
|
|
1160
|
+
if (input.topicId)
|
|
1161
|
+
add('EXISTS (SELECT 1 FROM json_each(topic_ids_json) WHERE value = ?)', input.topicId);
|
|
1087
1162
|
if (input.status) add('status = ?', input.status);
|
|
1088
1163
|
if (input.visibility) add('visibility = ?', input.visibility);
|
|
1089
1164
|
if (input.revoked !== undefined) {
|
|
@@ -1269,6 +1344,8 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
1269
1344
|
if (input.actorId) add('actor_id = ?', input.actorId);
|
|
1270
1345
|
if (input.actorKind) add('actor_kind = ?', input.actorKind);
|
|
1271
1346
|
if (input.tag) add('EXISTS (SELECT 1 FROM json_each(tags_json) WHERE value = ?)', input.tag);
|
|
1347
|
+
if (input.topicId)
|
|
1348
|
+
add('EXISTS (SELECT 1 FROM json_each(topic_ids_json) WHERE value = ?)', input.topicId);
|
|
1272
1349
|
if (input.status) add('status = ?', input.status);
|
|
1273
1350
|
if (input.pending) {
|
|
1274
1351
|
add(`((kind = 'kudos' AND status = 'unacknowledged') OR
|
|
@@ -1687,6 +1764,8 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
1687
1764
|
const supportedTypes = new Set([
|
|
1688
1765
|
'agent.created',
|
|
1689
1766
|
'agent.updated',
|
|
1767
|
+
'topic.created',
|
|
1768
|
+
'topic.updated',
|
|
1690
1769
|
'kudos.given',
|
|
1691
1770
|
'kudos.acknowledged',
|
|
1692
1771
|
'kudos.revoked',
|
|
@@ -1830,6 +1909,81 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
1830
1909
|
return candidates.length === 1 ? { match: candidates[0]!, candidates } : { candidates };
|
|
1831
1910
|
}
|
|
1832
1911
|
|
|
1912
|
+
/* -------------------------------------------------------------------- topics */
|
|
1913
|
+
|
|
1914
|
+
insertTopic(topic: Topic): void {
|
|
1915
|
+
const parsed = topicProfileSchema.parse(topic);
|
|
1916
|
+
this.db()
|
|
1917
|
+
.prepare(
|
|
1918
|
+
`INSERT INTO topics(id, display_name, status, created_at, updated_at)
|
|
1919
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
1920
|
+
)
|
|
1921
|
+
.run(parsed.id, parsed.displayName, parsed.status, parsed.createdAt, parsed.createdAt);
|
|
1922
|
+
this.insertTopicAliases(parsed.id, parsed.aliases ?? []);
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
updateTopic(topic: Topic, updatedAt: string): void {
|
|
1926
|
+
const parsed = topicProfileSchema.parse(topic);
|
|
1927
|
+
this.db()
|
|
1928
|
+
.prepare('UPDATE topics SET display_name = ?, status = ?, updated_at = ? WHERE id = ?')
|
|
1929
|
+
.run(parsed.displayName, parsed.status, updatedAt, parsed.id);
|
|
1930
|
+
this.db().prepare('DELETE FROM topic_aliases WHERE topic_id = ?').run(parsed.id);
|
|
1931
|
+
this.insertTopicAliases(parsed.id, parsed.aliases ?? []);
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
getTopic(idOrAlias: string): Topic | undefined {
|
|
1935
|
+
return this.resolveTopic(idOrAlias).match;
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
listTopics(status?: 'active' | 'archived'): Topic[] {
|
|
1939
|
+
const rows = (status
|
|
1940
|
+
? this.db()
|
|
1941
|
+
.prepare('SELECT * FROM topics WHERE status = ? ORDER BY display_name ASC')
|
|
1942
|
+
.all(status)
|
|
1943
|
+
: this.db()
|
|
1944
|
+
.prepare('SELECT * FROM topics ORDER BY display_name ASC')
|
|
1945
|
+
.all()) as unknown as TopicRow[];
|
|
1946
|
+
return rows.map((row) => this.topicFromRow(row));
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
/** Resolves a name case-insensitively, reporting ambiguity instead of guessing. */
|
|
1950
|
+
resolveTopic(query: string): { match?: Topic; candidates: Topic[] } {
|
|
1951
|
+
const normalized = query.trim().toLowerCase();
|
|
1952
|
+
const rows = this.db()
|
|
1953
|
+
.prepare(
|
|
1954
|
+
`SELECT DISTINCT t.*
|
|
1955
|
+
FROM topics t
|
|
1956
|
+
LEFT JOIN topic_aliases x ON x.topic_id = t.id
|
|
1957
|
+
WHERE lower(t.id) = ? OR lower(t.display_name) = ? OR x.alias = ?
|
|
1958
|
+
ORDER BY t.id ASC`,
|
|
1959
|
+
)
|
|
1960
|
+
.all(normalized, normalized, normalized) as unknown as TopicRow[];
|
|
1961
|
+
const candidates = rows.map((row) => this.topicFromRow(row));
|
|
1962
|
+
return candidates.length === 1 ? { match: candidates[0]!, candidates } : { candidates };
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
private topicFromRow(row: TopicRow): Topic {
|
|
1966
|
+
const aliasRows = this.db()
|
|
1967
|
+
.prepare('SELECT alias FROM topic_aliases WHERE topic_id = ? ORDER BY alias ASC')
|
|
1968
|
+
.all(row.id) as unknown as Array<{ alias: string }>;
|
|
1969
|
+
const aliases = aliasRows.map((aliasRow) => aliasRow.alias);
|
|
1970
|
+
return {
|
|
1971
|
+
id: row.id,
|
|
1972
|
+
displayName: row.display_name,
|
|
1973
|
+
...(aliases.length ? { aliases } : {}),
|
|
1974
|
+
status: row.status,
|
|
1975
|
+
createdAt: row.created_at,
|
|
1976
|
+
};
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
private insertTopicAliases(topicId: string, aliases: string[]): void {
|
|
1980
|
+
for (const alias of aliases) {
|
|
1981
|
+
this.db()
|
|
1982
|
+
.prepare('INSERT INTO topic_aliases(alias, topic_id) VALUES (?, ?)')
|
|
1983
|
+
.run(alias.trim().toLowerCase(), topicId);
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1833
1987
|
/* -------------------------------------------------------- post acknowledgment */
|
|
1834
1988
|
|
|
1835
1989
|
listPostAcknowledgments(postId: string): PostAcknowledgment[] {
|