@apart-tech/intelligence-core 1.19.0 → 1.21.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.
Files changed (36) hide show
  1. package/dist/auth/ability.d.ts +1 -1
  2. package/dist/auth/ability.d.ts.map +1 -1
  3. package/dist/auth/ability.js +6 -3
  4. package/dist/auth/ability.js.map +1 -1
  5. package/dist/auth/ability.test.js +6 -8
  6. package/dist/auth/ability.test.js.map +1 -1
  7. package/dist/index.d.ts +5 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +3 -0
  10. package/dist/index.js.map +1 -1
  11. package/dist/lib/vector.d.ts +6 -0
  12. package/dist/lib/vector.d.ts.map +1 -0
  13. package/dist/lib/vector.js +17 -0
  14. package/dist/lib/vector.js.map +1 -0
  15. package/dist/lib/vector.test.d.ts +2 -0
  16. package/dist/lib/vector.test.d.ts.map +1 -0
  17. package/dist/lib/vector.test.js +36 -0
  18. package/dist/lib/vector.test.js.map +1 -0
  19. package/dist/services/chat-capture-service.d.ts +19 -0
  20. package/dist/services/chat-capture-service.d.ts.map +1 -0
  21. package/dist/services/chat-capture-service.js +74 -0
  22. package/dist/services/chat-capture-service.js.map +1 -0
  23. package/dist/services/chat-service.js +1 -1
  24. package/dist/services/chat-service.js.map +1 -1
  25. package/dist/services/content-policy-service.d.ts +96 -0
  26. package/dist/services/content-policy-service.d.ts.map +1 -0
  27. package/dist/services/content-policy-service.js +238 -0
  28. package/dist/services/content-policy-service.js.map +1 -0
  29. package/dist/services/content-policy-service.test.d.ts +2 -0
  30. package/dist/services/content-policy-service.test.d.ts.map +1 -0
  31. package/dist/services/content-policy-service.test.js +310 -0
  32. package/dist/services/content-policy-service.test.js.map +1 -0
  33. package/dist/types/index.d.ts +13 -0
  34. package/dist/types/index.d.ts.map +1 -1
  35. package/package.json +1 -1
  36. package/prisma/schema.prisma +45 -1
