summaryrefslogtreecommitdiff
path: root/src/domain
diff options
context:
space:
mode:
authortanaka takashi <isazaka@gmail.com>2026-06-29 02:36:40 +0900
committertanaka takashi <isazaka@gmail.com>2026-06-29 02:36:40 +0900
commit4c6a831aa2a57e2bcc20aa3fc2f1b7172f5be285 (patch)
tree074021381abf57cad434430f25027115b681b8a9 /src/domain
parent8d38b06f11c713e547d70468340242f23322152c (diff)
feat: 会話メモリのプロトタイプを実装
- 6種類のMCPツールと入力検証を追加 - バイナリログ、スナップショット、復旧、圧縮処理を実装 - Unicode n-gram検索とドメインモデルを追加 - 単体テストと統合テストを追加 - アーキテクチャと実装状況を文書化 - Cコードの記述スタイルを統一
Diffstat (limited to 'src/domain')
-rw-r--r--src/domain/README.md11
-rw-r--r--src/domain/message.c255
-rw-r--r--src/domain/message.h60
-rw-r--r--src/domain/search.c350
-rw-r--r--src/domain/search.h22
5 files changed, 694 insertions, 4 deletions
diff --git a/src/domain/README.md b/src/domain/README.md
index 0d359af..a30039e 100644
--- a/src/domain/README.md
+++ b/src/domain/README.md
@@ -1,8 +1,11 @@
# Domain layer
-This directory is reserved for the conversation-memory domain model and its
-business rules. Conversation, message, retention, and search contracts will be
-defined here only after the next design phase.
+`message.h` defines immutable Message and metadata concepts, role/tier enums,
+input limits, deep-copy ownership, and UTF-8 validation. This layer has no mcpc
+or Win32 dependency.
-The domain layer must not depend on mcpc or on a concrete storage engine.
+`search.h` owns the ASCII-case-folded Unicode codepoint 2/3-gram inverted index,
+collision verification, candidate selection, and deterministic relevance score.
+`memory_tier` is descriptive in v1. Retention, summarization, and automatic tier
+transitions are intentionally not domain behaviors yet.
diff --git a/src/domain/message.c b/src/domain/message.c
new file mode 100644
index 0000000..d973be4
--- /dev/null
+++ b/src/domain/message.c
@@ -0,0 +1,255 @@
+#include "domain/message.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+static char *
+duplicate_string(const char *value)
+{
+ const size_t length = strlen(value);
+ char *copy = malloc(length + 1);
+ if (copy != NULL)
+ {
+ memcpy(copy, value, length + 1);
+ }
+ return copy;
+}
+
+void
+tiny_message_init(tiny_message_t *message)
+{
+ memset(message, 0, sizeof(*message));
+ message->metadata.memory_tier = TINY_TIER_SHORT;
+}
+
+void
+tiny_message_free(tiny_message_t *message)
+{
+ size_t index;
+ free(message->session_id);
+ free(message->content);
+ free(message->metadata.tool_call_id);
+ for (index = 0; index < message->metadata.tag_count; ++index)
+ {
+ free(message->metadata.tags[index]);
+ }
+ free(message->metadata.tags);
+ tiny_message_init(message);
+}
+
+int
+tiny_message_copy(tiny_message_t *destination, const tiny_message_t *source)
+{
+ size_t index;
+ tiny_message_init(destination);
+ memcpy(destination->message_id, source->message_id, sizeof(destination->message_id));
+ destination->timestamp_ms = source->timestamp_ms;
+ destination->role = source->role;
+ destination->metadata.memory_tier = source->metadata.memory_tier;
+ destination->session_id = duplicate_string(source->session_id);
+ destination->content = duplicate_string(source->content);
+ if (destination->session_id == NULL || destination->content == NULL)
+ {
+ goto failure;
+ }
+ if (source->metadata.tool_call_id != NULL)
+ {
+ destination->metadata.tool_call_id = duplicate_string(source->metadata.tool_call_id);
+ if (destination->metadata.tool_call_id == NULL)
+ {
+ goto failure;
+ }
+ }
+ if (source->metadata.tag_count > 0)
+ {
+ destination->metadata.tags = calloc(source->metadata.tag_count, sizeof(char *));
+ if (destination->metadata.tags == NULL)
+ {
+ goto failure;
+ }
+ for (index = 0; index < source->metadata.tag_count; ++index)
+ {
+ destination->metadata.tags[index] = duplicate_string(source->metadata.tags[index]);
+ if (destination->metadata.tags[index] == NULL)
+ {
+ goto failure;
+ }
+ destination->metadata.tag_count++;
+ }
+ }
+ return 0;
+
+failure:
+ tiny_message_free(destination);
+ return -1;
+}
+
+const char *
+tiny_role_name(tiny_role_t role)
+{
+ switch (role)
+ {
+ case TINY_ROLE_USER:
+ return "user";
+ case TINY_ROLE_ASSISTANT:
+ return "assistant";
+ case TINY_ROLE_SYSTEM:
+ return "system";
+ default:
+ return "unknown";
+ }
+}
+
+bool
+tiny_role_parse(const char *value, tiny_role_t *role)
+{
+ if (strcmp(value, "user") == 0)
+ {
+ *role = TINY_ROLE_USER;
+ }
+ else if (strcmp(value, "assistant") == 0)
+ {
+ *role = TINY_ROLE_ASSISTANT;
+ }
+ else if (strcmp(value, "system") == 0)
+ {
+ *role = TINY_ROLE_SYSTEM;
+ }
+ else
+ {
+ return false;
+ }
+ return true;
+}
+
+const char *
+tiny_tier_name(tiny_memory_tier_t tier)
+{
+ switch (tier)
+ {
+ case TINY_TIER_SHORT:
+ return "short";
+ case TINY_TIER_MID:
+ return "mid";
+ case TINY_TIER_LONG:
+ return "long";
+ case TINY_TIER_DISCARD:
+ return "discard";
+ default:
+ return "unknown";
+ }
+}
+
+bool
+tiny_tier_parse(const char *value, tiny_memory_tier_t *tier)
+{
+ if (strcmp(value, "short") == 0)
+ {
+ *tier = TINY_TIER_SHORT;
+ }
+ else if (strcmp(value, "mid") == 0)
+ {
+ *tier = TINY_TIER_MID;
+ }
+ else if (strcmp(value, "long") == 0)
+ {
+ *tier = TINY_TIER_LONG;
+ }
+ else if (strcmp(value, "discard") == 0)
+ {
+ *tier = TINY_TIER_DISCARD;
+ }
+ else
+ {
+ return false;
+ }
+ return true;
+}
+
+bool
+tiny_utf8_valid(const char *value, size_t length)
+{
+ size_t index = 0;
+ while (index < length)
+ {
+ const unsigned char first = (unsigned char)value[index++];
+ unsigned needed;
+ uint32_t codepoint;
+ if (first < 0x80)
+ {
+ if (first == 0)
+ {
+ return false;
+ }
+ continue;
+ }
+ if ((first & 0xe0) == 0xc0)
+ {
+ needed = 1;
+ codepoint = first & 0x1f;
+ }
+ else if ((first & 0xf0) == 0xe0)
+ {
+ needed = 2;
+ codepoint = first & 0x0f;
+ }
+ else if ((first & 0xf8) == 0xf0)
+ {
+ needed = 3;
+ codepoint = first & 0x07;
+ }
+ else
+ {
+ return false;
+ }
+ if (index + needed > length)
+ {
+ return false;
+ }
+ while (needed-- > 0)
+ {
+ const unsigned char next = (unsigned char)value[index++];
+ if ((next & 0xc0) != 0x80)
+ {
+ return false;
+ }
+ codepoint = (codepoint << 6) | (next & 0x3f);
+ }
+ if (codepoint > 0x10ffff || (codepoint >= 0xd800 && codepoint <= 0xdfff))
+ {
+ return false;
+ }
+ if ((first & 0xe0) == 0xc0 && codepoint < 0x80)
+ {
+ return false;
+ }
+ if ((first & 0xf0) == 0xe0 && codepoint < 0x800)
+ {
+ return false;
+ }
+ if ((first & 0xf8) == 0xf0 && codepoint < 0x10000)
+ {
+ return false;
+ }
+ }
+ return true;
+}
+
+bool
+tiny_message_id_valid(const char *value)
+{
+ static const char alphabet[] = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
+ size_t index;
+ if (strlen(value) != TINY_MESSAGE_ID_LENGTH || value[0] > '7')
+ {
+ return false;
+ }
+ for (index = 0; index < TINY_MESSAGE_ID_LENGTH; ++index)
+ {
+ if (strchr(alphabet, value[index]) == NULL)
+ {
+ return false;
+ }
+ }
+ return true;
+}
diff --git a/src/domain/message.h b/src/domain/message.h
new file mode 100644
index 0000000..fa36d2a
--- /dev/null
+++ b/src/domain/message.h
@@ -0,0 +1,60 @@
+#ifndef TINY_CONVMEM_DOMAIN_MESSAGE_H
+#define TINY_CONVMEM_DOMAIN_MESSAGE_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#define TINY_MESSAGE_ID_LENGTH 26
+#define TINY_SESSION_ID_MAX 256
+#define TINY_CONTENT_MAX (1024U * 1024U)
+#define TINY_TOOL_CALL_ID_MAX 256
+#define TINY_TAG_MAX 128
+#define TINY_TAG_COUNT_MAX 32
+#define TINY_QUERY_MAX 4096
+
+typedef enum tiny_role
+{
+ TINY_ROLE_USER = 1,
+ TINY_ROLE_ASSISTANT = 2,
+ TINY_ROLE_SYSTEM = 3
+} tiny_role_t;
+
+typedef enum tiny_memory_tier
+{
+ TINY_TIER_SHORT = 1,
+ TINY_TIER_MID = 2,
+ TINY_TIER_LONG = 3,
+ TINY_TIER_DISCARD = 4
+} tiny_memory_tier_t;
+
+typedef struct tiny_metadata
+{
+ char *tool_call_id;
+ char **tags;
+ size_t tag_count;
+ tiny_memory_tier_t memory_tier;
+} tiny_metadata_t;
+
+typedef struct tiny_message
+{
+ char message_id[TINY_MESSAGE_ID_LENGTH + 1];
+ char *session_id;
+ int64_t timestamp_ms;
+ tiny_role_t role;
+ char *content;
+ tiny_metadata_t metadata;
+} tiny_message_t;
+
+void tiny_message_init(tiny_message_t *message);
+void tiny_message_free(tiny_message_t *message);
+int tiny_message_copy(tiny_message_t *destination, const tiny_message_t *source);
+
+const char *tiny_role_name(tiny_role_t role);
+bool tiny_role_parse(const char *value, tiny_role_t *role);
+const char *tiny_tier_name(tiny_memory_tier_t tier);
+bool tiny_tier_parse(const char *value, tiny_memory_tier_t *tier);
+bool tiny_utf8_valid(const char *value, size_t length);
+bool tiny_message_id_valid(const char *value);
+
+#endif
diff --git a/src/domain/search.c b/src/domain/search.c
new file mode 100644
index 0000000..a24b667
--- /dev/null
+++ b/src/domain/search.c
@@ -0,0 +1,350 @@
+#include "domain/search.h"
+
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+typedef struct gram_bucket
+{
+ uint64_t hash;
+ uint32_t points[3];
+ unsigned width;
+ size_t *messages;
+ size_t count;
+ size_t capacity;
+} gram_bucket_t;
+
+struct tiny_search_index
+{
+ gram_bucket_t *buckets;
+ size_t count;
+ size_t capacity;
+ size_t message_count;
+};
+
+static bool
+decode_codepoints(const char *text, uint32_t **output, size_t *count)
+{
+ const size_t bytes = strlen(text);
+ size_t offset = 0, used = 0;
+ uint32_t *points = malloc((bytes + 1) * sizeof(uint32_t));
+ if (points == NULL)
+ {
+ return false;
+ }
+ while (offset < bytes)
+ {
+ unsigned char first = (unsigned char)text[offset++];
+ uint32_t point;
+ unsigned continuation;
+ if (first < 0x80)
+ {
+ point = first;
+ continuation = 0;
+ }
+ else if ((first & 0xe0) == 0xc0)
+ {
+ point = first & 0x1f;
+ continuation = 1;
+ }
+ else if ((first & 0xf0) == 0xe0)
+ {
+ point = first & 0x0f;
+ continuation = 2;
+ }
+ else
+ {
+ point = first & 0x07;
+ continuation = 3;
+ }
+ while (continuation-- > 0)
+ {
+ point = (point << 6) | ((unsigned char)text[offset++] & 0x3f);
+ }
+ if (point >= 'A' && point <= 'Z')
+ {
+ point += 'a' - 'A';
+ }
+ points[used++] = point;
+ }
+ *output = points;
+ *count = used;
+ return true;
+}
+
+static uint64_t
+gram_hash(const uint32_t *points, size_t width)
+{
+ uint64_t hash = 1469598103934665603ULL;
+ size_t index;
+ for (index = 0; index < width; ++index)
+ {
+ hash ^= points[index];
+ hash *= 1099511628211ULL;
+ }
+ return hash;
+}
+
+static gram_bucket_t *
+find_bucket(const tiny_search_index_t *index, const uint32_t *points, unsigned width)
+{
+ const uint64_t hash = gram_hash(points, width);
+ size_t position;
+ for (position = 0; position < index->count; ++position)
+ {
+ if (index->buckets[position].hash == hash && index->buckets[position].width == width
+ && memcmp(index->buckets[position].points, points, width * sizeof(uint32_t)) == 0)
+ {
+ return &index->buckets[position];
+ }
+ }
+ return NULL;
+}
+
+static bool
+add_gram(tiny_search_index_t *index, const uint32_t *points, unsigned width, size_t message_index)
+{
+ gram_bucket_t *bucket = find_bucket(index, points, width);
+ if (bucket == NULL)
+ {
+ gram_bucket_t *resized;
+ size_t capacity;
+ if (index->count == index->capacity)
+ {
+ capacity = index->capacity ? index->capacity * 2 : 64;
+ resized = realloc(index->buckets, capacity * sizeof(*resized));
+ if (!resized)
+ {
+ return false;
+ }
+ index->buckets = resized;
+ index->capacity = capacity;
+ }
+ bucket = &index->buckets[index->count++];
+ memset(bucket, 0, sizeof(*bucket));
+ bucket->hash = gram_hash(points, width);
+ bucket->width = width;
+ memcpy(bucket->points, points, width * sizeof(uint32_t));
+ }
+ if (bucket->count > 0 && bucket->messages[bucket->count - 1] == message_index)
+ {
+ return true;
+ }
+ if (bucket->count == bucket->capacity)
+ {
+ const size_t capacity = bucket->capacity ? bucket->capacity * 2 : 8;
+ size_t *resized = realloc(bucket->messages, capacity * sizeof(*resized));
+ if (!resized)
+ {
+ return false;
+ }
+ bucket->messages = resized;
+ bucket->capacity = capacity;
+ }
+ bucket->messages[bucket->count++] = message_index;
+ return true;
+}
+
+static bool
+index_text(tiny_search_index_t *index, const char *text, size_t message_index)
+{
+ uint32_t *points = NULL;
+ size_t count = 0, offset;
+ unsigned width;
+ if (!decode_codepoints(text, &points, &count))
+ {
+ return false;
+ }
+ for (width = 2; width <= 3; ++width)
+ {
+ for (offset = 0; offset + width <= count; ++offset)
+ {
+ if (!add_gram(index, points + offset, width, message_index))
+ {
+ free(points);
+ return false;
+ }
+ }
+ }
+ free(points);
+ return true;
+}
+
+tiny_search_index_t *
+tiny_search_index_create(void)
+{
+ return calloc(1, sizeof(tiny_search_index_t));
+}
+
+void
+tiny_search_index_free(tiny_search_index_t *index)
+{
+ size_t position;
+ if (!index)
+ {
+ return;
+ }
+ for (position = 0; position < index->count; ++position)
+ {
+ free(index->buckets[position].messages);
+ }
+ free(index->buckets);
+ free(index);
+}
+
+bool
+tiny_search_index_add(tiny_search_index_t *index,
+ const tiny_message_t *message,
+ size_t message_index)
+{
+ size_t tag;
+ if (!index_text(index, message->content, message_index))
+ {
+ return false;
+ }
+ for (tag = 0; tag < message->metadata.tag_count; ++tag)
+ {
+ if (!index_text(index, message->metadata.tags[tag], message_index))
+ {
+ return false;
+ }
+ }
+ if (message_index >= index->message_count)
+ {
+ index->message_count = message_index + 1;
+ }
+ return true;
+}
+
+bool
+tiny_search_index_candidates(const tiny_search_index_t *index,
+ const char *query,
+ size_t **output,
+ size_t *count)
+{
+ uint32_t *points = NULL;
+ size_t point_count = 0, width, offset, used = 0;
+ bool *seen;
+ size_t *items;
+ *output = NULL;
+ *count = 0;
+ if (!decode_codepoints(query, &points, &point_count))
+ {
+ return false;
+ }
+ items = malloc((index->message_count ? index->message_count : 1) * sizeof(*items));
+ seen = calloc(index->message_count ? index->message_count : 1, sizeof(*seen));
+ if (!items || !seen)
+ {
+ free(points);
+ free(items);
+ free(seen);
+ return false;
+ }
+ if (point_count < 2)
+ {
+ for (offset = 0; offset < index->message_count; ++offset)
+ {
+ seen[offset] = true;
+ items[used++] = offset;
+ }
+ }
+ else
+ {
+ gram_bucket_t *bucket;
+ width = point_count >= 3 ? 3 : 2;
+ for (offset = 0; offset + width <= point_count; ++offset)
+ {
+ if ((bucket = find_bucket(index, points + offset, (unsigned)width)) != NULL)
+ {
+ size_t item;
+ for (item = 0; item < bucket->count; ++item)
+ {
+ if (!seen[bucket->messages[item]])
+ {
+ seen[bucket->messages[item]] = true;
+ items[used++] = bucket->messages[item];
+ }
+ }
+ }
+ }
+ }
+ free(points);
+ free(seen);
+ *output = items;
+ *count = used;
+ return true;
+}
+
+double
+tiny_search_score(const char *query, const tiny_message_t *message)
+{
+ uint32_t *query_points = NULL, *message_points = NULL;
+ size_t query_count = 0, message_count = 0, width, query_offset, message_offset, matches = 0,
+ total, tag;
+ double score = 0.0;
+ if (!decode_codepoints(query, &query_points, &query_count)
+ || !decode_codepoints(message->content, &message_points, &message_count) || query_count == 0)
+ {
+ goto done;
+ }
+ width = query_count >= 3 ? 3 : (query_count >= 2 ? 2 : 1);
+ total = query_count - width + 1;
+ for (query_offset = 0; query_offset < total; ++query_offset)
+ {
+ bool found = false;
+ for (message_offset = 0; message_offset + width <= message_count; ++message_offset)
+ {
+ if (memcmp(query_points + query_offset,
+ message_points + message_offset,
+ width * sizeof(uint32_t))
+ == 0)
+ {
+ found = true;
+ break;
+ }
+ }
+ for (tag = 0; tag < message->metadata.tag_count && !found; ++tag)
+ {
+ uint32_t *tag_points = NULL;
+ size_t tag_count = 0;
+ if (!decode_codepoints(message->metadata.tags[tag], &tag_points, &tag_count))
+ {
+ continue;
+ }
+ for (message_offset = 0; message_offset + width <= tag_count; ++message_offset)
+ {
+ if (memcmp(query_points + query_offset,
+ tag_points + message_offset,
+ width * sizeof(uint32_t))
+ == 0)
+ {
+ found = true;
+ break;
+ }
+ }
+ free(tag_points);
+ }
+ if (found)
+ {
+ matches++;
+ }
+ }
+ score = (double)matches / (double)total;
+ if (query_count <= message_count)
+ {
+ for (message_offset = 0; message_offset + query_count <= message_count; ++message_offset)
+ {
+ if (memcmp(query_points, message_points + message_offset, query_count * sizeof(uint32_t))
+ == 0)
+ {
+ score += 1.0;
+ break;
+ }
+ }
+ }
+done:
+ free(query_points);
+ free(message_points);
+ return score;
+}
diff --git a/src/domain/search.h b/src/domain/search.h
new file mode 100644
index 0000000..3408998
--- /dev/null
+++ b/src/domain/search.h
@@ -0,0 +1,22 @@
+#ifndef TINY_CONVMEM_DOMAIN_SEARCH_H
+#define TINY_CONVMEM_DOMAIN_SEARCH_H
+
+#include <stdbool.h>
+#include <stddef.h>
+
+#include "domain/message.h"
+
+typedef struct tiny_search_index tiny_search_index_t;
+
+tiny_search_index_t *tiny_search_index_create(void);
+void tiny_search_index_free(tiny_search_index_t *index);
+bool tiny_search_index_add(tiny_search_index_t *index,
+ const tiny_message_t *message,
+ size_t message_index);
+bool tiny_search_index_candidates(const tiny_search_index_t *index,
+ const char *query,
+ size_t **message_indices,
+ size_t *count);
+double tiny_search_score(const char *query, const tiny_message_t *message);
+
+#endif