@wirecat/cli-core 0.419.0 → 0.420.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/CHANGELOG.md +36 -0
- package/dist/domain/providers.d.ts +20 -2
- package/dist/domain/providers.js +23 -3
- package/dist/domain/tag-consolidation.d.ts +30 -1
- package/dist/entities/index.d.ts +4 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/meetings/calendar-text.d.ts +13 -0
- package/dist/meetings/calendar-text.js +57 -0
- package/dist/meetings/event-store.d.ts +202 -0
- package/dist/meetings/event-store.js +1 -0
- package/dist/meetings/events.js +5 -1
- package/dist/meetings/index.d.ts +3 -0
- package/dist/meetings/index.js +2 -0
- package/dist/meetings/join-key.d.ts +7 -0
- package/dist/meetings/join-key.js +38 -0
- package/dist/meetings/model.d.ts +8 -2
- package/dist/meetings/read-answers.d.ts +6 -2
- package/dist/meetings/read-answers.js +9 -2
- package/dist/meetings/store.d.ts +2 -0
- package/dist/meetings/testing/event-store-contract.d.ts +10 -0
- package/dist/meetings/testing/event-store-contract.js +161 -0
- package/dist/meetings/testing/index.d.ts +1 -0
- package/dist/meetings/testing/index.js +1 -0
- package/dist/search/account-list.js +39 -7
- package/dist/services/agenda.js +5 -5
- package/dist/services/document-source.d.ts +7 -3
- package/dist/services/document-source.js +24 -12
- package/dist/services/documents/ingestion.d.ts +6 -1
- package/dist/services/documents/ingestion.js +4 -0
- package/dist/services/notes-search.js +3 -2
- package/dist/services/search-all.js +1 -0
- package/dist/services/task-tag-filters.d.ts +3 -0
- package/dist/services/task-tag-filters.js +8 -0
- package/dist/services/tasks.js +5 -9
- package/dist/store/document-transfer/index.js +12 -2
- package/dist/store/index.d.ts +2 -1
- package/dist/store/sqlite/accounts.js +1 -0
- package/dist/store/sqlite/calendar.d.ts +3 -0
- package/dist/store/sqlite/calendar.js +593 -0
- package/dist/store/sqlite/catalog.js +16 -5
- package/dist/store/sqlite/chat-rekey.js +1 -1
- package/dist/store/sqlite/chats.js +2 -2
- package/dist/store/sqlite/context-document-source.js +8 -4
- package/dist/store/sqlite/document-transfer/apply.js +24 -2
- package/dist/store/sqlite/document-transfer/collection.d.ts +19 -0
- package/dist/store/sqlite/document-transfer/collection.js +66 -0
- package/dist/store/sqlite/document-transfer/plan.js +15 -8
- package/dist/store/sqlite/document-transfer/snapshot.js +12 -7
- package/dist/store/sqlite/entity-types.d.ts +5 -1
- package/dist/store/sqlite/entity-types.js +5 -1
- package/dist/store/sqlite/events.d.ts +8 -2
- package/dist/store/sqlite/events.js +36 -12
- package/dist/store/sqlite/files.d.ts +93 -0
- package/dist/store/sqlite/files.js +293 -0
- package/dist/store/sqlite/folded.d.ts +3 -0
- package/dist/store/sqlite/folded.js +1 -0
- package/dist/store/sqlite/imported-documents.js +1 -1
- package/dist/store/sqlite/join-keys.d.ts +8 -0
- package/dist/store/sqlite/join-keys.js +27 -0
- package/dist/store/sqlite/knowledge.js +6 -6
- package/dist/store/sqlite/link-reviews.d.ts +9 -0
- package/dist/store/sqlite/link-reviews.js +12 -0
- package/dist/store/sqlite/lucene-fields.js +8 -6
- package/dist/store/sqlite/manifest.js +11 -0
- package/dist/store/sqlite/meeting-selection.js +1 -1
- package/dist/store/sqlite/meetings.js +6 -1
- package/dist/store/sqlite/migrations.generated.js +143 -0
- package/dist/store/sqlite/note-index.d.ts +8 -1
- package/dist/store/sqlite/note-index.js +14 -0
- package/dist/store/sqlite/note-search.js +6 -5
- package/dist/store/sqlite/notes.d.ts +18 -1
- package/dist/store/sqlite/notes.js +237 -40
- package/dist/store/sqlite/open-store.js +4 -0
- package/dist/store/sqlite/person-links.js +7 -2
- package/dist/store/sqlite/schema/canonical-tags.d.ts +87 -0
- package/dist/store/sqlite/schema/canonical-tags.js +8 -0
- package/dist/store/sqlite/schema/tag-consolidations.d.ts +270 -0
- package/dist/store/sqlite/schema/tag-consolidations.js +34 -0
- package/dist/store/sqlite/schema.d.ts +3589 -1634
- package/dist/store/sqlite/schema.js +191 -13
- package/dist/store/sqlite/tag-consolidation-apply.d.ts +3 -0
- package/dist/store/sqlite/tag-consolidation-apply.js +88 -0
- package/dist/store/sqlite/tag-consolidation-input.d.ts +16 -0
- package/dist/store/sqlite/tag-consolidation-input.js +52 -0
- package/dist/store/sqlite/tag-consolidation.js +51 -66
- package/dist/store/sqlite/tag-creation.js +17 -8
- package/dist/store/sqlite/tag-resolution.d.ts +5 -0
- package/dist/store/sqlite/tag-resolution.js +33 -0
- package/dist/store/sqlite/tags.d.ts +3 -1
- package/dist/store/sqlite/tags.js +70 -50
- package/dist/store/sqlite/tasks.d.ts +2 -0
- package/dist/store/sqlite/tasks.js +6 -1
- package/dist/store/sqlite/things.js +2 -1
- package/dist/store/sqlite/vocabulary.d.ts +1 -0
- package/dist/store/sqlite/vocabulary.js +30 -8
- package/dist/store/store.d.ts +6 -0
- package/dist/store/store.js +10 -0
- package/dist/store/tags.js +2 -2
- package/package.json +2 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { blob, desc, index, integer, primaryKey, real, sql, sqliteTable, text, unique, uniqueIndex, } from "./drizzle/core.js";
|
|
2
|
+
import { canonicalTags } from "./schema/canonical-tags.js";
|
|
2
3
|
/**
|
|
3
4
|
* The store's base tables — what `drizzle-kit generate` diffs against. Every column is explained in
|
|
4
5
|
* `docs/storage/schema.md`; the two must agree. A change here becomes a migration, under the rules at the
|
|
@@ -98,7 +99,11 @@ export const identities = sqliteTable("identities", {
|
|
|
98
99
|
createdAt: integer("created_at").notNull(),
|
|
99
100
|
updatedAt: integer("updated_at").notNull(),
|
|
100
101
|
description: text("description"),
|
|
101
|
-
|
|
102
|
+
email: text("email"),
|
|
103
|
+
}, (table) => [
|
|
104
|
+
unique().on(table.provider, table.externalId),
|
|
105
|
+
index("identities_by_email").on(table.email).where(sql `email IS NOT NULL`),
|
|
106
|
+
]);
|
|
102
107
|
export const identityLinks = sqliteTable("identity_links", {
|
|
103
108
|
identityId: integer("identity_id")
|
|
104
109
|
.primaryKey()
|
|
@@ -751,7 +756,38 @@ export const documents = sqliteTable("documents", {
|
|
|
751
756
|
createdAt: integer("created_at").notNull(),
|
|
752
757
|
updatedAt: integer("updated_at").notNull(),
|
|
753
758
|
deletedAt: integer("deleted_at"),
|
|
759
|
+
pinnedFileId: integer("pinned_file_id"),
|
|
754
760
|
}, (table) => [unique().on(table.accountId, table.externalId)]);
|
|
761
|
+
export const files = sqliteTable("files", {
|
|
762
|
+
id: integer("id").primaryKey(),
|
|
763
|
+
accountId: integer("account_id")
|
|
764
|
+
.notNull()
|
|
765
|
+
.references(() => accounts.id),
|
|
766
|
+
externalId: text("external_id").notNull(),
|
|
767
|
+
kind: text("kind").notNull(),
|
|
768
|
+
name: text("name").notNull(),
|
|
769
|
+
path: text("path").notNull(),
|
|
770
|
+
pathFolded: text("path_folded"),
|
|
771
|
+
parentId: integer("parent_id").references(() => files.id),
|
|
772
|
+
documentId: integer("document_id").references(() => documents.id),
|
|
773
|
+
mime: text("mime"),
|
|
774
|
+
size: integer("size"),
|
|
775
|
+
contentHash: text("content_hash"),
|
|
776
|
+
url: text("url"),
|
|
777
|
+
externalCreatedAt: integer("external_created_at"),
|
|
778
|
+
externalUpdatedAt: integer("external_updated_at"),
|
|
779
|
+
metadata: text("metadata"),
|
|
780
|
+
seenAt: integer("seen_at"),
|
|
781
|
+
createdAt: integer("created_at").notNull(),
|
|
782
|
+
updatedAt: integer("updated_at").notNull(),
|
|
783
|
+
deletedAt: integer("deleted_at"),
|
|
784
|
+
}, (table) => [
|
|
785
|
+
unique().on(table.accountId, table.externalId),
|
|
786
|
+
index("files_by_account_id_path_folded").on(table.accountId, table.pathFolded),
|
|
787
|
+
index("files_path_to_fold").on(table.id).where(sql `path_folded IS NULL`),
|
|
788
|
+
index("files_by_parent_id").on(table.parentId),
|
|
789
|
+
index("files_by_document_id").on(table.documentId),
|
|
790
|
+
]);
|
|
755
791
|
export const documentRevisions = sqliteTable("document_revisions", {
|
|
756
792
|
id: integer("id").primaryKey(),
|
|
757
793
|
documentId: integer("document_id")
|
|
@@ -817,6 +853,113 @@ export const memoryIndexPending = sqliteTable("memory_index_pending", {
|
|
|
817
853
|
id: integer("id").notNull(),
|
|
818
854
|
indexableType: text("indexable_type").notNull(),
|
|
819
855
|
}, (table) => [primaryKey({ columns: [table.indexableType, table.id] })]);
|
|
856
|
+
export const calendars = sqliteTable("calendars", {
|
|
857
|
+
id: integer("id").primaryKey(),
|
|
858
|
+
accountId: integer("account_id")
|
|
859
|
+
.notNull()
|
|
860
|
+
.references(() => accounts.id),
|
|
861
|
+
externalId: text("external_id").notNull(),
|
|
862
|
+
title: text("title"),
|
|
863
|
+
timezone: text("timezone"),
|
|
864
|
+
accessRole: text("access_role"),
|
|
865
|
+
primaryCalendar: integer("primary_calendar").notNull(),
|
|
866
|
+
synced: integer("synced").notNull(),
|
|
867
|
+
seenAt: integer("seen_at"),
|
|
868
|
+
createdAt: integer("created_at").notNull(),
|
|
869
|
+
updatedAt: integer("updated_at").notNull(),
|
|
870
|
+
deletedAt: integer("deleted_at"),
|
|
871
|
+
}, (table) => [unique().on(table.accountId, table.externalId)]);
|
|
872
|
+
export const calendarEntries = sqliteTable("calendar_entries", {
|
|
873
|
+
id: integer("id").primaryKey(),
|
|
874
|
+
calendarId: integer("calendar_id")
|
|
875
|
+
.notNull()
|
|
876
|
+
.references(() => calendars.id),
|
|
877
|
+
kind: text("kind").notNull(),
|
|
878
|
+
seriesEntryId: integer("series_entry_id").references(() => calendarEntries.id),
|
|
879
|
+
eventId: integer("event_id").references(() => events.id),
|
|
880
|
+
eventSeriesId: integer("event_series_id").references(() => eventSeries.id),
|
|
881
|
+
externalId: text("external_id").notNull(),
|
|
882
|
+
icalUid: text("ical_uid"),
|
|
883
|
+
originalStartsAt: integer("original_starts_at"),
|
|
884
|
+
exception: integer("exception").notNull(),
|
|
885
|
+
status: text("status").notNull(),
|
|
886
|
+
title: text("title"),
|
|
887
|
+
description: text("description"),
|
|
888
|
+
location: text("location"),
|
|
889
|
+
startsAt: integer("starts_at"),
|
|
890
|
+
endsAt: integer("ends_at"),
|
|
891
|
+
allDay: integer("all_day").notNull(),
|
|
892
|
+
startDate: text("start_date"),
|
|
893
|
+
endDate: text("end_date"),
|
|
894
|
+
timezone: text("timezone"),
|
|
895
|
+
recurrence: text("recurrence"),
|
|
896
|
+
recurrenceEndsAt: integer("recurrence_ends_at"),
|
|
897
|
+
eventType: text("event_type"),
|
|
898
|
+
transparency: text("transparency"),
|
|
899
|
+
visibility: text("visibility"),
|
|
900
|
+
ownResponse: text("own_response"),
|
|
901
|
+
organizerIdentityId: integer("organizer_identity_id").references(() => identities.id),
|
|
902
|
+
joinUrl: text("join_url"),
|
|
903
|
+
joinKey: text("join_key"),
|
|
904
|
+
conference: text("conference"),
|
|
905
|
+
url: text("url"),
|
|
906
|
+
sequence: integer("sequence"),
|
|
907
|
+
etag: text("etag"),
|
|
908
|
+
externalUpdatedAt: integer("external_updated_at"),
|
|
909
|
+
metadata: text("metadata"),
|
|
910
|
+
seenAt: integer("seen_at"),
|
|
911
|
+
createdAt: integer("created_at").notNull(),
|
|
912
|
+
updatedAt: integer("updated_at").notNull(),
|
|
913
|
+
deletedAt: integer("deleted_at"),
|
|
914
|
+
}, (table) => [
|
|
915
|
+
unique().on(table.calendarId, table.externalId),
|
|
916
|
+
unique().on(table.seriesEntryId, table.originalStartsAt),
|
|
917
|
+
index("calendar_entries_by_calendar_id_starts_at").on(table.calendarId, table.startsAt),
|
|
918
|
+
index("calendar_entries_by_ical_uid").on(table.icalUid).where(sql `ical_uid IS NOT NULL`),
|
|
919
|
+
index("calendar_entries_by_join_key").on(table.joinKey).where(sql `join_key IS NOT NULL`),
|
|
920
|
+
index("calendar_entries_by_event_id").on(table.eventId),
|
|
921
|
+
index("calendar_entries_by_event_series_id").on(table.eventSeriesId),
|
|
922
|
+
index("calendar_entries_by_event_type_event_id").on(table.eventType, table.eventId),
|
|
923
|
+
index("calendar_entries_by_organizer_identity_id").on(table.organizerIdentityId),
|
|
924
|
+
]);
|
|
925
|
+
export const calendarEntryRevisions = sqliteTable("calendar_entry_revisions", {
|
|
926
|
+
id: integer("id").primaryKey(),
|
|
927
|
+
entryId: integer("entry_id")
|
|
928
|
+
.notNull()
|
|
929
|
+
.references(() => calendarEntries.id),
|
|
930
|
+
title: text("title"),
|
|
931
|
+
startsAt: integer("starts_at"),
|
|
932
|
+
endsAt: integer("ends_at"),
|
|
933
|
+
location: text("location"),
|
|
934
|
+
status: text("status"),
|
|
935
|
+
recurrence: text("recurrence"),
|
|
936
|
+
recurrenceEndsAt: integer("recurrence_ends_at"),
|
|
937
|
+
externalUpdatedAt: integer("external_updated_at"),
|
|
938
|
+
createdAt: integer("created_at").notNull(),
|
|
939
|
+
}, (table) => [index("calendar_entry_revisions_by_entry_id_created_at").on(table.entryId, table.createdAt)]);
|
|
940
|
+
export const calendarEntryParticipants = sqliteTable("calendar_entry_participants", {
|
|
941
|
+
id: integer("id").primaryKey(),
|
|
942
|
+
entryId: integer("entry_id")
|
|
943
|
+
.notNull()
|
|
944
|
+
.references(() => calendarEntries.id),
|
|
945
|
+
identityId: integer("identity_id").references(() => identities.id),
|
|
946
|
+
email: text("email").notNull(),
|
|
947
|
+
name: text("name"),
|
|
948
|
+
response: text("response"),
|
|
949
|
+
optional: integer("optional").notNull(),
|
|
950
|
+
organizer: integer("organizer").notNull(),
|
|
951
|
+
self: integer("self").notNull(),
|
|
952
|
+
resource: integer("resource").notNull(),
|
|
953
|
+
createdAt: integer("created_at").notNull(),
|
|
954
|
+
updatedAt: integer("updated_at").notNull(),
|
|
955
|
+
}, (table) => [
|
|
956
|
+
unique().on(table.entryId, table.email),
|
|
957
|
+
index("calendar_entry_participants_by_identity_id").on(table.identityId),
|
|
958
|
+
]);
|
|
959
|
+
export const eventIndexPending = sqliteTable("event_index_pending", {
|
|
960
|
+
id: integer("id").notNull(),
|
|
961
|
+
indexableType: text("indexable_type").notNull(),
|
|
962
|
+
}, (table) => [primaryKey({ columns: [table.indexableType, table.id] })]);
|
|
820
963
|
export const eventSeries = sqliteTable("event_series", {
|
|
821
964
|
id: integer("id").primaryKey(),
|
|
822
965
|
title: text("title"),
|
|
@@ -838,6 +981,9 @@ export const events = sqliteTable("events", {
|
|
|
838
981
|
createdAt: integer("created_at").notNull(),
|
|
839
982
|
updatedAt: integer("updated_at").notNull(),
|
|
840
983
|
deletedAt: integer("deleted_at"),
|
|
984
|
+
status: text("status"),
|
|
985
|
+
allDay: integer("all_day"),
|
|
986
|
+
sourceDeletedAt: integer("source_deleted_at"),
|
|
841
987
|
}, (table) => [index("events_by_time").on(table.startsAt), index("events_by_event_series_id").on(table.eventSeriesId)]);
|
|
842
988
|
export const meetingSeries = sqliteTable("meeting_series", {
|
|
843
989
|
id: integer("id").primaryKey(),
|
|
@@ -883,10 +1029,12 @@ export const meetings = sqliteTable("meetings", {
|
|
|
883
1029
|
createdAt: integer("created_at").notNull(),
|
|
884
1030
|
updatedAt: integer("updated_at").notNull(),
|
|
885
1031
|
deletedAt: integer("deleted_at"),
|
|
886
|
-
/** `fold(title)`: SQLite's lower() folds ASCII only, so filters compare this. */
|
|
887
1032
|
titleFolded: text("title_folded"),
|
|
1033
|
+
joinKey: text("join_key"),
|
|
888
1034
|
}, (table) => [
|
|
889
1035
|
unique().on(table.accountId, table.externalId),
|
|
1036
|
+
index("meetings_by_join_key").on(table.joinKey).where(sql `join_key IS NOT NULL`),
|
|
1037
|
+
index("meetings_join_key_to_fill").on(table.id).where(sql `join_url IS NOT NULL AND join_key IS NULL`),
|
|
890
1038
|
index("meetings_by_time").on(table.accountId, desc(table.startedAt)),
|
|
891
1039
|
index("meetings_by_account_id_title_folded").on(table.accountId, table.titleFolded),
|
|
892
1040
|
index("meetings_title_to_fold").on(table.id).where(sql `title IS NOT NULL AND title_folded IS NULL`),
|
|
@@ -1252,26 +1400,23 @@ export const agentActions = sqliteTable("agent_actions", {
|
|
|
1252
1400
|
index("agent_actions_by_actor_type_actor_id").on(table.actorType, table.actorId),
|
|
1253
1401
|
index("agent_actions_by_target_type_target_id").on(table.targetType, table.targetId),
|
|
1254
1402
|
]);
|
|
1255
|
-
export
|
|
1256
|
-
|
|
1257
|
-
name: text("name").notNull().unique(),
|
|
1258
|
-
kind: text("kind").notNull().default("tag"),
|
|
1259
|
-
createdAt: integer("created_at").notNull(),
|
|
1260
|
-
updatedAt: integer("updated_at").notNull(),
|
|
1261
|
-
});
|
|
1262
|
-
export const autoTagClaims = sqliteTable("auto_tag_claims", {
|
|
1403
|
+
export { canonicalTags } from "./schema/canonical-tags.js";
|
|
1404
|
+
export const canonicalAutoTagClaims = sqliteTable("canonical_auto_tag_claims", {
|
|
1263
1405
|
chatId: integer("chat_id")
|
|
1264
1406
|
.notNull()
|
|
1265
1407
|
.references(() => chats.id),
|
|
1266
1408
|
tagId: integer("tag_id")
|
|
1267
1409
|
.notNull()
|
|
1268
|
-
.references(() =>
|
|
1410
|
+
.references(() => canonicalTags.id),
|
|
1269
1411
|
algorithm: text("algorithm").notNull(),
|
|
1270
1412
|
score: real("score").notNull(),
|
|
1271
1413
|
fields: text("fields").notNull(),
|
|
1272
1414
|
createdAt: integer("created_at").notNull(),
|
|
1273
1415
|
updatedAt: integer("updated_at").notNull(),
|
|
1274
|
-
}, (table) => [
|
|
1416
|
+
}, (table) => [
|
|
1417
|
+
primaryKey({ columns: [table.chatId, table.tagId] }),
|
|
1418
|
+
index("canonical_auto_tag_claims_by_tag_id").on(table.tagId),
|
|
1419
|
+
]);
|
|
1275
1420
|
export const links = sqliteTable("links", {
|
|
1276
1421
|
id: integer("id").primaryKey(),
|
|
1277
1422
|
fromType: text("from_type").notNull(),
|
|
@@ -1320,7 +1465,7 @@ export const taggings = sqliteTable("taggings", {
|
|
|
1320
1465
|
id: integer("id").primaryKey(),
|
|
1321
1466
|
tagId: integer("tag_id")
|
|
1322
1467
|
.notNull()
|
|
1323
|
-
.references(() =>
|
|
1468
|
+
.references(() => canonicalTags.id),
|
|
1324
1469
|
taggableType: text("taggable_type").notNull(),
|
|
1325
1470
|
taggableId: integer("taggable_id").notNull(),
|
|
1326
1471
|
main: integer("main").notNull().default(0),
|
|
@@ -1334,6 +1479,39 @@ export const taggings = sqliteTable("taggings", {
|
|
|
1334
1479
|
uniqueIndex("taggings_main_topic").on(table.taggableType, table.taggableId).where(sql `main = 1`),
|
|
1335
1480
|
index("taggings_by_author_type_author_id").on(table.authorType, table.authorId),
|
|
1336
1481
|
]);
|
|
1482
|
+
export { tagConsolidations, tagRedirects } from "./schema/tag-consolidations.js";
|
|
1483
|
+
export const entityLinkEvents = sqliteTable("entity_link_events", {
|
|
1484
|
+
id: integer("id").primaryKey(),
|
|
1485
|
+
linkableType: text("linkable_type").notNull(),
|
|
1486
|
+
linkableId: integer("linkable_id").notNull(),
|
|
1487
|
+
entityType: text("entity_type").notNull(),
|
|
1488
|
+
fromEntityId: integer("from_entity_id"),
|
|
1489
|
+
toEntityId: integer("to_entity_id"),
|
|
1490
|
+
method: text("method").notNull(),
|
|
1491
|
+
confidence: real("confidence"),
|
|
1492
|
+
author: text("author").notNull(),
|
|
1493
|
+
createdAt: integer("created_at").notNull(),
|
|
1494
|
+
}, (table) => [
|
|
1495
|
+
index("entity_link_events_by_linkable_type_linkable_id").on(table.linkableType, table.linkableId),
|
|
1496
|
+
index("entity_link_events_by_entity_type_to_entity_id").on(table.entityType, table.toEntityId),
|
|
1497
|
+
]);
|
|
1498
|
+
export const linkReviews = sqliteTable("link_reviews", {
|
|
1499
|
+
id: integer("id").primaryKey(),
|
|
1500
|
+
linkableType: text("linkable_type").notNull(),
|
|
1501
|
+
linkableId: integer("linkable_id").notNull(),
|
|
1502
|
+
entityType: text("entity_type").notNull(),
|
|
1503
|
+
candidates: text("candidates").notNull(),
|
|
1504
|
+
reason: text("reason").notNull(),
|
|
1505
|
+
status: text("status").notNull(),
|
|
1506
|
+
chosenId: integer("chosen_id"),
|
|
1507
|
+
decidedBy: text("decided_by"),
|
|
1508
|
+
decidedAt: integer("decided_at"),
|
|
1509
|
+
createdAt: integer("created_at").notNull(),
|
|
1510
|
+
updatedAt: integer("updated_at").notNull(),
|
|
1511
|
+
}, (table) => [
|
|
1512
|
+
unique().on(table.linkableType, table.linkableId, table.entityType),
|
|
1513
|
+
index("link_reviews_by_status_created_at").on(table.status, table.createdAt),
|
|
1514
|
+
]);
|
|
1337
1515
|
export const conversations = sqliteTable("conversations", {
|
|
1338
1516
|
id: integer("id").primaryKey(),
|
|
1339
1517
|
chatId: integer("chat_id")
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { TagConsolidationApplyOptions, TagConsolidationOptions, TagConsolidationPreview, TagConsolidationReceipt } from "../../domain/tag-consolidation.js";
|
|
2
|
+
import type { StoreContext } from "./open.js";
|
|
3
|
+
export declare const applyTagConsolidation: (context: StoreContext, preview: (options: TagConsolidationOptions) => TagConsolidationPreview, options: TagConsolidationApplyOptions) => TagConsolidationReceipt;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { CliError } from "@wirecat/cli-base";
|
|
3
|
+
import { createTagRedirectIndex } from "../../domain/tag-redirects.js";
|
|
4
|
+
import { ownerPerson } from "./actors.js";
|
|
5
|
+
import { consolidationApplyInput } from "./tag-consolidation-input.js";
|
|
6
|
+
import { tagRedirectSnapshot } from "./tag-resolution.js";
|
|
7
|
+
import { write } from "./transaction.js";
|
|
8
|
+
const receiptOf = (row) => ({
|
|
9
|
+
id: String(row.id),
|
|
10
|
+
sourceId: String(row.source_id),
|
|
11
|
+
survivorId: String(row.survivor_id),
|
|
12
|
+
sourceMemberIds: JSON.parse(String(row.source_member_ids)),
|
|
13
|
+
survivorMemberIds: JSON.parse(String(row.survivor_member_ids)),
|
|
14
|
+
actor: { type: "person", id: String(row.actor_id) },
|
|
15
|
+
createdAt: new Date(Number(row.created_at)).toISOString(),
|
|
16
|
+
idempotencyKey: String(row.idempotency_key),
|
|
17
|
+
vocabularyFingerprint: String(row.vocabulary_fingerprint),
|
|
18
|
+
previewFingerprint: String(row.preview_fingerprint),
|
|
19
|
+
counts: JSON.parse(String(row.counts)),
|
|
20
|
+
});
|
|
21
|
+
export const applyTagConsolidation = (context, preview, options) => {
|
|
22
|
+
const { source, survivor, maxRows, maxReadBytes } = consolidationApplyInput(options);
|
|
23
|
+
const { database, now } = context;
|
|
24
|
+
const check = () => {
|
|
25
|
+
if (options.signal?.aborted)
|
|
26
|
+
throw new CliError("cancelled", "tag consolidation was cancelled");
|
|
27
|
+
};
|
|
28
|
+
check();
|
|
29
|
+
return write(database, () => {
|
|
30
|
+
const owner = ownerPerson(database);
|
|
31
|
+
const request = createHash("sha256")
|
|
32
|
+
.update(JSON.stringify([
|
|
33
|
+
source,
|
|
34
|
+
survivor,
|
|
35
|
+
owner.type,
|
|
36
|
+
owner.id,
|
|
37
|
+
options.expectedVocabularyFingerprint,
|
|
38
|
+
options.expectedPreviewFingerprint,
|
|
39
|
+
]))
|
|
40
|
+
.digest("hex");
|
|
41
|
+
const held = database
|
|
42
|
+
.prepare("SELECT * FROM tag_consolidations WHERE idempotency_key=?")
|
|
43
|
+
.get(options.idempotencyKey);
|
|
44
|
+
if (held) {
|
|
45
|
+
if (held.request_fingerprint !== request)
|
|
46
|
+
throw new CliError("configuration_error", "idempotencyKey already records another reviewed tag consolidation");
|
|
47
|
+
return receiptOf(held);
|
|
48
|
+
}
|
|
49
|
+
const selected = preview({
|
|
50
|
+
sourceId: options.sourceId,
|
|
51
|
+
survivorId: options.survivorId,
|
|
52
|
+
expectedVocabularyFingerprint: options.expectedVocabularyFingerprint,
|
|
53
|
+
maxRows,
|
|
54
|
+
maxReadBytes,
|
|
55
|
+
sampleLimit: 1,
|
|
56
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
57
|
+
});
|
|
58
|
+
if (selected.previewFingerprint !== options.expectedPreviewFingerprint)
|
|
59
|
+
throw new CliError("configuration_error", "tag dependencies changed since the consolidation preview", {
|
|
60
|
+
reason: "vocabulary.stale",
|
|
61
|
+
});
|
|
62
|
+
const graph = tagRedirectSnapshot(database);
|
|
63
|
+
createTagRedirectIndex({
|
|
64
|
+
...graph,
|
|
65
|
+
redirects: [
|
|
66
|
+
...graph.redirects.map((row) => row.survivorId === String(source) ? { ...row, survivorId: String(survivor) } : row),
|
|
67
|
+
{ sourceId: String(source), survivorId: String(survivor) },
|
|
68
|
+
],
|
|
69
|
+
});
|
|
70
|
+
const at = now();
|
|
71
|
+
const counts = {
|
|
72
|
+
assignments: selected.assignments.source + selected.assignments.survivor,
|
|
73
|
+
aliases: selected.aliases.source + selected.aliases.survivor,
|
|
74
|
+
automaticClaims: selected.automaticClaims.source + selected.automaticClaims.survivor,
|
|
75
|
+
links: selected.links.total,
|
|
76
|
+
};
|
|
77
|
+
check();
|
|
78
|
+
database.prepare("UPDATE tag_redirects SET survivor_id=? WHERE survivor_id=?").run(survivor, source);
|
|
79
|
+
database
|
|
80
|
+
.prepare("INSERT INTO tag_redirects(source_id,survivor_id,created_at) VALUES(?,?,?)")
|
|
81
|
+
.run(source, survivor, at);
|
|
82
|
+
tagRedirectSnapshot(database);
|
|
83
|
+
const row = database
|
|
84
|
+
.prepare("INSERT INTO tag_consolidations(source_id,survivor_id,source_member_ids,survivor_member_ids,actor_type,actor_id,created_at,idempotency_key,vocabulary_fingerprint,preview_fingerprint,counts,request_fingerprint) VALUES(?,?,?,?,?,?,?,?,?,?,?,?) RETURNING *")
|
|
85
|
+
.get(source, survivor, JSON.stringify(selected.sourceMemberIds), JSON.stringify(selected.survivorMemberIds), owner.type, owner.id, at, options.idempotencyKey, selected.vocabularyFingerprint, selected.previewFingerprint, JSON.stringify(counts), request);
|
|
86
|
+
return receiptOf(row);
|
|
87
|
+
}, { beforeCommit: check });
|
|
88
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { TagConsolidationApplyOptions, TagConsolidationOptions } from "../../domain/tag-consolidation.js";
|
|
2
|
+
export declare const consolidationId: (value: unknown, field: string) => number;
|
|
3
|
+
export declare const consolidationInput: (options: TagConsolidationOptions, apply?: boolean) => {
|
|
4
|
+
source: number;
|
|
5
|
+
survivor: number;
|
|
6
|
+
maxRows: number;
|
|
7
|
+
maxReadBytes: number;
|
|
8
|
+
limit: number;
|
|
9
|
+
};
|
|
10
|
+
export declare const consolidationApplyInput: (options: TagConsolidationApplyOptions) => {
|
|
11
|
+
source: number;
|
|
12
|
+
survivor: number;
|
|
13
|
+
maxRows: number;
|
|
14
|
+
maxReadBytes: number;
|
|
15
|
+
limit: number;
|
|
16
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
export const consolidationId = (value, field) => {
|
|
3
|
+
if (typeof value !== "string" || !/^[1-9]\d*$/.test(value) || !Number.isSafeInteger(Number(value)))
|
|
4
|
+
throw new CliError("validation_error", `${field} must be a positive canonical tag id`);
|
|
5
|
+
return Number(value);
|
|
6
|
+
};
|
|
7
|
+
const bounded = (value, max, field) => {
|
|
8
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > max)
|
|
9
|
+
throw new CliError("validation_error", `${field} must be an integer from 1 to ${max}`);
|
|
10
|
+
return value;
|
|
11
|
+
};
|
|
12
|
+
export const consolidationInput = (options, apply = false) => {
|
|
13
|
+
if (!options || typeof options !== "object" || Array.isArray(options))
|
|
14
|
+
throw new CliError("validation_error", "consolidation options are required");
|
|
15
|
+
const allowed = new Set([
|
|
16
|
+
"sourceId",
|
|
17
|
+
"survivorId",
|
|
18
|
+
"expectedVocabularyFingerprint",
|
|
19
|
+
"maxRows",
|
|
20
|
+
"maxReadBytes",
|
|
21
|
+
"signal",
|
|
22
|
+
...(apply ? ["by", "expectedPreviewFingerprint", "idempotencyKey"] : ["sampleLimit"]),
|
|
23
|
+
]);
|
|
24
|
+
for (const field of Object.keys(options))
|
|
25
|
+
if (!allowed.has(field))
|
|
26
|
+
throw new CliError("validation_error", `unknown consolidation field: ${field}`);
|
|
27
|
+
const source = consolidationId(options.sourceId, "sourceId");
|
|
28
|
+
const survivor = consolidationId(options.survivorId, "survivorId");
|
|
29
|
+
if (source === survivor)
|
|
30
|
+
throw new CliError("validation_error", "sourceId and survivorId must differ");
|
|
31
|
+
if (typeof options.expectedVocabularyFingerprint !== "string" ||
|
|
32
|
+
!/^v1:all:all:[a-f0-9]{64}$/.test(options.expectedVocabularyFingerprint))
|
|
33
|
+
throw new CliError("validation_error", "expectedVocabularyFingerprint must come from a complete unfiltered vocabulary preview");
|
|
34
|
+
return {
|
|
35
|
+
source,
|
|
36
|
+
survivor,
|
|
37
|
+
maxRows: bounded(options.maxRows ?? 5000, 20000, "maxRows"),
|
|
38
|
+
maxReadBytes: bounded(options.maxReadBytes ?? 1024 * 1024, 16 * 1024 * 1024, "maxReadBytes"),
|
|
39
|
+
limit: apply ? 1 : bounded(options.sampleLimit ?? 10, 100, "sampleLimit"),
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
export const consolidationApplyInput = (options) => {
|
|
43
|
+
const input = consolidationInput(options, true);
|
|
44
|
+
if (options.by !== "owner")
|
|
45
|
+
throw new CliError("permission_error", "only the owner applies a reviewed tag consolidation");
|
|
46
|
+
if (typeof options.expectedPreviewFingerprint !== "string" ||
|
|
47
|
+
!/^tag-preview-v1:[a-f0-9]{64}$/.test(options.expectedPreviewFingerprint))
|
|
48
|
+
throw new CliError("validation_error", "expectedPreviewFingerprint must come from a tag consolidation preview");
|
|
49
|
+
if (typeof options.idempotencyKey !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/.test(options.idempotencyKey))
|
|
50
|
+
throw new CliError("validation_error", "idempotencyKey must be 1–128 ASCII identifier characters");
|
|
51
|
+
return input;
|
|
52
|
+
};
|
|
@@ -1,77 +1,59 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { CliError } from "@wirecat/cli-base";
|
|
3
|
+
import { createTagRedirectIndex } from "../../domain/tag-redirects.js";
|
|
4
|
+
import { applyTagConsolidation } from "./tag-consolidation-apply.js";
|
|
5
|
+
import { consolidationInput } from "./tag-consolidation-input.js";
|
|
6
|
+
import { tagCanonicalId, tagRedirectSnapshot } from "./tag-resolution.js";
|
|
3
7
|
import { read } from "./transaction.js";
|
|
4
8
|
const selections = [
|
|
5
|
-
["taggings", "tag_id IN (
|
|
6
|
-
["aliases", "aliasable_type='tag' AND aliasable_id IN (
|
|
7
|
-
["
|
|
8
|
-
[
|
|
9
|
+
["taggings", "tag_id IN (SELECT value FROM json_each(?))"],
|
|
10
|
+
["aliases", "aliasable_type='tag' AND aliasable_id IN (SELECT value FROM json_each(?))"],
|
|
11
|
+
["canonical_auto_tag_claims", "tag_id IN (SELECT value FROM json_each(?))"],
|
|
12
|
+
[
|
|
13
|
+
"links",
|
|
14
|
+
"(from_type='tag' AND from_id IN (SELECT value FROM json_each(?))) OR (to_type='tag' AND to_id IN (SELECT value FROM json_each(?)))",
|
|
15
|
+
],
|
|
9
16
|
];
|
|
10
|
-
const bounded = (value, max, field) => {
|
|
11
|
-
if (!Number.isSafeInteger(value) || value < 1 || value > max)
|
|
12
|
-
throw new CliError("validation_error", `${field} must be an integer from 1 to ${max}`);
|
|
13
|
-
return value;
|
|
14
|
-
};
|
|
15
|
-
const idOf = (value, field) => {
|
|
16
|
-
if (typeof value !== "string" || !/^[1-9]\d*$/.test(value) || !Number.isSafeInteger(Number(value)))
|
|
17
|
-
throw new CliError("validation_error", `${field} must be a positive canonical tag id`);
|
|
18
|
-
return Number(value);
|
|
19
|
-
};
|
|
20
17
|
const cancelled = (options) => {
|
|
21
18
|
if (options.signal?.aborted)
|
|
22
19
|
throw new CliError("cancelled", "tag consolidation preview was cancelled");
|
|
23
20
|
};
|
|
24
21
|
export const tagConsolidationStoreOver = (context, catalog) => ({
|
|
25
|
-
previewTagConsolidation: async (options) =>
|
|
26
|
-
|
|
27
|
-
throw new CliError("validation_error", "preview options are required");
|
|
28
|
-
const allowed = new Set([
|
|
29
|
-
"sourceId",
|
|
30
|
-
"survivorId",
|
|
31
|
-
"expectedVocabularyFingerprint",
|
|
32
|
-
"maxRows",
|
|
33
|
-
"maxReadBytes",
|
|
34
|
-
"sampleLimit",
|
|
35
|
-
"signal",
|
|
36
|
-
]);
|
|
37
|
-
for (const field of Object.keys(options))
|
|
38
|
-
if (!allowed.has(field))
|
|
39
|
-
throw new CliError("validation_error", `unknown preview field: ${field}`);
|
|
40
|
-
const source = idOf(options.sourceId, "sourceId");
|
|
41
|
-
const survivor = idOf(options.survivorId, "survivorId");
|
|
42
|
-
if (source === survivor)
|
|
43
|
-
throw new CliError("validation_error", "sourceId and survivorId must differ");
|
|
44
|
-
if (typeof options.expectedVocabularyFingerprint !== "string" ||
|
|
45
|
-
!/^v1:all:all:[a-f0-9]{64}$/.test(options.expectedVocabularyFingerprint))
|
|
46
|
-
throw new CliError("validation_error", "expectedVocabularyFingerprint must come from a complete unfiltered vocabulary preview");
|
|
47
|
-
const maxRows = bounded(options.maxRows ?? 5000, 20000, "maxRows");
|
|
48
|
-
const maxReadBytes = bounded(options.maxReadBytes ?? 1024 * 1024, 16 * 1024 * 1024, "maxReadBytes");
|
|
49
|
-
const limit = bounded(options.sampleLimit ?? 10, 100, "sampleLimit");
|
|
50
|
-
cancelled(options);
|
|
51
|
-
// The catalog and dependency rows must describe one SQLite snapshot, including when called inside a write.
|
|
52
|
-
return read(context.database, () => {
|
|
53
|
-
const current = catalog();
|
|
54
|
-
if (current.fingerprint !== options.expectedVocabularyFingerprint)
|
|
55
|
-
throw new CliError("configuration_error", "vocabulary changed since the preview", {
|
|
56
|
-
reason: "vocabulary.stale",
|
|
57
|
-
});
|
|
58
|
-
const selected = [source, survivor].map((id) => current.entries.find((entry) => entry.id === String(id) && entry.kind !== "project"));
|
|
59
|
-
if (selected.some((entry) => !entry))
|
|
60
|
-
throw new CliError("not_found", "a selected canonical tag is unavailable");
|
|
61
|
-
if (selected.some((entry) => entry?.kind !== "tag"))
|
|
62
|
-
throw new CliError("validation_error", "consolidation preview supports tags only");
|
|
63
|
-
return previewRows(context, options, source, survivor, maxRows, maxReadBytes, limit, current);
|
|
64
|
-
});
|
|
65
|
-
},
|
|
22
|
+
previewTagConsolidation: async (options) => previewTagConsolidation(context, catalog, options),
|
|
23
|
+
applyTagConsolidation: async (options) => applyTagConsolidation(context, (input) => previewTagConsolidation(context, catalog, input), options),
|
|
66
24
|
});
|
|
25
|
+
const previewTagConsolidation = (context, catalog, options) => {
|
|
26
|
+
const { source, survivor, maxRows, maxReadBytes, limit } = consolidationInput(options);
|
|
27
|
+
cancelled(options);
|
|
28
|
+
return read(context.database, () => {
|
|
29
|
+
const current = catalog();
|
|
30
|
+
if (current.fingerprint !== options.expectedVocabularyFingerprint)
|
|
31
|
+
throw new CliError("configuration_error", "vocabulary changed since the preview", { reason: "vocabulary.stale" });
|
|
32
|
+
if (tagCanonicalId(context.database, source) === tagCanonicalId(context.database, survivor))
|
|
33
|
+
throw new CliError("validation_error", "sourceId and survivorId already resolve to the same canonical tag");
|
|
34
|
+
const selected = [source, survivor].map((id) => current.entries.find((entry) => entry.id === String(id) && entry.kind !== "project"));
|
|
35
|
+
if (selected.some((entry) => !entry))
|
|
36
|
+
throw new CliError("not_found", "a selected canonical tag is unavailable");
|
|
37
|
+
if (selected.some((entry) => entry?.kind !== "tag"))
|
|
38
|
+
throw new CliError("validation_error", "consolidation preview supports tags only");
|
|
39
|
+
return previewRows(context, options, source, survivor, maxRows, maxReadBytes, limit, current);
|
|
40
|
+
});
|
|
41
|
+
};
|
|
67
42
|
const previewRows = (context, options, source, survivor, maxRows, maxReadBytes, limit, catalog) => {
|
|
68
43
|
const rows = new Map();
|
|
69
44
|
let count = 0;
|
|
70
|
-
|
|
71
|
-
const
|
|
45
|
+
const graph = tagRedirectSnapshot(context.database);
|
|
46
|
+
const index = createTagRedirectIndex(graph);
|
|
47
|
+
const sourceMemberIds = index.resolve(String(source)).memberIds;
|
|
48
|
+
const survivorMemberIds = index.resolve(String(survivor)).memberIds;
|
|
49
|
+
const sourceMembers = new Set(sourceMemberIds.map(Number));
|
|
50
|
+
const memberIds = JSON.stringify([...sourceMemberIds, ...survivorMemberIds].map(Number));
|
|
51
|
+
const captured = JSON.stringify(graph);
|
|
52
|
+
let readBytes = Buffer.byteLength(captured);
|
|
53
|
+
const hash = createHash("sha256").update(JSON.stringify([source, survivor, catalog.fingerprint, graph]));
|
|
72
54
|
for (const [table, where] of selections) {
|
|
73
55
|
cancelled(options);
|
|
74
|
-
const parameters = table === "links" ? [
|
|
56
|
+
const parameters = table === "links" ? [memberIds, memberIds] : [memberIds];
|
|
75
57
|
const columns = context.database
|
|
76
58
|
.prepare(`PRAGMA table_info(${table})`)
|
|
77
59
|
.all()
|
|
@@ -87,7 +69,7 @@ const previewRows = (context, options, source, survivor, maxRows, maxReadBytes,
|
|
|
87
69
|
if (count + size > maxRows || Number(totals?.bytes ?? 0) + readBytes > maxReadBytes)
|
|
88
70
|
throw new CliError("validation_error", "tag dependencies exceed the preview budget; no partial preview was produced", { reason: "vocabulary.consolidation-over-budget", table });
|
|
89
71
|
const values = context.database
|
|
90
|
-
.prepare(`SELECT * FROM ${table} WHERE ${where} ORDER BY ${table === "
|
|
72
|
+
.prepare(`SELECT * FROM ${table} WHERE ${where} ORDER BY ${table === "canonical_auto_tag_claims" ? "chat_id,tag_id" : "id"} LIMIT ?`)
|
|
91
73
|
.all(...parameters, maxRows + 1);
|
|
92
74
|
const serialized = JSON.stringify(values);
|
|
93
75
|
readBytes += Buffer.byteLength(serialized);
|
|
@@ -103,25 +85,26 @@ const previewRows = (context, options, source, survivor, maxRows, maxReadBytes,
|
|
|
103
85
|
cancelled(options);
|
|
104
86
|
const assignments = rows.get("taggings") ?? [];
|
|
105
87
|
const aliases = rows.get("aliases") ?? [];
|
|
106
|
-
const claims = rows.get("
|
|
88
|
+
const claims = rows.get("canonical_auto_tag_claims") ?? [];
|
|
107
89
|
const links = rows.get("links") ?? [];
|
|
108
90
|
const byTargetType = new Map();
|
|
109
91
|
for (const row of assignments) {
|
|
110
92
|
const type = String(row.taggable_type);
|
|
111
93
|
const totals = byTargetType.get(type) ?? { type, source: 0, survivor: 0 };
|
|
112
|
-
if (Number(row.tag_id)
|
|
94
|
+
if (sourceMembers.has(Number(row.tag_id)))
|
|
113
95
|
totals.source++;
|
|
114
96
|
else
|
|
115
97
|
totals.survivor++;
|
|
116
98
|
byTargetType.set(type, totals);
|
|
117
99
|
}
|
|
118
100
|
const counts = (values, field) => ({
|
|
119
|
-
source: values.filter((row) => Number(row[field])
|
|
120
|
-
survivor: values.filter((row) => Number(row[field])
|
|
101
|
+
source: values.filter((row) => sourceMembers.has(Number(row[field]))).length,
|
|
102
|
+
survivor: values.filter((row) => !sourceMembers.has(Number(row[field]))).length,
|
|
121
103
|
});
|
|
122
104
|
const overlaps = (values, key) => {
|
|
123
|
-
const targets = new Set(values.filter((row) => Number(row.tag_id)
|
|
124
|
-
return values.filter((row) => Number(row.tag_id)
|
|
105
|
+
const targets = new Set(values.filter((row) => sourceMembers.has(Number(row.tag_id))).map(key));
|
|
106
|
+
return new Set(values.filter((row) => !sourceMembers.has(Number(row.tag_id)) && targets.has(key(row))).map(key))
|
|
107
|
+
.size;
|
|
125
108
|
};
|
|
126
109
|
const target = (id) => {
|
|
127
110
|
const entry = catalog.entries.find((row) => row.kind === "tag" && row.id === String(id));
|
|
@@ -130,6 +113,8 @@ const previewRows = (context, options, source, survivor, maxRows, maxReadBytes,
|
|
|
130
113
|
return {
|
|
131
114
|
source: target(source),
|
|
132
115
|
survivor: target(survivor),
|
|
116
|
+
sourceMemberIds,
|
|
117
|
+
survivorMemberIds,
|
|
133
118
|
vocabularyFingerprint: catalog.fingerprint,
|
|
134
119
|
previewFingerprint: `tag-preview-v1:${hash.digest("hex")}`,
|
|
135
120
|
assignments: {
|
|
@@ -171,7 +156,7 @@ const previewRows = (context, options, source, survivor, maxRows, maxReadBytes,
|
|
|
171
156
|
rows: count,
|
|
172
157
|
readBytes,
|
|
173
158
|
samplesComplete: [assignments, aliases, links].every((values) => values.length <= limit),
|
|
174
|
-
applySupported:
|
|
159
|
+
applySupported: true,
|
|
175
160
|
},
|
|
176
161
|
};
|
|
177
162
|
};
|