@siduri-x/core 2.0.3 → 2.0.5

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.
@@ -370,7 +370,7 @@ describe('SiduriDatabase', () => {
370
370
  evidence: ['lore book chapter 3'],
371
371
  assertedAt: new Date().toISOString(),
372
372
  });
373
- expect(claim.status).toBe('PENDING');
373
+ expect(claim.status).toBe('pending');
374
374
  expect(claim.subject).toBe('Kur');
375
375
  expect(claim.predicate).toBe('is');
376
376
  expect(claim.value).toBe('a dark entity from the underworld');
@@ -401,7 +401,7 @@ describe('SiduriDatabase', () => {
401
401
  const approved = db.getApprovedClaims('siduri-test');
402
402
  expect(approved).toHaveLength(1);
403
403
  expect(approved[0].id).toBe(claim1.id);
404
- expect(approved[0].status).toBe('APPROVED');
404
+ expect(approved[0].status).toBe('approved');
405
405
  });
406
406
  it('searches claims using FTS5 full-text search', () => {
407
407
  db = new siduri_db_1.SiduriDatabase({ dbPath });
@@ -617,7 +617,7 @@ describe('SiduriDatabase', () => {
617
617
  expect(db2.getInventory(cId)).toHaveLength(1);
618
618
  expect(db2.getInventory(cId)[0].entityName).toBe('Aged Wine');
619
619
  const claims = db2.searchClaims(cId, 'wine');
620
- expect(claims.some((c) => c.id === claim.id && c.status === 'APPROVED')).toBe(true);
620
+ expect(claims.some((c) => c.id === claim.id && c.status === 'approved')).toBe(true);
621
621
  db2.close();
622
622
  // Prevent afterEach from double-closing
623
623
  db = null;
@@ -691,7 +691,7 @@ describe('SiduriDatabase', () => {
691
691
  const active = db.getActiveDirectives(cId);
692
692
  expect(active).toHaveLength(1);
693
693
  expect(active[0].id).toBe(dId);
694
- expect(active[0].status).toBe('ACTIVE');
694
+ expect(active[0].status).toBe('active');
695
695
  // 3. Revoke directive
696
696
  db.revokeDirective(dId);
697
697
  expect(db.getActiveDirectives(cId)).toHaveLength(0);
@@ -702,7 +702,7 @@ describe('SiduriDatabase', () => {
702
702
  companionId: cId,
703
703
  priority: 50,
704
704
  directive: 'Unsafe rule',
705
- status: 'PENDING',
705
+ status: 'pending',
706
706
  category: 'behavioral',
707
707
  });
708
708
  db.rejectDirective(d2Id);
@@ -717,22 +717,23 @@ describe('SiduriDatabase', () => {
717
717
  companionId: cId,
718
718
  priority: 50,
719
719
  directive: 'Rejected directive',
720
- status: 'REJECTED',
720
+ status: 'rejected',
721
721
  category: 'behavioral',
722
722
  });
723
- expect(() => db.approveDirective('dir-rejected-1')).toThrow(/invalid transition from status 'REJECTED' to 'ACTIVE'/);
723
+ expect(() => db.approveDirective('dir-rejected-1')).toThrow(/invalid transition from status 'rejected' to 'active'/i);
724
724
  // 2. Commit directive in ACTIVE state
725
725
  db.commitDirective({
726
726
  id: 'dir-active-1',
727
727
  companionId: cId,
728
728
  priority: 50,
729
729
  directive: 'Already active directive',
730
- status: 'ACTIVE',
730
+ status: 'active',
731
731
  category: 'behavioral',
732
732
  });
733
- expect(() => db.approveDirective('dir-active-1')).toThrow(/invalid transition from status 'ACTIVE' to 'ACTIVE'/);
733
+ // Approving an already-active directive is now idempotent (no-op, does not throw)
734
+ expect(() => db.approveDirective('dir-active-1')).not.toThrow();
734
735
  // 3. Rejecting an already ACTIVE directive throws
735
- expect(() => db.rejectDirective('dir-active-1')).toThrow(/invalid transition from status 'ACTIVE' to 'REJECTED'/);
736
+ expect(() => db.rejectDirective('dir-active-1')).toThrow(/invalid transition from status 'active' to 'rejected'/i);
736
737
  });
737
738
  it('automatically marks prior directive as SUPERSEDED when approving superseding directive', () => {
738
739
  db = new siduri_db_1.SiduriDatabase({ dbPath });
@@ -766,7 +767,7 @@ describe('SiduriDatabase', () => {
766
767
  expect(active).toHaveLength(1);
767
768
  expect(active[0].id).toBe('dir-replacement-1');
768
769
  const original = db.getDirective('dir-original-1', cId);
769
- expect(original?.status).toBe('SUPERSEDED');
770
+ expect(original?.status).toBe('superseded');
770
771
  });
771
772
  it('enforces companion isolation on directive approval', () => {
772
773
  db = new siduri_db_1.SiduriDatabase({ dbPath });
@@ -777,14 +778,14 @@ describe('SiduriDatabase', () => {
777
778
  companionId: cIdB,
778
779
  priority: 50,
779
780
  directive: 'Beta private rule',
780
- status: 'PENDING',
781
+ status: 'pending',
781
782
  category: 'behavioral',
782
783
  });
783
784
  // Alpha attempts to approve Beta's directive scoped to Alpha
784
785
  db.approveDirective('dir-beta-1', cIdA);
785
- // Beta's directive must remain PENDING and unapproved
786
+ // Beta's directive must remain pending and unapproved
786
787
  const betaDirective = db.getDirective('dir-beta-1', cIdB);
787
- expect(betaDirective?.status).toBe('PENDING');
788
+ expect(betaDirective?.status).toBe('pending');
788
789
  expect(db.getActiveDirectives(cIdB)).toHaveLength(0);
789
790
  // Beta approves its own directive successfully
790
791
  db.approveDirective('dir-beta-1', cIdB);
@@ -800,7 +801,7 @@ describe('SiduriDatabase', () => {
800
801
  predicate: 'likes',
801
802
  value: 'matcha',
802
803
  });
803
- expect(claim.status).toBe('PENDING');
804
+ expect(claim.status).toBe('pending');
804
805
  expect(db.getApprovedClaims(cId)).toHaveLength(0);
805
806
  // Approve
806
807
  db.approveClaim('claim-1');
@@ -835,7 +836,7 @@ describe('SiduriDatabase', () => {
835
836
  });
836
837
  db.rejectClaim(rejectedClaim.id);
837
838
  // Attempting to approve a REJECTED claim must throw
838
- expect(() => db.approveClaim(rejectedClaim.id)).toThrow(/invalid transition from status 'REJECTED' to 'APPROVED'/);
839
+ expect(() => db.approveClaim(rejectedClaim.id)).toThrow(/invalid transition from status 'rejected' to 'approved'/i);
839
840
  // 2. Propose and approve claim, then revoke
840
841
  const revokedClaim = db.proposeClaim({
841
842
  id: 'claim-revoked',
@@ -847,7 +848,7 @@ describe('SiduriDatabase', () => {
847
848
  db.approveClaim(revokedClaim.id);
848
849
  db.revokeClaim(revokedClaim.id);
849
850
  // Attempting to approve a REVOKED claim must throw
850
- expect(() => db.approveClaim(revokedClaim.id)).toThrow(/invalid transition from status 'REVOKED' to 'APPROVED'/);
851
+ expect(() => db.approveClaim(revokedClaim.id)).toThrow(/invalid transition from status 'revoked' to 'approved'/i);
851
852
  // 3. Propose and expire claim
852
853
  const expiredClaim = db.proposeClaim({
853
854
  id: 'claim-expired',
@@ -858,7 +859,7 @@ describe('SiduriDatabase', () => {
858
859
  });
859
860
  db.expireClaim(expiredClaim.id);
860
861
  // Attempting to approve an EXPIRED claim must throw
861
- expect(() => db.approveClaim(expiredClaim.id)).toThrow(/invalid transition from status 'EXPIRED' to 'APPROVED'/);
862
+ expect(() => db.approveClaim(expiredClaim.id)).toThrow(/invalid transition from status 'expired' to 'approved'/i);
862
863
  });
863
864
  });
864
865
  });
package/dist/teaching.js CHANGED
@@ -20,12 +20,20 @@ function extractDeterministicTeaching(message, context, sourceEventId) {
20
20
  if (!text) {
21
21
  return { claims, behaviorProposals };
22
22
  }
23
- const actorId = context?.actor?.actorId;
24
- const actorSubject = actorId ? `actor:${actorId}` : 'actor:anonymous';
23
+ const rawActorId = context?.actor?.actorId;
24
+ const cleanActorId = rawActorId ? rawActorId.replace(/^actor:/, '') : undefined;
25
+ const actorSubject = cleanActorId ? `actor:${cleanActorId}` : 'actor:anonymous';
25
26
  const companionId = context?.companionId || 'default';
26
27
  const sensitivity = context?.conversation?.channel === 'public' ? 'public' : 'private';
28
+ // Strip leading companion addressing: "Siduri, you are...", "Hey Siduri: ..."
29
+ const cleanText = text.replace(/^(?:(?:hey|hi|hello)\s+)?(?:siduri|companion|assistant)\s*[,:]\s*/i, '');
30
+ const isTeachMode = context?.mode === 'teach' || /^(?:!teach|\/teach|\bteach mode\b|\blearn this rule\b)/i.test(text);
31
+ const hasExplicitTeachingCue = isTeachMode ||
32
+ /\b(?:remember that|remember this rule|remember this|from now on|learn this|rule:)\b/i.test(text);
33
+ // Casual banter / hedged opinion filter to avoid mutating Self from generic conversation
34
+ const isCasualBanter = /\b(?:probably|maybe|such a|so |very |really |just |not |the funniest|the best|the worst|the coolest|great|awesome|funny|nice|kind|crazy|wrong|right|silly)\b/i;
27
35
  // 1. Companion's Name: "your name is X" / "you are called X"
28
- const companionNameMatch = text.match(/\b(?:your name is|you are called)\s+(.+?)(?=\s+and\s+(?:i\b|my\b|you\b)|[.;,]|$)/i);
36
+ const companionNameMatch = cleanText.match(/\b(?:your name is|you are called)\s+(.+?)(?=\s+and\s+(?:i\b|my\b|you\b)|[.;,]|$)/i);
29
37
  if (companionNameMatch) {
30
38
  const name = cleanValue(companionNameMatch[1], 80);
31
39
  claims.push({
@@ -45,12 +53,124 @@ function extractDeterministicTeaching(message, context, sourceEventId) {
45
53
  predicate: 'name',
46
54
  value: name,
47
55
  memoryClass: 'identity',
56
+ category: 'relational',
48
57
  sourceEventId,
49
58
  });
50
59
  }
51
- // 2. Actor's Name: "my name is X"
52
- const myNameMatch = text.match(/\bmy name is\s+(.+?)(?=\s+and\s+(?:i\b|my\b|you\b)|[.;,]|$)/i);
53
- if (myNameMatch && !/\b(?:private|public|everywhere)\b/i.test(text)) {
60
+ // 2. Identity / Role Teaching:
61
+ // "your role is X", "you work at X", "you were created by X", "you are from X",
62
+ // or explicit "you are [an/a] X [at Y]" when in teach mode or with explicit teaching cues
63
+ const roleMatch = cleanText.match(/\byour role is\s+(.+?)(?=\s+and\s+(?:i\b|my\b|you\b)|[.;,]|$)/i);
64
+ const workMatch = cleanText.match(/\byou work (?:at|for)\s+(.+?)(?=\s+and\s+(?:i\b|my\b|you\b)|[.;,]|$)/i);
65
+ const createdByMatch = cleanText.match(/\byou (?:were|are) created by\s+(.+?)(?=\s+and\s+(?:i\b|my\b|you\b)|[.;,]|$)/i);
66
+ const fromMatch = cleanText.match(/\byou are from\s+(.+?)(?=\s+and\s+(?:i\b|my\b|you\b)|[.;,]|$)/i);
67
+ // Match "you are [an/a/the] X [at Y]" only when in Teach mode or explicit teaching cues, and not casual banter
68
+ const youAreMatch = (isTeachMode || hasExplicitTeachingCue)
69
+ ? cleanText.match(/\b(?:remember that\s+)?you are\s+(?:(?:an?|the)\s+)?(.+?)(?=\s+and\s+(?:i\b|my\b|you\b)|[.;,]|$)/i)
70
+ : null;
71
+ const validYouAreRole = youAreMatch &&
72
+ !companionNameMatch &&
73
+ !fromMatch &&
74
+ !isCasualBanter.test(youAreMatch[1]) &&
75
+ !/^(?:called|named)\b/i.test(youAreMatch[1])
76
+ ? cleanValue(youAreMatch[1].replace(/^(?:an?|the)\s+/i, ''), 100)
77
+ : undefined;
78
+ const rawEffectiveRole = roleMatch ? cleanValue(roleMatch[1], 100) : validYouAreRole;
79
+ const effectiveRole = rawEffectiveRole ? rawEffectiveRole.replace(/^(?:an?|the)\s+/i, '') : undefined;
80
+ if (effectiveRole) {
81
+ claims.push({
82
+ subject: `companion:${companionId}`,
83
+ predicate: 'role',
84
+ value: effectiveRole,
85
+ content: `The companion's role is ${effectiveRole}.`,
86
+ claimType: 'semantic',
87
+ provenance: 'deterministic_teaching',
88
+ sensitivity: 'public',
89
+ sourceEventId,
90
+ });
91
+ behaviorProposals.push({
92
+ directive: `Acknowledge role as ${effectiveRole}`,
93
+ priority: 70,
94
+ subject: `companion:${companionId}`,
95
+ predicate: 'role',
96
+ value: effectiveRole,
97
+ memoryClass: 'identity',
98
+ category: 'relational',
99
+ sourceEventId,
100
+ });
101
+ }
102
+ if (workMatch) {
103
+ const workplace = cleanValue(workMatch[1], 80);
104
+ claims.push({
105
+ subject: `companion:${companionId}`,
106
+ predicate: 'origin',
107
+ value: workplace,
108
+ content: `The companion works at ${workplace}.`,
109
+ claimType: 'semantic',
110
+ provenance: 'deterministic_teaching',
111
+ sensitivity: 'public',
112
+ sourceEventId,
113
+ });
114
+ behaviorProposals.push({
115
+ directive: `Acknowledge workplace as ${workplace}`,
116
+ priority: 70,
117
+ subject: `companion:${companionId}`,
118
+ predicate: 'origin',
119
+ value: workplace,
120
+ memoryClass: 'identity',
121
+ category: 'relational',
122
+ sourceEventId,
123
+ });
124
+ }
125
+ if (createdByMatch) {
126
+ const creator = cleanValue(createdByMatch[1], 80);
127
+ claims.push({
128
+ subject: `companion:${companionId}`,
129
+ predicate: 'created_by',
130
+ value: creator,
131
+ content: `The companion was created by ${creator}.`,
132
+ claimType: 'semantic',
133
+ provenance: 'deterministic_teaching',
134
+ sensitivity: 'public',
135
+ sourceEventId,
136
+ });
137
+ behaviorProposals.push({
138
+ directive: `Acknowledge creator as ${creator}`,
139
+ priority: 75,
140
+ subject: `companion:${companionId}`,
141
+ predicate: 'created_by',
142
+ value: creator,
143
+ memoryClass: 'identity',
144
+ category: 'relational',
145
+ sourceEventId,
146
+ });
147
+ }
148
+ if (fromMatch) {
149
+ const origin = cleanValue(fromMatch[1], 80);
150
+ claims.push({
151
+ subject: `companion:${companionId}`,
152
+ predicate: 'origin',
153
+ value: origin,
154
+ content: `The companion is from ${origin}.`,
155
+ claimType: 'semantic',
156
+ provenance: 'deterministic_teaching',
157
+ sensitivity: 'public',
158
+ sourceEventId,
159
+ });
160
+ behaviorProposals.push({
161
+ directive: `Acknowledge origin as ${origin}`,
162
+ priority: 70,
163
+ subject: `companion:${companionId}`,
164
+ predicate: 'origin',
165
+ value: origin,
166
+ memoryClass: 'identity',
167
+ category: 'relational',
168
+ sourceEventId,
169
+ });
170
+ }
171
+ // 3. Actor's Name: "my name is X"
172
+ const myNameMatch = cleanText.match(/\bmy name is\s+(.+?)(?=\s+and\s+(?:i\b|my\b|you\b)|[.;,]|$)/i);
173
+ if (myNameMatch && !/\b(?:private|public|everywhere)\b/i.test(cleanText)) {
54
174
  const name = cleanValue(myNameMatch[1], 80);
55
175
  claims.push({
56
176
  subject: actorSubject,
@@ -63,8 +183,8 @@ function extractDeterministicTeaching(message, context, sourceEventId) {
63
183
  sourceEventId,
64
184
  });
65
185
  }
66
- // 3. Preferred Address / Call me X: "call me X"
67
- const callMeMatch = text.match(/\b(?:(?:from now on|only),?\s*)?call me\s+(.+?)(?:\s+(?:in private|privately|in public|publicly|everywhere|in direct conversations))?(?=\s+and\s+(?:i\b|my\b|you\b)|[.;,]|$)/i);
186
+ // 4. Preferred Address / Call me X: "call me X"
187
+ const callMeMatch = cleanText.match(/\b(?:(?:from now on|only),?\s*)?call me\s+(.+?)(?:\s+(?:in private|privately|in public|publicly|everywhere|in direct conversations))?(?=\s+and\s+(?:i\b|my\b|you\b)|[.;,]|$)/i);
68
188
  if (callMeMatch) {
69
189
  const address = cleanValue(callMeMatch[1], 80);
70
190
  const directiveInstruction = `Address ${actorSubject} as ${address}`;
@@ -85,13 +205,15 @@ function extractDeterministicTeaching(message, context, sourceEventId) {
85
205
  predicate: 'preferred_address',
86
206
  value: address,
87
207
  memoryClass: 'behavioral',
208
+ category: 'behavioral',
88
209
  sourceEventId,
89
210
  });
90
211
  }
91
- // 4. Stated relationship: "I am your X" / "I'm your creator"
92
- const relMatch = text.match(/\b(?:i am|i'm)\s+your\s+([A-Za-z0-9_\s-]+?)(?=\s+and\s+(?:i\b|my\b|you\b)|[.;,]|$)/i);
212
+ // 5. Stated relationship: "I am your creator, Kur Zagin" / "I am your X" / "I'm your creator"
213
+ const relMatch = cleanText.match(/\b(?:i am|i'm)\s+your\s+([A-Za-z0-9_-]+)(?:,\s*([A-Za-z0-9_\s-]+?))?(?=\s+and\s+(?:i\b|my\b|you\b)|[.;]|$)/i);
93
214
  if (relMatch) {
94
215
  const relationship = cleanValue(relMatch[1], 60);
216
+ const actorName = relMatch[2] ? cleanValue(relMatch[2], 80) : undefined;
95
217
  claims.push({
96
218
  subject: actorSubject,
97
219
  predicate: 'stated_relationship',
@@ -109,11 +231,78 @@ function extractDeterministicTeaching(message, context, sourceEventId) {
109
231
  predicate: 'stated_relationship',
110
232
  value: relationship,
111
233
  memoryClass: 'relationship',
234
+ category: 'relational',
235
+ sourceEventId,
236
+ });
237
+ if (actorName) {
238
+ claims.push({
239
+ subject: actorSubject,
240
+ predicate: 'name',
241
+ value: actorName,
242
+ content: `The actor's name is ${actorName}.`,
243
+ claimType: 'preference',
244
+ provenance: 'deterministic_teaching',
245
+ sensitivity,
246
+ sourceEventId,
247
+ });
248
+ }
249
+ }
250
+ // 6. Behavioral rule / directive: "Remember this rule: X" / "Remember that rule: X" / "Rule: X" / "From now on, always X"
251
+ const ruleMatch = cleanText.match(/\b(?:remember\s+(?:this|that)\s+rule:\s*|rule:\s*|(?:from now on,?\s+)?always\s+)(.+?)(?=\s+and\s+(?:i\b|my\b|you\b)|[.;]|$)/i);
252
+ if (ruleMatch) {
253
+ let instruction = cleanValue(ruleMatch[1], 160);
254
+ if (instruction) {
255
+ instruction = instruction.charAt(0).toUpperCase() + instruction.slice(1);
256
+ behaviorProposals.push({
257
+ directive: instruction,
258
+ priority: 60,
259
+ subject: actorSubject,
260
+ predicate: 'rule',
261
+ value: instruction,
262
+ category: 'behavioral',
263
+ memoryClass: 'behavioral',
264
+ sourceEventId,
265
+ });
266
+ claims.push({
267
+ subject: actorSubject,
268
+ predicate: 'behavioral_rule',
269
+ value: instruction,
270
+ content: `Behavioral rule: ${instruction}`,
271
+ claimType: 'preference',
272
+ provenance: 'deterministic_teaching',
273
+ sensitivity,
274
+ sourceEventId,
275
+ });
276
+ }
277
+ }
278
+ // Preference: "I prefer concise answers"
279
+ const prefAnswersMatch = cleanText.match(/\b(?:i prefer|my preference is)\s+(.+?)(?=\s+and\s+(?:i\b|my\b|you\b)|[.;,]|$)/i);
280
+ if (prefAnswersMatch && !cleanText.match(/\bmy\s+preferred\s+([A-Za-z0-9_]+)\s+is\b/i)) {
281
+ const prefVal = cleanValue(prefAnswersMatch[1], 100);
282
+ const directive = `Prefer ${prefVal}`;
283
+ behaviorProposals.push({
284
+ directive,
285
+ priority: 60,
286
+ subject: actorSubject,
287
+ predicate: 'preference',
288
+ value: prefVal,
289
+ category: 'behavioral',
290
+ memoryClass: 'behavioral',
291
+ sourceEventId,
292
+ });
293
+ claims.push({
294
+ subject: actorSubject,
295
+ predicate: 'preference',
296
+ value: prefVal,
297
+ content: `The actor prefers ${prefVal}.`,
298
+ claimType: 'preference',
299
+ provenance: 'deterministic_teaching',
300
+ sensitivity,
112
301
  sourceEventId,
113
302
  });
114
303
  }
115
- // 5. Explicit Domain / Preference fact: "my preferred X is Y" / "my X is Y"
116
- const prefMatch = text.match(/\bmy\s+preferred\s+([A-Za-z0-9_]+)\s+is\s+(.+?)(?=\s+and\s+(?:i\b|my\b|you\b)|[.;,]|$)/i);
304
+ // 7. Explicit Domain / Preference fact: "my preferred X is Y" / "my X is Y"
305
+ const prefMatch = cleanText.match(/\bmy\s+preferred\s+([A-Za-z0-9_]+)\s+is\s+(.+?)(?=\s+and\s+(?:i\b|my\b|you\b)|[.;,]|$)/i);
117
306
  if (prefMatch) {
118
307
  const predicate = cleanValue(prefMatch[1], 40);
119
308
  const val = cleanValue(prefMatch[2], 100);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siduri-x/core",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
4
4
  "description": "Core runtime types, evidence protocol, action dispatcher, capability validation, and SiduriRuntime protocol",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {