@opengeni/core 0.17.1 → 0.18.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/domain/memory-slack-publication.d.ts +107 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +255 -2
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
- package/src/domain/capabilities.ts +15 -1
- package/src/domain/memory-slack-publication.ts +448 -0
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "OpenGeni framework-agnostic core: the domain, access, and billing layers (neutral access, off-HTTP V2 surface). Behavior-preserving extraction from apps/api — keeps Hono's HTTPException for error throwing (typed-errors cleanup deferred).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -35,14 +35,14 @@
|
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
37
37
|
"@opengeni/codex": "^0.2.9",
|
|
38
|
-
"@opengeni/config": "^0.
|
|
39
|
-
"@opengeni/contracts": "^0.
|
|
40
|
-
"@opengeni/db": "^0.
|
|
41
|
-
"@opengeni/documents": "^0.2.
|
|
42
|
-
"@opengeni/events": "^0.3.
|
|
43
|
-
"@opengeni/observability": "^0.4.
|
|
44
|
-
"@opengeni/runtime": "^0.
|
|
45
|
-
"@opengeni/storage": "^0.2.
|
|
38
|
+
"@opengeni/config": "^0.10.0",
|
|
39
|
+
"@opengeni/contracts": "^0.31.0",
|
|
40
|
+
"@opengeni/db": "^0.22.1",
|
|
41
|
+
"@opengeni/documents": "^0.2.69",
|
|
42
|
+
"@opengeni/events": "^0.3.59",
|
|
43
|
+
"@opengeni/observability": "^0.4.3",
|
|
44
|
+
"@opengeni/runtime": "^0.17.0",
|
|
45
|
+
"@opengeni/storage": "^0.2.53",
|
|
46
46
|
"hono": "^4.12.18"
|
|
47
47
|
},
|
|
48
48
|
"engines": {
|
|
@@ -898,6 +898,19 @@ function packCatalogItem(
|
|
|
898
898
|
pack: ReturnType<typeof listCapabilityPacks>[number],
|
|
899
899
|
source: "built_in" | "manual",
|
|
900
900
|
): CapabilityCatalogItem {
|
|
901
|
+
const customMetadata = { ...pack.metadata };
|
|
902
|
+
for (const key of [
|
|
903
|
+
"packId",
|
|
904
|
+
"version",
|
|
905
|
+
"connectors",
|
|
906
|
+
"knowledge",
|
|
907
|
+
"scheduledTaskTemplates",
|
|
908
|
+
"sandboxImage",
|
|
909
|
+
"sandboxProviderImages",
|
|
910
|
+
"skills",
|
|
911
|
+
]) {
|
|
912
|
+
delete customMetadata[key];
|
|
913
|
+
}
|
|
901
914
|
return CapabilityCatalogItem.parse({
|
|
902
915
|
id: `pack:${pack.id}`,
|
|
903
916
|
kind: "pack",
|
|
@@ -912,6 +925,7 @@ function packCatalogItem(
|
|
|
912
925
|
notes: "Enables role-scoped tools, connectors, knowledge, and scheduled-task templates.",
|
|
913
926
|
},
|
|
914
927
|
metadata: {
|
|
928
|
+
...customMetadata,
|
|
915
929
|
packId: pack.id,
|
|
916
930
|
version: pack.version,
|
|
917
931
|
connectors: pack.connectors,
|
|
@@ -919,8 +933,8 @@ function packCatalogItem(
|
|
|
919
933
|
scheduledTaskTemplates: pack.scheduledTaskTemplates,
|
|
920
934
|
// Runtime composition surface only: skill names, never file content.
|
|
921
935
|
...(pack.sandboxImage ? { sandboxImage: pack.sandboxImage } : {}),
|
|
936
|
+
...(pack.sandboxProviderImages ? { sandboxProviderImages: pack.sandboxProviderImages } : {}),
|
|
922
937
|
...(pack.skills.length > 0 ? { skills: pack.skills.map((skill) => skill.name) } : {}),
|
|
923
|
-
...pack.metadata,
|
|
924
938
|
},
|
|
925
939
|
});
|
|
926
940
|
}
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
redactSensitiveText,
|
|
4
|
+
stableJson,
|
|
5
|
+
type KnowledgeMemoryKind,
|
|
6
|
+
type KnowledgeMemoryStatus,
|
|
7
|
+
} from "@opengeni/contracts";
|
|
8
|
+
|
|
9
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
10
|
+
const SELECTOR_SEGMENT_PATTERN = /^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$/;
|
|
11
|
+
const encoder = new TextEncoder();
|
|
12
|
+
const decoder = new TextDecoder();
|
|
13
|
+
|
|
14
|
+
export const MEMORY_SLACK_PROJECTION_VERSION = 1;
|
|
15
|
+
export const MEMORY_SLACK_SUMMARY_MAX_UTF8_BYTES = 512;
|
|
16
|
+
export const MEMORY_SLACK_NAMESPACE_MAX_UTF8_BYTES = 128;
|
|
17
|
+
export const MEMORY_SLACK_LABEL_MAX_UTF8_BYTES = 64;
|
|
18
|
+
export const MEMORY_SLACK_LABEL_MAX_COUNT = 8;
|
|
19
|
+
export const MEMORY_SLACK_OWNER_MAX_UTF8_BYTES = 96;
|
|
20
|
+
export const MEMORY_SLACK_PROJECTION_MAX_UTF8_BYTES = 4096;
|
|
21
|
+
|
|
22
|
+
export type MemorySlackImportance = "major" | "normal" | "minor";
|
|
23
|
+
export type MemorySlackRequestedMode = "auto" | "review" | "never";
|
|
24
|
+
export type MemorySlackDeliveryMode = Exclude<MemorySlackRequestedMode, "never">;
|
|
25
|
+
export type MemorySlackChangeKind = "created" | "corrected" | "superseded";
|
|
26
|
+
export type MemorySlackOrigin = "native" | "slack_derived";
|
|
27
|
+
export type MemorySlackAudience = "workspace" | "restricted";
|
|
28
|
+
|
|
29
|
+
export type MemorySlackPublicationPolicy = {
|
|
30
|
+
enabled: boolean;
|
|
31
|
+
autoImportances: readonly MemorySlackImportance[];
|
|
32
|
+
reviewImportances: readonly MemorySlackImportance[];
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Nothing publishes without an explicit workspace-admin enablement. When
|
|
37
|
+
* enabled without tuning, major changes may publish automatically, normal
|
|
38
|
+
* changes require review, and minor changes stay quiet.
|
|
39
|
+
*/
|
|
40
|
+
export const DEFAULT_MEMORY_SLACK_PUBLICATION_POLICY = {
|
|
41
|
+
enabled: false,
|
|
42
|
+
autoImportances: ["major"],
|
|
43
|
+
reviewImportances: ["normal"],
|
|
44
|
+
} as const satisfies MemorySlackPublicationPolicy;
|
|
45
|
+
|
|
46
|
+
export type MemorySlackPublicationInput = {
|
|
47
|
+
context: {
|
|
48
|
+
accountId: string;
|
|
49
|
+
workspaceId: string;
|
|
50
|
+
now: string;
|
|
51
|
+
};
|
|
52
|
+
policy?: MemorySlackPublicationPolicy | undefined;
|
|
53
|
+
memory: {
|
|
54
|
+
sourceType: "workspace_memory";
|
|
55
|
+
accountId: string;
|
|
56
|
+
workspaceId: string;
|
|
57
|
+
id: string;
|
|
58
|
+
version: number;
|
|
59
|
+
status: KnowledgeMemoryStatus;
|
|
60
|
+
kind: KnowledgeMemoryKind;
|
|
61
|
+
scopeType: "workspace" | "user" | "role" | "session" | "ephemeral" | "legacy";
|
|
62
|
+
namespace: string;
|
|
63
|
+
labels: readonly string[];
|
|
64
|
+
validFrom: string;
|
|
65
|
+
validUntil: string | null;
|
|
66
|
+
supersedesId: string | null;
|
|
67
|
+
supersededById: string | null;
|
|
68
|
+
};
|
|
69
|
+
change: {
|
|
70
|
+
kind: MemorySlackChangeKind;
|
|
71
|
+
relatedMemoryId: string | null;
|
|
72
|
+
occurredAt: string;
|
|
73
|
+
origin: MemorySlackOrigin;
|
|
74
|
+
ownerLabel?: string | null | undefined;
|
|
75
|
+
};
|
|
76
|
+
distribution: {
|
|
77
|
+
importance: MemorySlackImportance;
|
|
78
|
+
audience: MemorySlackAudience;
|
|
79
|
+
slackMode: MemorySlackRequestedMode;
|
|
80
|
+
shareSummary: string;
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export type MemorySlackPublicationDenialReason =
|
|
85
|
+
| "disabled"
|
|
86
|
+
| "invalid_input"
|
|
87
|
+
| "unsupported_source"
|
|
88
|
+
| "tenant_mismatch"
|
|
89
|
+
| "not_decision"
|
|
90
|
+
| "status_ineligible"
|
|
91
|
+
| "restricted_scope"
|
|
92
|
+
| "restricted_audience"
|
|
93
|
+
| "not_yet_valid"
|
|
94
|
+
| "expired"
|
|
95
|
+
| "slack_origin_loop"
|
|
96
|
+
| "mode_never"
|
|
97
|
+
| "below_noise_policy"
|
|
98
|
+
| "missing_summary"
|
|
99
|
+
| "invalid_change_lineage";
|
|
100
|
+
|
|
101
|
+
export type MemorySlackPublicationProjection = {
|
|
102
|
+
version: typeof MEMORY_SLACK_PROJECTION_VERSION;
|
|
103
|
+
workspaceId: string;
|
|
104
|
+
memoryId: string;
|
|
105
|
+
memoryVersion: number;
|
|
106
|
+
changeKind: MemorySlackChangeKind;
|
|
107
|
+
relatedMemoryId: string | null;
|
|
108
|
+
occurredAt: string;
|
|
109
|
+
importance: MemorySlackImportance;
|
|
110
|
+
deliveryMode: MemorySlackDeliveryMode;
|
|
111
|
+
summary: string;
|
|
112
|
+
summaryRedacted: boolean;
|
|
113
|
+
summaryTruncated: boolean;
|
|
114
|
+
namespace: string;
|
|
115
|
+
labels: string[];
|
|
116
|
+
labelsTruncated: boolean;
|
|
117
|
+
ownerLabel: string | null;
|
|
118
|
+
ownerLabelRedacted: boolean;
|
|
119
|
+
ownerLabelTruncated: boolean;
|
|
120
|
+
authoritativeRecord: {
|
|
121
|
+
workspaceId: string;
|
|
122
|
+
memoryId: string;
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export type MemorySlackPublicationDecision =
|
|
127
|
+
| {
|
|
128
|
+
eligible: false;
|
|
129
|
+
reason: MemorySlackPublicationDenialReason;
|
|
130
|
+
}
|
|
131
|
+
| {
|
|
132
|
+
eligible: true;
|
|
133
|
+
deliveryMode: MemorySlackDeliveryMode;
|
|
134
|
+
idempotencyKey: string;
|
|
135
|
+
projection: MemorySlackPublicationProjection;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const IMPORTANCES = new Set<MemorySlackImportance>(["major", "normal", "minor"]);
|
|
139
|
+
const REQUESTED_MODES = new Set<MemorySlackRequestedMode>(["auto", "review", "never"]);
|
|
140
|
+
const CHANGE_KINDS = new Set<MemorySlackChangeKind>(["created", "corrected", "superseded"]);
|
|
141
|
+
const ORIGINS = new Set<MemorySlackOrigin>(["native", "slack_derived"]);
|
|
142
|
+
const AUDIENCES = new Set<MemorySlackAudience>(["workspace", "restricted"]);
|
|
143
|
+
const SCOPE_TYPES = new Set<MemorySlackPublicationInput["memory"]["scopeType"]>([
|
|
144
|
+
"workspace",
|
|
145
|
+
"user",
|
|
146
|
+
"role",
|
|
147
|
+
"session",
|
|
148
|
+
"ephemeral",
|
|
149
|
+
"legacy",
|
|
150
|
+
]);
|
|
151
|
+
const MEMORY_STATUSES = new Set<KnowledgeMemoryStatus>([
|
|
152
|
+
"proposed",
|
|
153
|
+
"approved",
|
|
154
|
+
"rejected",
|
|
155
|
+
"active",
|
|
156
|
+
"superseded",
|
|
157
|
+
"archived",
|
|
158
|
+
]);
|
|
159
|
+
const MEMORY_KINDS = new Set<KnowledgeMemoryKind>([
|
|
160
|
+
"semantic",
|
|
161
|
+
"episodic",
|
|
162
|
+
"procedural",
|
|
163
|
+
"decision",
|
|
164
|
+
"preference",
|
|
165
|
+
]);
|
|
166
|
+
const VISIBLE_STATUSES = new Set<KnowledgeMemoryStatus>(["active", "approved"]);
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Produce the only data shape a later delivery slice may persist or
|
|
170
|
+
* render. This is a deterministic policy/security boundary, not a persistence
|
|
171
|
+
* hook: callers must durably bind the returned projection and key in a later
|
|
172
|
+
* transaction/outbox slice.
|
|
173
|
+
*/
|
|
174
|
+
export function evaluateMemorySlackPublication(
|
|
175
|
+
input: MemorySlackPublicationInput,
|
|
176
|
+
): MemorySlackPublicationDecision {
|
|
177
|
+
const policy = input.policy ?? DEFAULT_MEMORY_SLACK_PUBLICATION_POLICY;
|
|
178
|
+
if (!validPolicy(policy)) return denied("invalid_input");
|
|
179
|
+
if (!policy.enabled) return denied("disabled");
|
|
180
|
+
|
|
181
|
+
if (input.memory.sourceType !== "workspace_memory") return denied("unsupported_source");
|
|
182
|
+
if (!validIdentity(input)) return denied("invalid_input");
|
|
183
|
+
if (
|
|
184
|
+
input.memory.accountId !== input.context.accountId ||
|
|
185
|
+
input.memory.workspaceId !== input.context.workspaceId
|
|
186
|
+
) {
|
|
187
|
+
return denied("tenant_mismatch");
|
|
188
|
+
}
|
|
189
|
+
if (input.memory.kind !== "decision") return denied("not_decision");
|
|
190
|
+
if (input.memory.scopeType !== "workspace") return denied("restricted_scope");
|
|
191
|
+
if (input.distribution.audience !== "workspace") return denied("restricted_audience");
|
|
192
|
+
if (input.change.origin === "slack_derived") return denied("slack_origin_loop");
|
|
193
|
+
if (input.distribution.slackMode === "never") return denied("mode_never");
|
|
194
|
+
|
|
195
|
+
const timeDecision = evaluateValidity(input);
|
|
196
|
+
if (timeDecision) return denied(timeDecision);
|
|
197
|
+
if (!statusEligibleForChange(input)) return denied("status_ineligible");
|
|
198
|
+
if (!validChangeLineage(input)) return denied("invalid_change_lineage");
|
|
199
|
+
|
|
200
|
+
const deliveryMode = effectiveDeliveryMode(policy, input.distribution);
|
|
201
|
+
if (!deliveryMode) return denied("below_noise_policy");
|
|
202
|
+
|
|
203
|
+
const collapsedSummary = collapseText(input.distribution.shareSummary);
|
|
204
|
+
const redactedSummary = redactSensitiveText(collapsedSummary);
|
|
205
|
+
if (!redactedSummary) return denied("missing_summary");
|
|
206
|
+
const summary = truncateUtf8(redactedSummary, MEMORY_SLACK_SUMMARY_MAX_UTF8_BYTES);
|
|
207
|
+
|
|
208
|
+
const namespace = normalizeNamespace(input.memory.namespace);
|
|
209
|
+
const labels = normalizeLabels(input.memory.labels);
|
|
210
|
+
if (!namespace || !labels) return denied("invalid_input");
|
|
211
|
+
|
|
212
|
+
const owner = boundedOptionalText(input.change.ownerLabel, MEMORY_SLACK_OWNER_MAX_UTF8_BYTES);
|
|
213
|
+
const projection: MemorySlackPublicationProjection = {
|
|
214
|
+
version: MEMORY_SLACK_PROJECTION_VERSION,
|
|
215
|
+
workspaceId: input.context.workspaceId,
|
|
216
|
+
memoryId: input.memory.id,
|
|
217
|
+
memoryVersion: input.memory.version,
|
|
218
|
+
changeKind: input.change.kind,
|
|
219
|
+
relatedMemoryId: input.change.relatedMemoryId,
|
|
220
|
+
occurredAt: new Date(input.change.occurredAt).toISOString(),
|
|
221
|
+
importance: input.distribution.importance,
|
|
222
|
+
deliveryMode,
|
|
223
|
+
summary: summary.value,
|
|
224
|
+
summaryRedacted: redactedSummary !== collapsedSummary,
|
|
225
|
+
summaryTruncated: summary.truncated,
|
|
226
|
+
namespace,
|
|
227
|
+
labels: labels.values,
|
|
228
|
+
labelsTruncated: labels.truncated,
|
|
229
|
+
ownerLabel: owner.value,
|
|
230
|
+
ownerLabelRedacted: owner.redacted,
|
|
231
|
+
ownerLabelTruncated: owner.truncated,
|
|
232
|
+
authoritativeRecord: {
|
|
233
|
+
workspaceId: input.context.workspaceId,
|
|
234
|
+
memoryId: input.memory.id,
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
if (utf8Bytes(stableJson(projection)) > MEMORY_SLACK_PROJECTION_MAX_UTF8_BYTES) {
|
|
239
|
+
return denied("invalid_input");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const digest = createHash("sha256").update(stableJson(projection), "utf8").digest("hex");
|
|
243
|
+
return {
|
|
244
|
+
eligible: true,
|
|
245
|
+
deliveryMode,
|
|
246
|
+
idempotencyKey: `memory-slack:v${MEMORY_SLACK_PROJECTION_VERSION}:${digest}`,
|
|
247
|
+
projection,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function validPolicy(policy: MemorySlackPublicationPolicy): boolean {
|
|
252
|
+
return (
|
|
253
|
+
typeof policy.enabled === "boolean" &&
|
|
254
|
+
Array.isArray(policy.autoImportances) &&
|
|
255
|
+
Array.isArray(policy.reviewImportances) &&
|
|
256
|
+
policy.autoImportances.every((importance) => IMPORTANCES.has(importance)) &&
|
|
257
|
+
policy.reviewImportances.every((importance) => IMPORTANCES.has(importance))
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function validIdentity(input: MemorySlackPublicationInput): boolean {
|
|
262
|
+
const uuids = [
|
|
263
|
+
input.context.accountId,
|
|
264
|
+
input.context.workspaceId,
|
|
265
|
+
input.memory.accountId,
|
|
266
|
+
input.memory.workspaceId,
|
|
267
|
+
input.memory.id,
|
|
268
|
+
input.memory.supersedesId,
|
|
269
|
+
input.memory.supersededById,
|
|
270
|
+
input.change.relatedMemoryId,
|
|
271
|
+
].filter((value): value is string => value !== null);
|
|
272
|
+
return (
|
|
273
|
+
uuids.every((value) => UUID_PATTERN.test(value)) &&
|
|
274
|
+
Number.isSafeInteger(input.memory.version) &&
|
|
275
|
+
input.memory.version > 0 &&
|
|
276
|
+
typeof input.memory.namespace === "string" &&
|
|
277
|
+
Array.isArray(input.memory.labels) &&
|
|
278
|
+
input.memory.labels.every((label) => typeof label === "string") &&
|
|
279
|
+
(input.change.ownerLabel === undefined ||
|
|
280
|
+
input.change.ownerLabel === null ||
|
|
281
|
+
typeof input.change.ownerLabel === "string") &&
|
|
282
|
+
typeof input.distribution.shareSummary === "string" &&
|
|
283
|
+
IMPORTANCES.has(input.distribution.importance) &&
|
|
284
|
+
REQUESTED_MODES.has(input.distribution.slackMode) &&
|
|
285
|
+
CHANGE_KINDS.has(input.change.kind) &&
|
|
286
|
+
ORIGINS.has(input.change.origin) &&
|
|
287
|
+
AUDIENCES.has(input.distribution.audience) &&
|
|
288
|
+
SCOPE_TYPES.has(input.memory.scopeType) &&
|
|
289
|
+
MEMORY_STATUSES.has(input.memory.status) &&
|
|
290
|
+
MEMORY_KINDS.has(input.memory.kind)
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function evaluateValidity(
|
|
295
|
+
input: MemorySlackPublicationInput,
|
|
296
|
+
): "invalid_input" | "not_yet_valid" | "expired" | null {
|
|
297
|
+
const now = Date.parse(input.context.now);
|
|
298
|
+
const occurredAt = Date.parse(input.change.occurredAt);
|
|
299
|
+
const validFrom = Date.parse(input.memory.validFrom);
|
|
300
|
+
const validUntil = input.memory.validUntil === null ? null : Date.parse(input.memory.validUntil);
|
|
301
|
+
if (
|
|
302
|
+
!Number.isFinite(now) ||
|
|
303
|
+
!Number.isFinite(occurredAt) ||
|
|
304
|
+
!Number.isFinite(validFrom) ||
|
|
305
|
+
(validUntil !== null && !Number.isFinite(validUntil)) ||
|
|
306
|
+
(validUntil !== null && validUntil <= validFrom)
|
|
307
|
+
) {
|
|
308
|
+
return "invalid_input";
|
|
309
|
+
}
|
|
310
|
+
if (now < validFrom) return "not_yet_valid";
|
|
311
|
+
if (validUntil !== null && now >= validUntil) return "expired";
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function validChangeLineage(input: MemorySlackPublicationInput): boolean {
|
|
316
|
+
if (
|
|
317
|
+
input.memory.supersedesId === input.memory.id ||
|
|
318
|
+
input.memory.supersededById === input.memory.id ||
|
|
319
|
+
input.change.relatedMemoryId === input.memory.id
|
|
320
|
+
) {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
switch (input.change.kind) {
|
|
325
|
+
case "created":
|
|
326
|
+
return (
|
|
327
|
+
input.change.relatedMemoryId === null &&
|
|
328
|
+
input.memory.supersedesId === null &&
|
|
329
|
+
input.memory.supersededById === null
|
|
330
|
+
);
|
|
331
|
+
case "corrected":
|
|
332
|
+
return (
|
|
333
|
+
input.change.relatedMemoryId !== null &&
|
|
334
|
+
input.change.relatedMemoryId !== input.memory.id &&
|
|
335
|
+
input.memory.supersedesId === input.change.relatedMemoryId &&
|
|
336
|
+
input.memory.supersededById === null
|
|
337
|
+
);
|
|
338
|
+
case "superseded":
|
|
339
|
+
return (
|
|
340
|
+
input.change.relatedMemoryId !== null &&
|
|
341
|
+
input.change.relatedMemoryId !== input.memory.id &&
|
|
342
|
+
input.memory.supersededById === input.change.relatedMemoryId
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function statusEligibleForChange(input: MemorySlackPublicationInput): boolean {
|
|
348
|
+
return input.change.kind === "superseded"
|
|
349
|
+
? input.memory.status === "superseded"
|
|
350
|
+
: VISIBLE_STATUSES.has(input.memory.status);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function effectiveDeliveryMode(
|
|
354
|
+
policy: MemorySlackPublicationPolicy,
|
|
355
|
+
distribution: MemorySlackPublicationInput["distribution"],
|
|
356
|
+
): MemorySlackDeliveryMode | null {
|
|
357
|
+
const auto = new Set(policy.autoImportances);
|
|
358
|
+
const review = new Set(policy.reviewImportances);
|
|
359
|
+
if (distribution.slackMode === "review") {
|
|
360
|
+
return auto.has(distribution.importance) || review.has(distribution.importance)
|
|
361
|
+
? "review"
|
|
362
|
+
: null;
|
|
363
|
+
}
|
|
364
|
+
if (auto.has(distribution.importance)) return "auto";
|
|
365
|
+
if (review.has(distribution.importance)) return "review";
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function normalizeNamespace(value: string): string | null {
|
|
370
|
+
const trimmed = value.trim();
|
|
371
|
+
if (redactSensitiveText(trimmed) !== trimmed) return null;
|
|
372
|
+
const namespace = trimmed.toLowerCase();
|
|
373
|
+
if (!namespace || utf8Bytes(namespace) > MEMORY_SLACK_NAMESPACE_MAX_UTF8_BYTES) return null;
|
|
374
|
+
if (redactSensitiveText(namespace) !== namespace) return null;
|
|
375
|
+
const segments = namespace.split("/");
|
|
376
|
+
if (segments.some((segment) => !SELECTOR_SEGMENT_PATTERN.test(segment))) return null;
|
|
377
|
+
return segments.join("/");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function normalizeLabels(
|
|
381
|
+
values: readonly string[],
|
|
382
|
+
): { values: string[]; truncated: boolean } | null {
|
|
383
|
+
if (!Array.isArray(values)) return null;
|
|
384
|
+
const labels = new Set<string>();
|
|
385
|
+
for (const value of values) {
|
|
386
|
+
if (typeof value !== "string") return null;
|
|
387
|
+
const trimmed = value.trim();
|
|
388
|
+
if (redactSensitiveText(trimmed) !== trimmed) return null;
|
|
389
|
+
const label = trimmed.toLowerCase();
|
|
390
|
+
if (
|
|
391
|
+
redactSensitiveText(label) !== label ||
|
|
392
|
+
!SELECTOR_SEGMENT_PATTERN.test(label) ||
|
|
393
|
+
utf8Bytes(label) > MEMORY_SLACK_LABEL_MAX_UTF8_BYTES
|
|
394
|
+
) {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
labels.add(label);
|
|
398
|
+
}
|
|
399
|
+
const sorted = [...labels].sort();
|
|
400
|
+
return {
|
|
401
|
+
values: sorted.slice(0, MEMORY_SLACK_LABEL_MAX_COUNT),
|
|
402
|
+
truncated: sorted.length > MEMORY_SLACK_LABEL_MAX_COUNT,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function boundedOptionalText(
|
|
407
|
+
value: string | null | undefined,
|
|
408
|
+
maxBytes: number,
|
|
409
|
+
): { value: string | null; redacted: boolean; truncated: boolean } {
|
|
410
|
+
const collapsed = collapseText(value ?? "");
|
|
411
|
+
if (!collapsed) return { value: null, redacted: false, truncated: false };
|
|
412
|
+
const redacted = redactSensitiveText(collapsed);
|
|
413
|
+
const bounded = truncateUtf8(redacted, maxBytes);
|
|
414
|
+
return {
|
|
415
|
+
value: bounded.value || null,
|
|
416
|
+
redacted: redacted !== collapsed,
|
|
417
|
+
truncated: bounded.truncated,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function collapseText(value: string): string {
|
|
422
|
+
// eslint-disable-next-line no-control-regex
|
|
423
|
+
return value
|
|
424
|
+
.replace(/[\u0000-\u001F\u007F-\u009F]/g, " ")
|
|
425
|
+
.replace(/\s+/g, " ")
|
|
426
|
+
.trim();
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function truncateUtf8(value: string, maxBytes: number): { value: string; truncated: boolean } {
|
|
430
|
+
const bytes = encoder.encode(value);
|
|
431
|
+
if (bytes.byteLength <= maxBytes) return { value, truncated: false };
|
|
432
|
+
const marker = "…";
|
|
433
|
+
const markerBytes = encoder.encode(marker);
|
|
434
|
+
let end = Math.max(0, maxBytes - markerBytes.byteLength);
|
|
435
|
+
while (end > 0 && (bytes[end]! & 0xc0) === 0x80) end -= 1;
|
|
436
|
+
return {
|
|
437
|
+
value: `${decoder.decode(bytes.subarray(0, end)).trimEnd()}${marker}`,
|
|
438
|
+
truncated: true,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function utf8Bytes(value: string): number {
|
|
443
|
+
return encoder.encode(value).byteLength;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function denied(reason: MemorySlackPublicationDenialReason): MemorySlackPublicationDecision {
|
|
447
|
+
return { eligible: false, reason };
|
|
448
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -63,6 +63,7 @@ export * from "./domain/session-tool-policy";
|
|
|
63
63
|
export * from "./domain/scheduled-tasks";
|
|
64
64
|
export * from "./domain/sessions";
|
|
65
65
|
export * from "./domain/insights";
|
|
66
|
+
export * from "./domain/memory-slack-publication";
|
|
66
67
|
export * from "./domain/slack-bot";
|
|
67
68
|
export * from "./domain/workspace-members";
|
|
68
69
|
export * from "./application/new-session-drafts";
|