@siduri-x/core 1.0.8 → 2.0.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.
@@ -41,16 +41,14 @@ describe('Architecture: Core & Organ Package Boundaries (Phase 2)', () => {
41
41
  const corePackageJsonPath = path.resolve(__dirname, '../package.json');
42
42
  const EXPECTED_ORGANS = [
43
43
  { dir: 'brain', name: '@siduri-x/brain', organType: 'brain', configKey: 'brain' },
44
- { dir: 'memory', name: '@siduri-x/memory', organType: 'memory', configKey: 'memory' },
45
- { dir: 'knowledge', name: '@siduri-x/knowledge', organType: 'knowledge', configKey: 'knowledge' },
46
- { dir: 'behavior', name: '@siduri-x/behavior', organType: 'behavior', configKey: 'behavior' },
47
44
  { dir: 'ear', name: '@siduri-x/ear', organType: 'ear', configKey: 'ear' },
45
+ { dir: 'voice', name: '@siduri-x/voice', organType: 'voice', configKey: 'voice' },
46
+ { dir: 'mouth', name: '@siduri-x/mouth', organType: 'mouth', configKey: 'mouth' },
48
47
  { dir: 'vision', name: '@siduri-x/vision', organType: 'vision', configKey: 'vision' },
49
48
  { dir: 'hands', name: '@siduri-x/hands', organType: 'hands', configKey: 'hands' },
50
49
  { dir: 'body', name: '@siduri-x/body', organType: 'body', configKey: 'body' },
51
- { dir: 'voice', name: '@siduri-x/voice', organType: 'voice', configKey: 'voice' },
52
50
  { dir: 'observation', name: '@siduri-x/observation', organType: 'observation', configKey: 'observation' },
53
- { dir: 'mouth', name: '@siduri-x/mouth', organType: 'mouth', configKey: 'mouth' },
51
+ { dir: 'eknowledge', name: '@siduri-x/eknowledge', organType: 'eknowledge', configKey: 'eknowledge' },
54
52
  ];
55
53
  it('package.json has zero dependencies on @siduri-x organ packages', () => {
56
54
  const pkg = JSON.parse(fs.readFileSync(corePackageJsonPath, 'utf8'));
@@ -78,7 +76,7 @@ describe('Architecture: Core & Organ Package Boundaries (Phase 2)', () => {
78
76
  }
79
77
  expect(forbiddenImports).toEqual([]);
80
78
  });
81
- it('all 10 organ packages have a valid organ-manifest.json', () => {
79
+ it('all 9 peripheral organ packages have a valid organ-manifest.json', () => {
82
80
  for (const organ of EXPECTED_ORGANS) {
83
81
  const manifestPath = path.join(rootOrgansDir, organ.dir, 'organ-manifest.json');
84
82
  expect(fs.existsSync(manifestPath)).toBe(true);
@@ -108,10 +106,4 @@ describe('Architecture: Core & Organ Package Boundaries (Phase 2)', () => {
108
106
  }
109
107
  }
110
108
  });
111
- it('memory organ packages SQL migrations', () => {
112
- const memoryMigrationsDir = path.join(rootOrgansDir, 'memory', 'migrations');
113
- expect(fs.existsSync(memoryMigrationsDir)).toBe(true);
114
- const files = fs.readdirSync(memoryMigrationsDir).filter((f) => f.endsWith('.sql'));
115
- expect(files.length).toBeGreaterThanOrEqual(1);
116
- });
117
109
  });
@@ -1,4 +1,4 @@
1
- import { KnowledgeOrgan, MemoryOrgan, KnowledgeItem, Claim, BehaviorDirective, EvidenceRecord, ResponseCitation, RequestContext } from './index';
1
+ import { KnowledgeOrgan, MemoryOrgan, KnowledgeItem, Claim, BehaviorDirective, EvidenceRecord, ResponseCitation, RequestContext, SelfRepository, LifeDatabase, EpisodicMemoryStore, EKnowledgeOrgan } from './index';
2
2
  export interface ContextRetrievalParams {
3
3
  companionId: string;
4
4
  perceivedText: string;
@@ -6,8 +6,10 @@ export interface ContextRetrievalParams {
6
6
  role: 'OWNER' | 'VIEWER' | 'OPERATOR';
7
7
  isContextObject: boolean;
8
8
  shouldQueryKnowledge: boolean;
9
- knowledge?: KnowledgeOrgan;
10
- memory?: MemoryOrgan;
9
+ knowledge?: KnowledgeOrgan | LifeDatabase;
10
+ memory?: MemoryOrgan | EpisodicMemoryStore;
11
+ self?: SelfRepository;
12
+ externalKnowledge?: EKnowledgeOrgan | KnowledgeOrgan;
11
13
  }
12
14
  export interface RetrievedContext {
13
15
  knowledgeData: KnowledgeItem[];
@@ -16,9 +18,10 @@ export interface RetrievedContext {
16
18
  subsystemDiagnostics: Record<string, string>;
17
19
  collectedEvidence: EvidenceRecord[];
18
20
  citations: ResponseCitation[];
21
+ lifeContext?: string[];
19
22
  }
20
23
  /**
21
- * Concurrently queries Knowledge and Memory organs with graceful degradation,
22
- * collecting diagnostics and synthesizing evidence records and citations.
24
+ * Concurrently queries Self, Knowledge (Life DB), External Knowledge, and Memory
25
+ * in a single parallel pass with graceful degradation.
23
26
  */
24
27
  export declare function retrieveRuntimeContext(params: ContextRetrievalParams): Promise<RetrievedContext>;
@@ -2,41 +2,69 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.retrieveRuntimeContext = retrieveRuntimeContext;
4
4
  /**
5
- * Concurrently queries Knowledge and Memory organs with graceful degradation,
6
- * collecting diagnostics and synthesizing evidence records and citations.
5
+ * Concurrently queries Self, Knowledge (Life DB), External Knowledge, and Memory
6
+ * in a single parallel pass with graceful degradation.
7
7
  */
8
8
  async function retrieveRuntimeContext(params) {
9
- const { companionId, perceivedText, requestContext, role, isContextObject, shouldQueryKnowledge, knowledge, memory, } = params;
9
+ const { companionId, perceivedText, requestContext, role, isContextObject, shouldQueryKnowledge, knowledge, memory, self, externalKnowledge, } = params;
10
10
  const queryOptions = isContextObject
11
11
  ? {
12
12
  limit: 5,
13
13
  }
14
14
  : role;
15
15
  const subsystemDiagnostics = {};
16
- const [knowledgeData, memoryData, activeDirectives] = await Promise.all([
17
- knowledge && shouldQueryKnowledge && typeof knowledge.search === 'function'
18
- ? knowledge.search(perceivedText).catch((e) => {
16
+ // 1. Resolve External Knowledge organ (either explicitly passed or from legacy knowledge with .search)
17
+ const extKnowledge = externalKnowledge || (knowledge && typeof knowledge.search === 'function' ? knowledge : undefined);
18
+ // 2. Query all 4 streams in parallel
19
+ const [knowledgeData, memoryData, selfOrMemoryDirectives, lifeContext] = await Promise.all([
20
+ // Stream A: External Cited Lore / Documentation
21
+ extKnowledge && shouldQueryKnowledge && typeof extKnowledge.search === 'function'
22
+ ? extKnowledge.search(perceivedText).catch((e) => {
19
23
  console.error('[SiduriRuntime] Knowledge search failed:', e.message);
20
24
  subsystemDiagnostics['knowledge'] = `UNAVAILABLE: ${e.message}`;
21
25
  return [];
22
26
  })
23
27
  : Promise.resolve([]),
28
+ // Stream B: Episodic Memory / Verified Claims (SQLite FTS5)
24
29
  memory && typeof memory.searchClaims === 'function'
25
- ? memory.searchClaims(perceivedText, queryOptions, 5).catch((e) => {
26
- console.error('[SiduriRuntime] Memory search failed:', e.message);
27
- subsystemDiagnostics['memory_claims'] = `UNAVAILABLE: ${e.message}`;
30
+ ? (async () => {
31
+ try {
32
+ // Support both (companionId, query, limit) and (query, options, limit)
33
+ const result = await memory.searchClaims(perceivedText, queryOptions, 5);
34
+ return result || [];
35
+ }
36
+ catch (e) {
37
+ console.error('[SiduriRuntime] Memory search failed:', e.message);
38
+ subsystemDiagnostics['memory_claims'] = `UNAVAILABLE: ${e.message}`;
39
+ return [];
40
+ }
41
+ })()
42
+ : Promise.resolve([]),
43
+ // Stream C: Active Directives from Self (or fallback Memory)
44
+ self && typeof self.getActiveDirectives === 'function'
45
+ ? self.getActiveDirectives(companionId).catch((e) => {
46
+ console.error('[SiduriRuntime] Self directives failed:', e.message);
47
+ subsystemDiagnostics['self_directives'] = `UNAVAILABLE: ${e.message}`;
28
48
  return [];
29
49
  })
30
- : Promise.resolve([]),
31
- memory && typeof memory.getDirectives === 'function'
32
- ? memory.getDirectives().catch((e) => {
33
- console.error('[SiduriRuntime] Memory directives failed:', e.message);
34
- subsystemDiagnostics['memory_directives'] = `UNAVAILABLE: ${e.message}`;
50
+ : memory && typeof memory.getDirectives === 'function'
51
+ ? memory.getDirectives().catch((e) => {
52
+ console.error('[SiduriRuntime] Memory directives failed:', e.message);
53
+ subsystemDiagnostics['memory_directives'] = `UNAVAILABLE: ${e.message}`;
54
+ return [];
55
+ })
56
+ : Promise.resolve([]),
57
+ // Stream D: Sovereign Life DB Context (Finances, Inventory, Schedule, Preferences)
58
+ knowledge && typeof knowledge.queryContext === 'function'
59
+ ? knowledge.queryContext(companionId, perceivedText).catch((e) => {
60
+ console.error('[SiduriRuntime] Life DB context failed:', e.message);
61
+ subsystemDiagnostics['life_db'] = `UNAVAILABLE: ${e.message}`;
35
62
  return [];
36
63
  })
37
64
  : Promise.resolve([]),
38
65
  ]);
39
- // Build evidence records from retrieved knowledge context
66
+ const activeDirectives = (selfOrMemoryDirectives || []);
67
+ // Build evidence records from retrieved external knowledge context
40
68
  const collectedEvidence = [];
41
69
  const citations = [];
42
70
  if (knowledgeData.length > 0) {
@@ -86,5 +114,6 @@ async function retrieveRuntimeContext(params) {
86
114
  subsystemDiagnostics,
87
115
  collectedEvidence,
88
116
  citations,
117
+ lifeContext,
89
118
  };
90
119
  }
@@ -0,0 +1 @@
1
+ export * from './siduri-db';
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./siduri-db"), exports);
package/dist/index.d.ts CHANGED
@@ -22,6 +22,9 @@ export * from './action-executor';
22
22
  export * from './experience-emitter';
23
23
  export * from './response-envelope';
24
24
  export * from './session-history';
25
+ export * from './schema-validator';
26
+ export * from './siduri-db';
27
+ export * from './database';
25
28
  import { EvidenceRecord } from './evidence';
26
29
  import { ActionIntent } from './action';
27
30
  import { RequestContext } from './context';
@@ -248,3 +251,38 @@ export interface HealthProbeResult {
248
251
  }
249
252
  export type HealthProbeFn = (context: HealthProbeContext) => Promise<HealthProbeResult> | HealthProbeResult;
250
253
  export * from './mouth-types';
254
+ import type { SelfIdentity, PersonalityTraits, SelfDirective, SelfRelationship, LifeInventoryItem, LifeScheduleItem, LifePreference, MemoryClaim, EpisodicEvent } from './siduri-db';
255
+ export interface SelfRepository {
256
+ getIdentity(companionId: string): Promise<SelfIdentity | undefined>;
257
+ getPersonality(companionId: string): Promise<PersonalityTraits>;
258
+ getActiveDirectives(companionId: string): Promise<SelfDirective[]>;
259
+ getRelationship(companionId: string, entityId: string): Promise<SelfRelationship | null>;
260
+ commitDirectives(companionId: string, directives: SelfDirective[]): Promise<void>;
261
+ updateRelationship(companionId: string, rel: SelfRelationship): Promise<void>;
262
+ disableDirective?(id: string): Promise<void>;
263
+ getActiveSelf?(companionId: string): Promise<{
264
+ identity?: SelfIdentity;
265
+ personality: PersonalityTraits;
266
+ directives: SelfDirective[];
267
+ }>;
268
+ }
269
+ export interface LifeDatabase {
270
+ getInventory(companionId: string, domain?: string): Promise<LifeInventoryItem[]>;
271
+ getFinanceSummary?(companionId: string): Promise<any>;
272
+ getSchedule?(companionId: string, windowStart?: Date, windowEnd?: Date): Promise<LifeScheduleItem[]>;
273
+ getPreferences?(companionId: string): Promise<Record<string, string> | LifePreference[]>;
274
+ queryContext(companionId: string, query: string): Promise<string[]>;
275
+ searchLifeContext?(queryText: string): Promise<string[]>;
276
+ }
277
+ export interface EpisodicMemoryStore {
278
+ recordEvent(companionId: string, event: any): Promise<void>;
279
+ searchClaims(companionId: string, query: string, limit?: number): Promise<MemoryClaim[]>;
280
+ proposeClaim(claim: any): Promise<MemoryClaim>;
281
+ approveClaim(claimId: string): Promise<void>;
282
+ rejectClaim?(claimId: string): Promise<void>;
283
+ getApprovedClaims?(companionId: string, limit?: number): Promise<MemoryClaim[]>;
284
+ getRecentEvents?(companionId: string, limit?: number): Promise<EpisodicEvent[]>;
285
+ }
286
+ export interface EKnowledgeOrgan {
287
+ search(query: string): Promise<KnowledgeItem[]>;
288
+ }
package/dist/index.js CHANGED
@@ -39,5 +39,8 @@ __exportStar(require("./action-executor"), exports);
39
39
  __exportStar(require("./experience-emitter"), exports);
40
40
  __exportStar(require("./response-envelope"), exports);
41
41
  __exportStar(require("./session-history"), exports);
42
+ __exportStar(require("./schema-validator"), exports);
43
+ __exportStar(require("./siduri-db"), exports);
44
+ __exportStar(require("./database"), exports);
42
45
  // Mouth (Communication & Output Delivery)
43
46
  __exportStar(require("./mouth-types"), exports);
@@ -9,6 +9,7 @@ export interface PromptCompilationParams {
9
9
  subsystemDiagnostics: Record<string, string>;
10
10
  knowledgeData: KnowledgeItem[];
11
11
  memoryData: Claim[];
12
+ lifeContext?: string[];
12
13
  }
13
14
  export interface CompiledPrompts {
14
15
  systemPrompt: string;
@@ -5,7 +5,7 @@ exports.compilePrompts = compilePrompts;
5
5
  * Compiles neutral system prompt and formatted context prompt from structured data.
6
6
  */
7
7
  async function compilePrompts(params) {
8
- const { companionName, companionId, role, requestContext, behavior, activeDirectives, subsystemDiagnostics, knowledgeData, memoryData, } = params;
8
+ const { companionName, companionId, role, requestContext, behavior, activeDirectives, subsystemDiagnostics, knowledgeData, memoryData, lifeContext, } = params;
9
9
  let contextPrompt = '';
10
10
  if (Object.keys(subsystemDiagnostics).length > 0) {
11
11
  contextPrompt +=
@@ -29,6 +29,12 @@ async function compilePrompts(params) {
29
29
  memoryData.map((m) => `- ${m.subject} ${m.predicate} ${m.value}`).join('\n') +
30
30
  '\n';
31
31
  }
32
+ if (lifeContext && lifeContext.length > 0) {
33
+ contextPrompt +=
34
+ 'LIFE CONTEXT:\n' +
35
+ lifeContext.map((l) => `- ${l}`).join('\n') +
36
+ '\n';
37
+ }
32
38
  // Compile Behavior with neutral context metadata
33
39
  const behaviorInjections = behavior && typeof behavior.compile === 'function'
34
40
  ? await behavior.compile({
@@ -66,4 +66,30 @@ describe('SiduriRuntime Facade Methods & Delegation', () => {
66
66
  await runtime.resetMemory();
67
67
  expect(mockMemory.resetMemory).toHaveBeenCalled();
68
68
  });
69
+ test('configures SqliteActionStore when actionStore is sqlite', () => {
70
+ const runtime = new runtime_1.SiduriRuntime('comp-sqlite', {
71
+ id: 'comp-sqlite',
72
+ name: 'Sqlite Test',
73
+ actionStore: 'sqlite',
74
+ });
75
+ expect(runtime.actionPolicy.getStore()).toBeDefined();
76
+ // Verify it is an instance of SqliteActionStore
77
+ expect(runtime.actionPolicy.getStore().constructor.name).toBe('SqliteActionStore');
78
+ });
79
+ test('accepts custom actionStore via RuntimeOrgans', () => {
80
+ const customStore = {
81
+ recordExecution: jest.fn(),
82
+ getExecution: jest.fn(),
83
+ updateExecution: jest.fn(),
84
+ recordApproval: jest.fn(),
85
+ getApproval: jest.fn(),
86
+ recordAudit: jest.fn(),
87
+ getAuditLog: jest.fn(),
88
+ verifyAuditChain: jest.fn(),
89
+ };
90
+ const runtime = new runtime_1.SiduriRuntime('comp-custom', { id: 'comp-custom', name: 'Custom' }, {
91
+ actionStore: customStore,
92
+ });
93
+ expect(runtime.actionPolicy.getStore()).toBe(customStore);
94
+ });
69
95
  });
package/dist/runtime.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { BrainOrgan, MemoryOrgan, VoiceOrgan, KnowledgeOrgan, VisionOrgan, BehaviorOrgan, BodyOrgan, HandsOrgan, EarOrgan, ObservationOrgan, ObservationResult, Observation, Message, RequestContext, ActionPolicyEngine, ResponseGatingEngine, StageResponseOptions, ApproveResponseOptions, RejectResponseOptions, ExperienceDispatcher, ExperienceAdapter, OrganConfig, Claim, BehaviorDirective, StagedResponsePlan, ResponseGateEvaluation, EvidenceRecord, MouthOrgan, MouthUtterance, MouthMedium, FormattedMouthOutput, MouthStreamChunk, MouthChannel } from './index';
1
+ import { BrainOrgan, MemoryOrgan, VoiceOrgan, KnowledgeOrgan, VisionOrgan, BehaviorOrgan, BodyOrgan, HandsOrgan, EarOrgan, ObservationOrgan, ObservationResult, Observation, Message, RequestContext, ActionPolicyEngine, ActionStore, ResponseGatingEngine, StageResponseOptions, ApproveResponseOptions, RejectResponseOptions, ExperienceDispatcher, ExperienceAdapter, OrganConfig, Claim, BehaviorDirective, StagedResponsePlan, ResponseGateEvaluation, EvidenceRecord, MouthOrgan, MouthUtterance, MouthMedium, FormattedMouthOutput, MouthStreamChunk, MouthChannel, SelfRepository, EKnowledgeOrgan } from './index';
2
2
  export interface SiduriRuntimeConfig {
3
3
  name: string;
4
4
  brain?: OrganConfig | Record<string, unknown>;
@@ -12,7 +12,14 @@ export interface SiduriRuntimeConfig {
12
12
  ear?: OrganConfig | Record<string, unknown>;
13
13
  observation?: OrganConfig | Record<string, unknown>;
14
14
  mouth?: OrganConfig | Record<string, unknown>;
15
+ self?: OrganConfig | Record<string, unknown>;
16
+ externalKnowledge?: OrganConfig | Record<string, unknown>;
15
17
  actionPolicy?: Record<string, unknown>;
18
+ actionStore?: 'in-memory' | 'sqlite' | {
19
+ type: 'sqlite' | 'in-memory';
20
+ dbPath?: string;
21
+ };
22
+ actionStorePath?: string;
16
23
  [key: string]: unknown;
17
24
  }
18
25
  export interface RuntimeOrgans {
@@ -27,6 +34,9 @@ export interface RuntimeOrgans {
27
34
  ear?: EarOrgan;
28
35
  observation?: ObservationOrgan;
29
36
  mouth?: MouthOrgan;
37
+ self?: SelfRepository;
38
+ externalKnowledge?: EKnowledgeOrgan;
39
+ actionStore?: ActionStore;
30
40
  actionPolicy?: ActionPolicyEngine;
31
41
  }
32
42
  export interface CompanionPerception {
@@ -59,6 +69,8 @@ export declare class SiduriRuntime {
59
69
  ear?: EarOrgan;
60
70
  observation?: ObservationOrgan;
61
71
  mouth?: MouthOrgan;
72
+ self?: SelfRepository;
73
+ externalKnowledge?: EKnowledgeOrgan;
62
74
  gating: ResponseGatingEngine;
63
75
  actionPolicy: ActionPolicyEngine;
64
76
  dispatcher: ExperienceDispatcher;
package/dist/runtime.js CHANGED
@@ -31,6 +31,8 @@ class SiduriRuntime {
31
31
  ear;
32
32
  observation;
33
33
  mouth;
34
+ self;
35
+ externalKnowledge;
34
36
  gating;
35
37
  actionPolicy;
36
38
  dispatcher;
@@ -56,8 +58,20 @@ class SiduriRuntime {
56
58
  this.ear = organs.ear;
57
59
  this.observation = organs.observation;
58
60
  this.mouth = organs.mouth;
61
+ this.self = organs.self;
62
+ this.externalKnowledge = organs.externalKnowledge;
59
63
  this.gating = new index_1.ResponseGatingEngine();
60
- this.actionPolicy = organs.actionPolicy || new index_1.ActionPolicyEngine();
64
+ let actionStore = organs.actionStore;
65
+ if (!actionStore) {
66
+ const storeOpt = config.actionStore;
67
+ const storePath = config.actionStorePath || (typeof storeOpt === 'object' ? storeOpt.dbPath : undefined);
68
+ if (storeOpt === 'sqlite' || (typeof storeOpt === 'object' && storeOpt.type === 'sqlite') || storePath) {
69
+ actionStore = new index_1.SqliteActionStore({ dbPath: storePath });
70
+ }
71
+ }
72
+ this.actionPolicy = organs.actionPolicy || new index_1.ActionPolicyEngine({
73
+ store: actionStore,
74
+ });
61
75
  this.dispatcher = new index_1.ExperienceDispatcher();
62
76
  if (this.voice && typeof this.voice.handleEvent === 'function') {
63
77
  this.dispatcher.registerAdapter(this.voice);
@@ -234,6 +248,8 @@ class SiduriRuntime {
234
248
  shouldQueryKnowledge: intent.shouldQueryKnowledge,
235
249
  knowledge: this.knowledge,
236
250
  memory: this.memory,
251
+ self: this.self,
252
+ externalKnowledge: this.externalKnowledge,
237
253
  });
238
254
  // 4. Neutral system prompt and contextual prompt compilation
239
255
  const prompts = await (0, prompt_compiler_1.compilePrompts)({
@@ -246,6 +262,7 @@ class SiduriRuntime {
246
262
  subsystemDiagnostics: contextRetrieval.subsystemDiagnostics,
247
263
  knowledgeData: contextRetrieval.knowledgeData,
248
264
  memoryData: contextRetrieval.memoryData,
265
+ lifeContext: contextRetrieval.lifeContext,
249
266
  });
250
267
  // 5. Cognition planning via BrainOrgan
251
268
  const plan = await (0, cognition_planner_1.generateCognitionPlan)({
@@ -0,0 +1,9 @@
1
+ export declare class ConfigValidationError extends Error {
2
+ errors: string[];
3
+ constructor(errors: string[]);
4
+ }
5
+ /**
6
+ * Validates a companion configuration object against a JSON schema (draft-07 compatible).
7
+ * Throws ConfigValidationError if validation errors are detected.
8
+ */
9
+ export declare function validateCompanionConfig(config: unknown, schema: any, path?: string): void;
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ConfigValidationError = void 0;
4
+ exports.validateCompanionConfig = validateCompanionConfig;
5
+ class ConfigValidationError extends Error {
6
+ errors;
7
+ constructor(errors) {
8
+ super(`Siduri configuration validation failed:\n${errors.map((e) => ` - ${e}`).join('\n')}`);
9
+ this.name = 'ConfigValidationError';
10
+ this.errors = errors;
11
+ }
12
+ }
13
+ exports.ConfigValidationError = ConfigValidationError;
14
+ /**
15
+ * Validates a companion configuration object against a JSON schema (draft-07 compatible).
16
+ * Throws ConfigValidationError if validation errors are detected.
17
+ */
18
+ function validateCompanionConfig(config, schema, path = '$') {
19
+ const errors = [];
20
+ function validateNode(value, nodeSchema, curPath) {
21
+ if (!nodeSchema || typeof nodeSchema !== 'object')
22
+ return;
23
+ // Type validation
24
+ if (nodeSchema.type !== undefined) {
25
+ const type = nodeSchema.type;
26
+ if (type === 'object') {
27
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
28
+ errors.push(`${curPath}: expected object, received ${value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value}`);
29
+ return;
30
+ }
31
+ }
32
+ else if (type === 'array') {
33
+ if (!Array.isArray(value)) {
34
+ errors.push(`${curPath}: expected array, received ${typeof value}`);
35
+ return;
36
+ }
37
+ }
38
+ else if (type === 'string') {
39
+ if (typeof value !== 'string') {
40
+ errors.push(`${curPath}: expected string, received ${typeof value}`);
41
+ return;
42
+ }
43
+ }
44
+ else if (type === 'number') {
45
+ if (typeof value !== 'number' || Number.isNaN(value)) {
46
+ errors.push(`${curPath}: expected number, received ${typeof value}`);
47
+ return;
48
+ }
49
+ }
50
+ else if (type === 'integer') {
51
+ if (typeof value !== 'number' || !Number.isInteger(value)) {
52
+ errors.push(`${curPath}: expected integer, received ${typeof value}`);
53
+ return;
54
+ }
55
+ }
56
+ else if (type === 'boolean') {
57
+ if (typeof value !== 'boolean') {
58
+ errors.push(`${curPath}: expected boolean, received ${typeof value}`);
59
+ return;
60
+ }
61
+ }
62
+ }
63
+ // Enum validation
64
+ if (Array.isArray(nodeSchema.enum)) {
65
+ if (!nodeSchema.enum.includes(value)) {
66
+ errors.push(`${curPath}: invalid value ${JSON.stringify(value)}, expected one of: ${nodeSchema.enum.map((v) => JSON.stringify(v)).join(', ')}`);
67
+ return;
68
+ }
69
+ }
70
+ // Object properties validation
71
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
72
+ if (Array.isArray(nodeSchema.required)) {
73
+ for (const reqKey of nodeSchema.required) {
74
+ if (value[reqKey] === undefined) {
75
+ errors.push(`${curPath}.${reqKey}: is required`);
76
+ }
77
+ }
78
+ }
79
+ const definedProps = nodeSchema.properties || {};
80
+ if (nodeSchema.additionalProperties === false) {
81
+ for (const key of Object.keys(value)) {
82
+ // Allow $schema property at root level
83
+ if (curPath === '$' && key === '$schema')
84
+ continue;
85
+ if (!(key in definedProps)) {
86
+ errors.push(`${curPath}.${key}: unexpected property is not allowed`);
87
+ }
88
+ }
89
+ }
90
+ for (const [propName, propSchema] of Object.entries(definedProps)) {
91
+ if (value[propName] !== undefined) {
92
+ validateNode(value[propName], propSchema, `${curPath}.${propName}`);
93
+ }
94
+ }
95
+ }
96
+ // Array items validation
97
+ if (Array.isArray(value) && nodeSchema.items) {
98
+ value.forEach((item, index) => {
99
+ validateNode(item, nodeSchema.items, `${curPath}[${index}]`);
100
+ });
101
+ }
102
+ }
103
+ validateNode(config, schema, path);
104
+ if (errors.length > 0) {
105
+ throw new ConfigValidationError(errors);
106
+ }
107
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const schema_validator_1 = require("./schema-validator");
4
+ describe('validateCompanionConfig', () => {
5
+ const sampleSchema = {
6
+ type: 'object',
7
+ required: ['id', 'name', 'organs'],
8
+ additionalProperties: false,
9
+ properties: {
10
+ $schema: { type: 'string' },
11
+ id: { type: 'string' },
12
+ name: { type: 'string' },
13
+ organs: {
14
+ type: 'object',
15
+ additionalProperties: false,
16
+ properties: {
17
+ brain: {
18
+ type: 'object',
19
+ required: ['provider', 'model'],
20
+ properties: {
21
+ provider: {
22
+ type: 'string',
23
+ enum: ['openrouter', 'openai-compatible'],
24
+ },
25
+ model: { type: 'string' },
26
+ },
27
+ },
28
+ memory: {
29
+ type: 'object',
30
+ required: ['provider'],
31
+ properties: {
32
+ provider: {
33
+ type: 'string',
34
+ enum: ['postgres', 'in-memory', 'none'],
35
+ },
36
+ maxConnections: { type: 'number' },
37
+ },
38
+ },
39
+ },
40
+ },
41
+ },
42
+ };
43
+ test('accepts valid configuration matching schema', () => {
44
+ const validConfig = {
45
+ $schema: './siduri.schema.json',
46
+ id: 'companion-1',
47
+ name: 'Test Companion',
48
+ organs: {
49
+ brain: {
50
+ provider: 'openrouter',
51
+ model: 'gpt-4o',
52
+ },
53
+ },
54
+ };
55
+ expect(() => (0, schema_validator_1.validateCompanionConfig)(validConfig, sampleSchema)).not.toThrow();
56
+ });
57
+ test('throws ConfigValidationError if missing required root field', () => {
58
+ const invalidConfig = {
59
+ name: 'Missing Id and Organs',
60
+ };
61
+ expect(() => (0, schema_validator_1.validateCompanionConfig)(invalidConfig, sampleSchema)).toThrow(schema_validator_1.ConfigValidationError);
62
+ try {
63
+ (0, schema_validator_1.validateCompanionConfig)(invalidConfig, sampleSchema);
64
+ }
65
+ catch (err) {
66
+ expect(err.errors).toContain('$.id: is required');
67
+ expect(err.errors).toContain('$.organs: is required');
68
+ }
69
+ });
70
+ test('throws ConfigValidationError on unexpected property when additionalProperties is false', () => {
71
+ const invalidConfig = {
72
+ id: 'c-1',
73
+ name: 'Test',
74
+ organs: {},
75
+ extraField: 'not allowed',
76
+ };
77
+ expect(() => (0, schema_validator_1.validateCompanionConfig)(invalidConfig, sampleSchema)).toThrow(schema_validator_1.ConfigValidationError);
78
+ try {
79
+ (0, schema_validator_1.validateCompanionConfig)(invalidConfig, sampleSchema);
80
+ }
81
+ catch (err) {
82
+ expect(err.errors).toContain('$.extraField: unexpected property is not allowed');
83
+ }
84
+ });
85
+ test('throws ConfigValidationError on invalid enum value', () => {
86
+ const invalidConfig = {
87
+ id: 'c-1',
88
+ name: 'Test',
89
+ organs: {
90
+ brain: {
91
+ provider: 'invalid-brain-provider',
92
+ model: 'gpt-4',
93
+ },
94
+ },
95
+ };
96
+ expect(() => (0, schema_validator_1.validateCompanionConfig)(invalidConfig, sampleSchema)).toThrow(schema_validator_1.ConfigValidationError);
97
+ try {
98
+ (0, schema_validator_1.validateCompanionConfig)(invalidConfig, sampleSchema);
99
+ }
100
+ catch (err) {
101
+ expect(err.errors.some((e) => e.includes('invalid value "invalid-brain-provider"'))).toBe(true);
102
+ }
103
+ });
104
+ test('throws ConfigValidationError on invalid type', () => {
105
+ const invalidConfig = {
106
+ id: 12345, // should be string
107
+ name: 'Test',
108
+ organs: {
109
+ memory: {
110
+ provider: 'postgres',
111
+ maxConnections: 'ten', // should be number
112
+ },
113
+ },
114
+ };
115
+ expect(() => (0, schema_validator_1.validateCompanionConfig)(invalidConfig, sampleSchema)).toThrow(schema_validator_1.ConfigValidationError);
116
+ try {
117
+ (0, schema_validator_1.validateCompanionConfig)(invalidConfig, sampleSchema);
118
+ }
119
+ catch (err) {
120
+ expect(err.errors).toContain('$.id: expected string, received number');
121
+ expect(err.errors).toContain('$.organs.memory.maxConnections: expected number, received string');
122
+ }
123
+ });
124
+ });