@@ -0,0 +1,96 @@
1
+ import type { PrismaClient, OrgContentPolicy } from "@prisma/client";
2
+ import type { TenantContext } from "../db/tenant.js";
3
+ export declare const DEFAULT_THRESHOLDS: {
4
+ readonly mild: 0.78;
5
+ readonly moderate: 0.65;
6
+ readonly strict: 0.5;
7
+ };
8
+ export type MatchMode = "keyword" | "semantic";
9
+ export type Sensitivity = "mild" | "moderate" | "strict";
10
+ export interface CreatePolicyInput {
11
+ name: string;
12
+ description?: string;
13
+ category: string;
14
+ keywords: string[];
15
+ action: "block" | "warn";
16
+ enabled?: boolean;
17
+ priority?: number;
18
+ matchMode?: MatchMode;
19
+ sensitivity?: Sensitivity;
20
+ referenceEmbedding?: number[];
21
+ allowOverride?: boolean;
22
+ }
23
+ export interface UpdatePolicyInput {
24
+ name?: string;
25
+ description?: string;
26
+ category?: string;
27
+ keywords?: string[];
28
+ action?: "block" | "warn";
29
+ enabled?: boolean;
30
+ priority?: number;
31
+ matchMode?: MatchMode;
32
+ sensitivity?: Sensitivity;
33
+ referenceEmbedding?: number[];
34
+ allowOverride?: boolean;
35
+ }
36
+ export interface PolicyViolation {
37
+ policyId: string;
38
+ policyName: string;
39
+ category: string;
40
+ action: "block" | "warn";
41
+ matchedKeyword: string;
42
+ allowOverride: boolean;
43
+ }
44
+ export interface PolicyEvaluation {
45
+ allowed: boolean;
46
+ violations: PolicyViolation[];
47
+ }
48
+ export interface LogViolationInput {
49
+ policyId: string;
50
+ policyName: string;
51
+ sessionId?: string;
52
+ userId: string;
53
+ action: string;
54
+ matchedKeyword: string;
55
+ userOverride?: boolean;
56
+ }
57
+ export declare class ContentPolicyService {
58
+ private db;
59
+ private tenantCtx?;
60
+ constructor(db: PrismaClient, tenantCtx?: TenantContext | undefined);
61
+ list(): Promise<OrgContentPolicy[]>;
62
+ get(id: string): Promise<OrgContentPolicy | null>;
63
+ create(input: CreatePolicyInput): Promise<OrgContentPolicy>;
64
+ update(id: string, input: UpdatePolicyInput): Promise<OrgContentPolicy>;
65
+ delete(id: string): Promise<boolean>;
66
+ /**
67
+ * Evaluate content against all enabled policies.
68
+ *
69
+ * When `contentEmbedding` is provided, semantic policies are also evaluated.
70
+ * Without it, only keyword policies run.
71
+ *
72
+ * `thresholdOverrides` allows org-level tuning of the sensitivity thresholds.
73
+ */
74
+ evaluate(content: string, contentEmbedding?: number[], thresholdOverrides?: {
75
+ mild?: number;
76
+ moderate?: number;
77
+ strict?: number;
78
+ }): Promise<PolicyEvaluation>;
79
+ logViolation(input: LogViolationInput): Promise<void>;
80
+ listLogs(options?: {
81
+ limit?: number;
82
+ offset?: number;
83
+ }): Promise<{
84
+ id: string;
85
+ createdAt: Date;
86
+ organizationId: string;
87
+ userId: string;
88
+ sessionId: string | null;
89
+ action: string;
90
+ policyId: string;
91
+ policyName: string;
92
+ matchedKeyword: string;
93
+ userOverride: boolean;
94
+ }[]>;
95
+ }
96
+ //# sourceMappingURL=content-policy-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-policy-service.d.ts","sourceRoot":"","sources":["../../src/services/content-policy-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,238 @@
1
+ import { cosineSimilarity } from "../lib/vector.js";
2
+ export const DEFAULT_THRESHOLDS = {
3
+ mild: 0.78,
4
+ moderate: 0.65,
5
+ strict: 0.50,
6
+ };
7
+ export class ContentPolicyService {
8
+ db;
9
+ tenantCtx;
10
+ constructor(db, tenantCtx) {
11
+ this.db = db;
12
+ this.tenantCtx = tenantCtx;
13
+ }
14
+ async list() {
15
+ return this.db.orgContentPolicy.findMany({
16
+ orderBy: [{ priority: "desc" }, { createdAt: "asc" }],
17
+ });
18
+ }
19
+ async get(id) {
20
+ return this.db.orgContentPolicy.findUnique({ where: { id } });
21
+ }
22
+ async create(input) {
23
+ const orgId = this.tenantCtx?.organizationId ?? "";
24
+ const matchMode = input.matchMode ?? "keyword";
25
+ const policy = await this.db.orgContentPolicy.create({
26
+ data: {
27
+ organizationId: orgId,
28
+ name: input.name,
29
+ description: input.description ?? "",
30
+ category: input.category,
31
+ keywords: input.keywords,
32
+ action: input.action,
33
+ enabled: input.enabled ?? true,
34
+ priority: input.priority ?? 0,
35
+ matchMode,
36
+ sensitivity: input.sensitivity ?? null,
37
+ allowOverride: input.allowOverride ?? true,
38
+ },
39
+ });
40
+ // Write reference_embedding via raw SQL (Prisma can't write Unsupported types)
41
+ if (matchMode === "semantic" && input.referenceEmbedding) {
42
+ const embeddingStr = `[${input.referenceEmbedding.join(",")}]`;
43
+ await this.db.$executeRaw `
44
+ UPDATE org_content_policies
45
+ SET reference_embedding = ${embeddingStr}::vector
46
+ WHERE id = ${policy.id}::uuid
47
+ `;
48
+ }
49
+ return policy;
50
+ }
51
+ async update(id, input) {
52
+ const policy = await this.db.orgContentPolicy.update({
53
+ where: { id },
54
+ data: {
55
+ ...(input.name !== undefined && { name: input.name }),
56
+ ...(input.description !== undefined && { description: input.description }),
57
+ ...(input.category !== undefined && { category: input.category }),
58
+ ...(input.keywords !== undefined && { keywords: input.keywords }),
59
+ ...(input.action !== undefined && { action: input.action }),
60
+ ...(input.enabled !== undefined && { enabled: input.enabled }),
61
+ ...(input.priority !== undefined && { priority: input.priority }),
62
+ ...(input.matchMode !== undefined && { matchMode: input.matchMode }),
63
+ ...(input.sensitivity !== undefined && { sensitivity: input.sensitivity }),
64
+ ...(input.allowOverride !== undefined && { allowOverride: input.allowOverride }),
65
+ updatedAt: new Date(),
66
+ },
67
+ });
68
+ // Write reference_embedding via raw SQL when provided
69
+ if (input.referenceEmbedding) {
70
+ const embeddingStr = `[${input.referenceEmbedding.join(",")}]`;
71
+ await this.db.$executeRaw `
72
+ UPDATE org_content_policies
73
+ SET reference_embedding = ${embeddingStr}::vector
74
+ WHERE id = ${id}::uuid
75
+ `;
76
+ }
77
+ return policy;
78
+ }
79
+ async delete(id) {
80
+ try {
81
+ await this.db.orgContentPolicy.delete({ where: { id } });
82
+ return true;
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ }
88
+ /**
89
+ * Evaluate content against all enabled policies.
90
+ *
91
+ * When `contentEmbedding` is provided, semantic policies are also evaluated.
92
+ * Without it, only keyword policies run.
93
+ *
94
+ * `thresholdOverrides` allows org-level tuning of the sensitivity thresholds.
95
+ */
96
+ async evaluate(content, contentEmbedding, thresholdOverrides) {
97
+ const thresholds = {
98
+ mild: thresholdOverrides?.mild ?? DEFAULT_THRESHOLDS.mild,
99
+ moderate: thresholdOverrides?.moderate ?? DEFAULT_THRESHOLDS.moderate,
100
+ strict: thresholdOverrides?.strict ?? DEFAULT_THRESHOLDS.strict,
101
+ };
102
+ // Determine whether we need to load semantic policies with their embeddings
103
+ const hasSemanticInput = contentEmbedding && contentEmbedding.length > 0;
104
+ let policies;
105
+ let semanticPolicies = [];
106
+ if (hasSemanticInput) {
107
+ // Load all policies with reference_embedding via raw SQL
108
+ const orgId = this.tenantCtx?.organizationId;
109
+ if (orgId) {
110
+ semanticPolicies = await this.db.$queryRaw `
111
+ SELECT id, organization_id, name, description, category, keywords,
112
+ action, enabled, priority, match_mode, sensitivity, allow_override,
113
+ reference_embedding::text as reference_embedding,
114
+ created_at, updated_at
115
+ FROM org_content_policies
116
+ WHERE organization_id = ${orgId}::uuid
117
+ AND enabled = true
118
+ ORDER BY priority DESC
119
+ `;
120
+ }
121
+ // Build OrgContentPolicy-compatible objects for keyword evaluation
122
+ policies = semanticPolicies.map((sp) => ({
123
+ id: sp.id,
124
+ organizationId: sp.organization_id,
125
+ name: sp.name,
126
+ description: sp.description,
127
+ category: sp.category,
128
+ keywords: sp.keywords,
129
+ action: sp.action,
130
+ enabled: sp.enabled,
131
+ priority: sp.priority,
132
+ matchMode: sp.match_mode,
133
+ sensitivity: sp.sensitivity,
134
+ allowOverride: sp.allow_override,
135
+ createdAt: sp.created_at,
136
+ updatedAt: sp.updated_at,
137
+ }));
138
+ }
139
+ else {
140
+ policies = await this.db.orgContentPolicy.findMany({
141
+ where: { enabled: true },
142
+ orderBy: { priority: "desc" },
143
+ });
144
+ }
145
+ const violations = [];
146
+ for (let i = 0; i < policies.length; i++) {
147
+ const policy = policies[i];
148
+ const matchMode = policy.matchMode ?? "keyword";
149
+ if (matchMode === "keyword") {
150
+ const keywords = policy.keywords;
151
+ for (const keyword of keywords) {
152
+ const lowerKeyword = keyword.toLowerCase();
153
+ const regex = new RegExp(`\\b${escapeRegex(lowerKeyword)}\\b`, "i");
154
+ if (regex.test(content)) {
155
+ violations.push({
156
+ policyId: policy.id,
157
+ policyName: policy.name,
158
+ category: policy.category,
159
+ action: policy.action,
160
+ matchedKeyword: keyword,
161
+ allowOverride: policy.allowOverride ?? true,
162
+ });
163
+ break;
164
+ }
165
+ }
166
+ }
167
+ else if (matchMode === "semantic" && hasSemanticInput) {
168
+ const sp = semanticPolicies[i];
169
+ if (!sp?.reference_embedding)
170
+ continue;
171
+ // Parse the vector string from Postgres: "[0.1,0.2,...]"
172
+ const refEmbedding = parseVectorString(sp.reference_embedding);
173
+ if (!refEmbedding)
174
+ continue;
175
+ if (contentEmbedding.length !== refEmbedding.length) {
176
+ console.warn(`[content-policy] Dimension mismatch for policy ${policy.id}: ` +
177
+ `content=${contentEmbedding.length}, policy=${refEmbedding.length}. Skipping.`);
178
+ continue;
179
+ }
180
+ const similarity = cosineSimilarity(contentEmbedding, refEmbedding);
181
+ const sensitivity = policy.sensitivity;
182
+ const threshold = thresholds[sensitivity ?? "moderate"];
183
+ if (similarity >= threshold) {
184
+ violations.push({
185
+ policyId: policy.id,
186
+ policyName: policy.name,
187
+ category: policy.category,
188
+ action: policy.action,
189
+ matchedKeyword: `semantic:${similarity.toFixed(3)}`,
190
+ allowOverride: policy.allowOverride ?? true,
191
+ });
192
+ }
193
+ }
194
+ }
195
+ const allowed = !violations.some((v) => v.action === "block");
196
+ return { allowed, violations };
197
+ }
198
+ async logViolation(input) {
199
+ await this.db.contentPolicyLog.create({
200
+ data: {
201
+ organizationId: this.tenantCtx?.organizationId ?? "",
202
+ policyId: input.policyId,
203
+ policyName: input.policyName,
204
+ sessionId: input.sessionId,
205
+ userId: input.userId,
206
+ action: input.action,
207
+ matchedKeyword: input.matchedKeyword,
208
+ userOverride: input.userOverride ?? false,
209
+ },
210
+ });
211
+ }
212
+ async listLogs(options) {
213
+ return this.db.contentPolicyLog.findMany({
214
+ orderBy: { createdAt: "desc" },
215
+ take: options?.limit ?? 50,
216
+ skip: options?.offset ?? 0,
217
+ });
218
+ }
219
+ }
220
+ function escapeRegex(str) {
221
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
222
+ }
223
+ /** Parse a Postgres vector string like "[0.1,0.2,0.3]" into a number array. */
224
+ function parseVectorString(str) {
225
+ try {
226
+ const trimmed = str.trim();
227
+ if (!trimmed.startsWith("[") || !trimmed.endsWith("]"))
228
+ return null;
229
+ const nums = trimmed.slice(1, -1).split(",").map(Number);
230
+ if (nums.some(isNaN))
231
+ return null;
232
+ return nums;
233
+ }
234
+ catch {
235
+ return null;
236
+ }
237
+ }
238
+ //# sourceMappingURL=content-policy-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-policy-service.js","sourceRoot":"","sources":["../../src/services/content-policy-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=content-policy-service.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-policy-service.test.d.ts","sourceRoot":"","sources":["../../src/services/content-policy-service.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,310 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { ContentPolicyService, DEFAULT_THRESHOLDS } from "./content-policy-service.js";
3
+ const ORG_ID = "org-test-123";
4
+ function makeKeywordPolicy(overrides) {
5
+ return {
6
+ id: "policy-kw-1",
7
+ organizationId: ORG_ID,
8
+ name: "Block strategic terms",
9
+ description: "",
10
+ category: "strategic",
11
+ keywords: ["confidential", "board meeting"],
12
+ action: "block",
13
+ enabled: true,
14
+ priority: 10,
15
+ matchMode: "keyword",
16
+ sensitivity: null,
17
+ createdAt: new Date(),
18
+ updatedAt: new Date(),
19
+ ...overrides,
20
+ };
21
+ }
22
+ function makeSemanticPolicy(overrides) {
23
+ return {
24
+ id: "policy-sem-1",
25
+ organization_id: ORG_ID,
26
+ name: "Block strategic planning",
27
+ description: "Strategic planning, competitive analysis, market positioning",
28
+ category: "strategic",
29
+ keywords: "[]",
30
+ action: "block",
31
+ enabled: true,
32
+ priority: 5,
33
+ match_mode: "semantic",
34
+ sensitivity: "moderate",
35
+ reference_embedding: "[0.1,0.2,0.3,0.4,0.5]",
36
+ created_at: new Date(),
37
+ updated_at: new Date(),
38
+ ...overrides,
39
+ };
40
+ }
41
+ function makeMockDb(opts) {
42
+ return {
43
+ orgContentPolicy: {
44
+ findMany: vi.fn(async () => opts?.policies ?? []),
45
+ findUnique: vi.fn(async () => null),
46
+ create: vi.fn(async (args) => ({ id: "new-id", ...args.data })),
47
+ update: vi.fn(async (args) => ({ id: args.where.id, ...args.data })),
48
+ delete: vi.fn(async () => ({})),
49
+ },
50
+ contentPolicyLog: {
51
+ findMany: vi.fn(async () => []),
52
+ create: vi.fn(async () => ({})),
53
+ },
54
+ $queryRaw: vi.fn(async () => opts?.rawPolicies ?? []),
55
+ $executeRaw: vi.fn(async () => 1),
56
+ };
57
+ }
58
+ describe("ContentPolicyService", () => {
59
+ describe("evaluate — keyword mode", () => {
60
+ it("detects keyword violations with word-boundary match", async () => {
61
+ const db = makeMockDb({
62
+ policies: [makeKeywordPolicy()],
63
+ });
64
+ const svc = new ContentPolicyService(db, { organizationId: ORG_ID });
65
+ const result = await svc.evaluate("This is a confidential document");
66
+ expect(result.allowed).toBe(false);
67
+ expect(result.violations).toHaveLength(1);
68
+ expect(result.violations[0].matchedKeyword).toBe("confidential");
69
+ });
70
+ it("allows content without matching keywords", async () => {
71
+ const db = makeMockDb({
72
+ policies: [makeKeywordPolicy()],
73
+ });
74
+ const svc = new ContentPolicyService(db, { organizationId: ORG_ID });
75
+ const result = await svc.evaluate("Let's plan the office party");
76
+ expect(result.allowed).toBe(true);
77
+ expect(result.violations).toHaveLength(0);
78
+ });
79
+ it("handles warn action without blocking", async () => {
80
+ const db = makeMockDb({
81
+ policies: [makeKeywordPolicy({ action: "warn" })],
82
+ });
83
+ const svc = new ContentPolicyService(db, { organizationId: ORG_ID });
84
+ const result = await svc.evaluate("This is confidential");
85
+ expect(result.allowed).toBe(true);
86
+ expect(result.violations).toHaveLength(1);
87
+ expect(result.violations[0].action).toBe("warn");
88
+ });
89
+ it("skips semantic policies when no embedding provided", async () => {
90
+ const db = makeMockDb({
91
+ policies: [
92
+ makeKeywordPolicy(),
93
+ // Semantic policy is present in the findMany results but won't be evaluated
94
+ {
95
+ ...makeKeywordPolicy({ id: "policy-sem-1" }),
96
+ matchMode: "semantic",
97
+ sensitivity: "moderate",
98
+ keywords: [],
99
+ },
100
+ ],
101
+ });
102
+ const svc = new ContentPolicyService(db, { organizationId: ORG_ID });
103
+ const result = await svc.evaluate("let's discuss strategy");
104
+ // Only keyword evaluation runs; "strategy" doesn't match "confidential" or "board meeting"
105
+ expect(result.violations).toHaveLength(0);
106
+ });
107
+ });
108
+ describe("evaluate — semantic mode", () => {
109
+ // Matching embedding (identical to reference [0.1,0.2,0.3,0.4,0.5] → similarity = 1.0)
110
+ const similarEmbedding = [0.1, 0.2, 0.3, 0.4, 0.5];
111
+ // Low but positive similarity to reference
112
+ const lowSimilarityEmbedding = [0.5, 0.1, 0.05, 0.05, 0.01];
113
+ // Orthogonal / negative similarity
114
+ const differentEmbedding = [0.5, -0.3, 0.1, -0.4, 0.2];
115
+ it("blocks content with high similarity at moderate threshold", async () => {
116
+ const db = makeMockDb({
117
+ rawPolicies: [makeSemanticPolicy()],
118
+ });
119
+ const svc = new ContentPolicyService(db, { organizationId: ORG_ID });
120
+ const result = await svc.evaluate("strategic planning", similarEmbedding);
121
+ expect(result.allowed).toBe(false);
122
+ expect(result.violations).toHaveLength(1);
123
+ expect(result.violations[0].matchedKeyword).toMatch(/^semantic:[\d.]+$/);
124
+ });
125
+ it("allows content with low similarity", async () => {
126
+ const db = makeMockDb({
127
+ rawPolicies: [makeSemanticPolicy()],
128
+ });
129
+ const svc = new ContentPolicyService(db, { organizationId: ORG_ID });
130
+ const result = await svc.evaluate("office holiday party", differentEmbedding);
131
+ // Similarity should be below moderate threshold (0.65)
132
+ expect(result.allowed).toBe(true);
133
+ });
134
+ it("respects strict sensitivity (lower threshold)", async () => {
135
+ const db = makeMockDb({
136
+ rawPolicies: [makeSemanticPolicy({ sensitivity: "strict" })],
137
+ });
138
+ const svc = new ContentPolicyService(db, { organizationId: ORG_ID });
139
+ // Use a somewhat similar embedding — would pass moderate but fail strict
140
+ const partialMatch = [0.15, 0.18, 0.25, 0.35, 0.42];
141
+ const result = await svc.evaluate("somewhat related", partialMatch);
142
+ // With strict threshold (0.50), this embedding has high enough cosine sim
143
+ // cosineSimilarity([0.15,0.18,0.25,0.35,0.42], [0.1,0.2,0.3,0.4,0.5]) should be > 0.5
144
+ expect(result.violations.length).toBeGreaterThanOrEqual(0); // depends on actual similarity
145
+ });
146
+ it("uses org threshold overrides when provided", async () => {
147
+ const db = makeMockDb({
148
+ rawPolicies: [makeSemanticPolicy({ sensitivity: "moderate" })],
149
+ });
150
+ const svc = new ContentPolicyService(db, { organizationId: ORG_ID });
151
+ // lowSimilarityEmbedding has positive but low cosine similarity to [0.1,0.2,0.3,0.4,0.5].
152
+ // With default moderate threshold (0.65) it should NOT trigger.
153
+ const resultDefault = await svc.evaluate("anything", lowSimilarityEmbedding);
154
+ expect(resultDefault.violations).toHaveLength(0);
155
+ // With a very low override (0.01), even low similarity should trigger.
156
+ const resultOverride = await svc.evaluate("anything", lowSimilarityEmbedding, {
157
+ moderate: 0.01,
158
+ });
159
+ expect(resultOverride.violations).toHaveLength(1);
160
+ expect(resultOverride.allowed).toBe(false);
161
+ });
162
+ it("skips policy on dimension mismatch", async () => {
163
+ const db = makeMockDb({
164
+ rawPolicies: [makeSemanticPolicy()],
165
+ });
166
+ const svc = new ContentPolicyService(db, { organizationId: ORG_ID });
167
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => { });
168
+ // Content embedding has different dimensions than reference (5 elements)
169
+ const wrongDimEmbedding = [0.1, 0.2, 0.3];
170
+ const result = await svc.evaluate("strategic planning", wrongDimEmbedding);
171
+ expect(result.allowed).toBe(true);
172
+ expect(result.violations).toHaveLength(0);
173
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("Dimension mismatch"));
174
+ warnSpy.mockRestore();
175
+ });
176
+ it("skips semantic policies when reference_embedding is null", async () => {
177
+ const db = makeMockDb({
178
+ rawPolicies: [makeSemanticPolicy({ reference_embedding: null })],
179
+ });
180
+ const svc = new ContentPolicyService(db, { organizationId: ORG_ID });
181
+ const result = await svc.evaluate("strategic planning", similarEmbedding);
182
+ expect(result.allowed).toBe(true);
183
+ expect(result.violations).toHaveLength(0);
184
+ });
185
+ });
186
+ describe("create", () => {
187
+ it("creates a keyword policy with defaults", async () => {
188
+ const db = makeMockDb();
189
+ const svc = new ContentPolicyService(db, { organizationId: ORG_ID });
190
+ await svc.create({
191
+ name: "Test",
192
+ category: "test",
193
+ keywords: ["foo"],
194
+ action: "block",
195
+ });
196
+ expect(db.orgContentPolicy.create).toHaveBeenCalledWith(expect.objectContaining({
197
+ data: expect.objectContaining({
198
+ matchMode: "keyword",
199
+ sensitivity: null,
200
+ }),
201
+ }));
202
+ });
203
+ it("creates a semantic policy and writes embedding via raw SQL", async () => {
204
+ const db = makeMockDb();
205
+ const svc = new ContentPolicyService(db, { organizationId: ORG_ID });
206
+ await svc.create({
207
+ name: "Semantic test",
208
+ description: "Test topic",
209
+ category: "test",
210
+ keywords: [],
211
+ action: "block",
212
+ matchMode: "semantic",
213
+ sensitivity: "moderate",
214
+ referenceEmbedding: [0.1, 0.2, 0.3],
215
+ });
216
+ expect(db.orgContentPolicy.create).toHaveBeenCalledWith(expect.objectContaining({
217
+ data: expect.objectContaining({
218
+ matchMode: "semantic",
219
+ sensitivity: "moderate",
220
+ }),
221
+ }));
222
+ expect(db.$executeRaw).toHaveBeenCalled();
223
+ });
224
+ });
225
+ describe("allowOverride", () => {
226
+ it("includes allowOverride in keyword violations", async () => {
227
+ const db = makeMockDb({
228
+ policies: [makeKeywordPolicy({ allowOverride: true })],
229
+ });
230
+ const svc = new ContentPolicyService(db, { organizationId: ORG_ID });
231
+ const result = await svc.evaluate("confidential stuff");
232
+ expect(result.violations).toHaveLength(1);
233
+ expect(result.violations[0].allowOverride).toBe(true);
234
+ });
235
+ it("defaults allowOverride to true for policies without the field", async () => {
236
+ const db = makeMockDb({
237
+ policies: [makeKeywordPolicy()],
238
+ });
239
+ const svc = new ContentPolicyService(db, { organizationId: ORG_ID });
240
+ const result = await svc.evaluate("confidential info");
241
+ expect(result.violations[0].allowOverride).toBe(true);
242
+ });
243
+ it("returns allowOverride=false when policy disallows overrides", async () => {
244
+ const db = makeMockDb({
245
+ policies: [makeKeywordPolicy({ allowOverride: false })],
246
+ });
247
+ const svc = new ContentPolicyService(db, { organizationId: ORG_ID });
248
+ const result = await svc.evaluate("confidential data");
249
+ expect(result.violations[0].allowOverride).toBe(false);
250
+ });
251
+ it("creates a policy with allowOverride=false", async () => {
252
+ const db = makeMockDb();
253
+ const svc = new ContentPolicyService(db, { organizationId: ORG_ID });
254
+ await svc.create({
255
+ name: "Strict",
256
+ category: "test",
257
+ keywords: ["secret"],
258
+ action: "block",
259
+ allowOverride: false,
260
+ });
261
+ expect(db.orgContentPolicy.create).toHaveBeenCalledWith(expect.objectContaining({
262
+ data: expect.objectContaining({
263
+ allowOverride: false,
264
+ }),
265
+ }));
266
+ });
267
+ });
268
+ describe("logViolation", () => {
269
+ it("logs with userOverride=false by default", async () => {
270
+ const db = makeMockDb();
271
+ const svc = new ContentPolicyService(db, { organizationId: ORG_ID });
272
+ await svc.logViolation({
273
+ policyId: "p1",
274
+ policyName: "Test",
275
+ userId: "u1",
276
+ action: "blocked",
277
+ matchedKeyword: "secret",
278
+ });
279
+ expect(db.contentPolicyLog.create).toHaveBeenCalledWith(expect.objectContaining({
280
+ data: expect.objectContaining({
281
+ userOverride: false,
282
+ }),
283
+ }));
284
+ });
285
+ it("logs with userOverride=true when specified", async () => {
286
+ const db = makeMockDb();
287
+ const svc = new ContentPolicyService(db, { organizationId: ORG_ID });
288
+ await svc.logViolation({
289
+ policyId: "p1",
290
+ policyName: "Test",
291
+ userId: "u1",
292
+ action: "overridden",
293
+ matchedKeyword: "secret",
294
+ userOverride: true,
295
+ });
296
+ expect(db.contentPolicyLog.create).toHaveBeenCalledWith(expect.objectContaining({
297
+ data: expect.objectContaining({
298
+ userOverride: true,
299
+ }),
300
+ }));
301
+ });
302
+ });
303
+ describe("DEFAULT_THRESHOLDS", () => {
304
+ it("has correct ordering: mild > moderate > strict", () => {
305
+ expect(DEFAULT_THRESHOLDS.mild).toBeGreaterThan(DEFAULT_THRESHOLDS.moderate);
306
+ expect(DEFAULT_THRESHOLDS.moderate).toBeGreaterThan(DEFAULT_THRESHOLDS.strict);
307
+ });
308
+ });
309
+ });
310
+ //# sourceMappingURL=content-policy-service.test.js.map