@opengeni/documents 0.5.18 → 0.5.38

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.
@@ -0,0 +1,214 @@
1
+ import {
2
+ KNOWLEDGE_BODY_MAX_BYTES,
3
+ KNOWLEDGE_METADATA_MAX_BYTES,
4
+ KNOWLEDGE_METADATA_MAX_DEPTH,
5
+ KNOWLEDGE_METADATA_MAX_ITEMS,
6
+ KNOWLEDGE_SOURCE_STRING_MAX_BYTES,
7
+ KNOWLEDGE_SOURCE_URI_MAX_BYTES,
8
+ KNOWLEDGE_SUMMARY_MAX_BYTES,
9
+ KNOWLEDGE_TITLE_MAX_BYTES,
10
+ KNOWLEDGE_TOPIC_MAX_BYTES,
11
+ KNOWLEDGE_TOPICS_MAX_ITEMS,
12
+ type KnowledgeRecord,
13
+ type KnowledgeSource,
14
+ } from "@opengeni/contracts";
15
+
16
+ type ProjectionField = KnowledgeRecord["projection"]["fields"][number];
17
+
18
+ export type KnowledgeProjectionInput = {
19
+ title: string;
20
+ body: string | null;
21
+ summary: string | null;
22
+ topics: unknown;
23
+ metadata: unknown;
24
+ source: KnowledgeSource;
25
+ };
26
+
27
+ export type KnowledgeProjectionResult = Pick<
28
+ KnowledgeRecord,
29
+ "title" | "content" | "projection"
30
+ > & {
31
+ source: KnowledgeSource;
32
+ };
33
+
34
+ const utf8Bytes = (value: string): number => Buffer.byteLength(value, "utf8");
35
+
36
+ function truncateUtf8(value: string, maxBytes: number): { value: string; truncated: boolean } {
37
+ if (utf8Bytes(value) <= maxBytes) return { value, truncated: false };
38
+ let low = 0;
39
+ let high = value.length;
40
+ while (low < high) {
41
+ const middle = Math.ceil((low + high) / 2);
42
+ const end =
43
+ middle > 0 && middle < value.length && /[\uD800-\uDBFF]/u.test(value[middle - 1]!)
44
+ ? middle - 1
45
+ : middle;
46
+ if (utf8Bytes(value.slice(0, end)) <= maxBytes) low = middle;
47
+ else high = middle - 1;
48
+ }
49
+ let end = low;
50
+ if (end > 0 && end < value.length && /[\uD800-\uDBFF]/u.test(value[end - 1]!)) end -= 1;
51
+ while (end > 0 && utf8Bytes(value.slice(0, end)) > maxBytes) end -= 1;
52
+ return { value: value.slice(0, end), truncated: true };
53
+ }
54
+
55
+ function projectNullableString(
56
+ value: string | null,
57
+ maxBytes: number,
58
+ field: ProjectionField,
59
+ fields: Set<ProjectionField>,
60
+ ): string | null {
61
+ if (value === null) return null;
62
+ const projected = truncateUtf8(value, maxBytes);
63
+ if (projected.truncated) fields.add(field);
64
+ return projected.value;
65
+ }
66
+
67
+ function projectSourceUri(value: string | null, fields: Set<ProjectionField>): string | null {
68
+ if (value === null || value === "") return null;
69
+ if (utf8Bytes(value) > KNOWLEDGE_SOURCE_URI_MAX_BYTES) {
70
+ fields.add("provenance.source.uri");
71
+ return null;
72
+ }
73
+ return value;
74
+ }
75
+
76
+ function projectTopics(value: unknown, fields: Set<ProjectionField>): string[] {
77
+ if (!Array.isArray(value)) {
78
+ if (value !== null && value !== undefined) fields.add("content.topics");
79
+ return [];
80
+ }
81
+ const result: string[] = [];
82
+ for (const candidate of value) {
83
+ if (result.length >= KNOWLEDGE_TOPICS_MAX_ITEMS) {
84
+ fields.add("content.topics");
85
+ break;
86
+ }
87
+ if (typeof candidate !== "string") {
88
+ fields.add("content.topics");
89
+ continue;
90
+ }
91
+ const projected = truncateUtf8(candidate, KNOWLEDGE_TOPIC_MAX_BYTES);
92
+ if (projected.truncated) fields.add("content.topics");
93
+ result.push(projected.value);
94
+ }
95
+ return result;
96
+ }
97
+
98
+ function projectMetadata(value: unknown): { value: Record<string, unknown>; truncated: boolean } {
99
+ let remainingItems = KNOWLEDGE_METADATA_MAX_ITEMS;
100
+ let truncated = false;
101
+
102
+ const visit = (candidate: unknown, depth: number): unknown => {
103
+ if (remainingItems <= 0 || depth > KNOWLEDGE_METADATA_MAX_DEPTH) {
104
+ truncated = true;
105
+ return undefined;
106
+ }
107
+ remainingItems -= 1;
108
+ if (
109
+ candidate === null ||
110
+ typeof candidate === "boolean" ||
111
+ (typeof candidate === "number" && Number.isFinite(candidate))
112
+ ) {
113
+ return candidate;
114
+ }
115
+ if (typeof candidate === "string") {
116
+ const projected = truncateUtf8(candidate, KNOWLEDGE_SOURCE_STRING_MAX_BYTES);
117
+ if (projected.truncated) truncated = true;
118
+ return projected.value;
119
+ }
120
+ if (Array.isArray(candidate)) {
121
+ const result: unknown[] = [];
122
+ for (const item of candidate) {
123
+ const projected = visit(item, depth + 1);
124
+ if (projected === undefined) break;
125
+ result.push(projected);
126
+ }
127
+ return result;
128
+ }
129
+ if (candidate && typeof candidate === "object") {
130
+ const result: Record<string, unknown> = {};
131
+ for (const key of Object.keys(candidate as Record<string, unknown>).sort()) {
132
+ if (utf8Bytes(key) > KNOWLEDGE_TOPIC_MAX_BYTES) {
133
+ truncated = true;
134
+ continue;
135
+ }
136
+ const projected = visit((candidate as Record<string, unknown>)[key], depth + 1);
137
+ if (projected === undefined) break;
138
+ result[key] = projected;
139
+ }
140
+ return result;
141
+ }
142
+ truncated = true;
143
+ return undefined;
144
+ };
145
+
146
+ const root = visit(value, 0);
147
+ let result = root && typeof root === "object" && !Array.isArray(root) ? root : {};
148
+ if (result !== root) truncated = true;
149
+ const bounded: Record<string, unknown> = {};
150
+ for (const key of Object.keys(result as Record<string, unknown>).sort()) {
151
+ bounded[key] = (result as Record<string, unknown>)[key];
152
+ if (utf8Bytes(JSON.stringify(bounded)) > KNOWLEDGE_METADATA_MAX_BYTES) {
153
+ delete bounded[key];
154
+ truncated = true;
155
+ break;
156
+ }
157
+ }
158
+ result = bounded;
159
+ return { value: result as Record<string, unknown>, truncated };
160
+ }
161
+
162
+ /** Deterministic, byte-bounded projection at the agent-facing Knowledge envelope. */
163
+ export function projectKnowledgeRecord(input: KnowledgeProjectionInput): KnowledgeProjectionResult {
164
+ const fields = new Set<ProjectionField>();
165
+ const title = truncateUtf8(input.title, KNOWLEDGE_TITLE_MAX_BYTES);
166
+ if (title.truncated) fields.add("title");
167
+ const metadata = projectMetadata(input.metadata);
168
+ if (metadata.truncated) fields.add("content.metadata");
169
+ const source: KnowledgeSource = {
170
+ ...input.source,
171
+ uri: projectSourceUri(input.source.uri, fields),
172
+ externalId: projectNullableString(
173
+ input.source.externalId,
174
+ KNOWLEDGE_SOURCE_STRING_MAX_BYTES,
175
+ "provenance.source.externalId",
176
+ fields,
177
+ ),
178
+ title: projectNullableString(
179
+ input.source.title,
180
+ KNOWLEDGE_SOURCE_STRING_MAX_BYTES,
181
+ "provenance.source.title",
182
+ fields,
183
+ ),
184
+ author: projectNullableString(
185
+ input.source.author,
186
+ KNOWLEDGE_SOURCE_STRING_MAX_BYTES,
187
+ "provenance.source.author",
188
+ fields,
189
+ ),
190
+ version: projectNullableString(
191
+ input.source.version,
192
+ KNOWLEDGE_SOURCE_STRING_MAX_BYTES,
193
+ "provenance.source.version",
194
+ fields,
195
+ ),
196
+ };
197
+ return {
198
+ title: title.value,
199
+ content: {
200
+ format: "markdown",
201
+ body: projectNullableString(input.body, KNOWLEDGE_BODY_MAX_BYTES, "content.body", fields),
202
+ summary: projectNullableString(
203
+ input.summary,
204
+ KNOWLEDGE_SUMMARY_MAX_BYTES,
205
+ "content.summary",
206
+ fields,
207
+ ),
208
+ topics: projectTopics(input.topics, fields),
209
+ metadata: metadata.value,
210
+ },
211
+ source,
212
+ projection: { truncated: fields.size > 0, fields: [...fields].sort() },
213
+ };
214
+ }