@siduri-x/core 1.0.4 → 1.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.
Files changed (45) hide show
  1. package/package.json +1 -1
  2. package/dist/action-policy.d.ts +0 -45
  3. package/dist/action-policy.js +0 -224
  4. package/dist/action-policy.test.d.ts +0 -1
  5. package/dist/action-policy.test.js +0 -194
  6. package/dist/action.d.ts +0 -72
  7. package/dist/action.js +0 -2
  8. package/dist/adversarial.test.d.ts +0 -1
  9. package/dist/adversarial.test.js +0 -493
  10. package/dist/architecture-boundary.test.d.ts +0 -1
  11. package/dist/architecture-boundary.test.js +0 -116
  12. package/dist/capability.d.ts +0 -57
  13. package/dist/capability.js +0 -130
  14. package/dist/capability.test.d.ts +0 -1
  15. package/dist/capability.test.js +0 -269
  16. package/dist/chat-contract.d.ts +0 -77
  17. package/dist/chat-contract.js +0 -65
  18. package/dist/context.d.ts +0 -47
  19. package/dist/context.js +0 -92
  20. package/dist/context.test.d.ts +0 -1
  21. package/dist/context.test.js +0 -109
  22. package/dist/dispatcher.d.ts +0 -14
  23. package/dist/dispatcher.js +0 -40
  24. package/dist/dispatcher.test.d.ts +0 -1
  25. package/dist/dispatcher.test.js +0 -60
  26. package/dist/ear-types.d.ts +0 -33
  27. package/dist/ear-types.js +0 -2
  28. package/dist/evidence.d.ts +0 -72
  29. package/dist/evidence.js +0 -45
  30. package/dist/evidence.test.d.ts +0 -1
  31. package/dist/evidence.test.js +0 -101
  32. package/dist/experience.d.ts +0 -56
  33. package/dist/experience.js +0 -78
  34. package/dist/experience.test.d.ts +0 -1
  35. package/dist/experience.test.js +0 -58
  36. package/dist/gating.d.ts +0 -45
  37. package/dist/gating.js +0 -189
  38. package/dist/gating.test.d.ts +0 -1
  39. package/dist/gating.test.js +0 -190
  40. package/dist/index.d.ts +0 -266
  41. package/dist/index.js +0 -29
  42. package/dist/runtime.d.ts +0 -50
  43. package/dist/runtime.js +0 -411
  44. package/dist/teaching.d.ts +0 -15
  45. package/dist/teaching.js +0 -159
@@ -1,78 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createExperienceEvents = createExperienceEvents;
4
- exports.validateExperienceEvent = validateExperienceEvent;
5
- function generateEventId(kind) {
6
- return `evt-${kind}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
7
- }
8
- function createExperienceEvents(options) {
9
- const nowStr = options.now
10
- ? new Date(options.now).toISOString()
11
- : new Date().toISOString();
12
- const events = [];
13
- // 1. Voice event
14
- events.push({
15
- eventId: generateEventId('voice'),
16
- companionId: options.companionId,
17
- responseId: options.responseId,
18
- correlationId: options.correlationId,
19
- channel: options.channel,
20
- audienceId: options.audienceId,
21
- approval: 'APPROVED',
22
- kind: 'voice',
23
- lifecycle: 'STARTED',
24
- evidenceIds: options.evidenceIds ?? [],
25
- citations: options.citations,
26
- text: options.speech,
27
- language: options.language || 'ja',
28
- createdAt: nowStr,
29
- expiresAt: options.expiresAt,
30
- });
31
- // 2. Avatar/Body event
32
- events.push({
33
- eventId: generateEventId('avatar'),
34
- companionId: options.companionId,
35
- responseId: options.responseId,
36
- correlationId: options.correlationId,
37
- channel: options.channel,
38
- audienceId: options.audienceId,
39
- approval: 'APPROVED',
40
- kind: 'avatar',
41
- lifecycle: 'STARTED',
42
- evidenceIds: options.evidenceIds ?? [],
43
- text: options.speech,
44
- language: options.language || 'ja',
45
- expression: options.expression || 'neutral',
46
- action: options.action || 'talk',
47
- createdAt: nowStr,
48
- expiresAt: options.expiresAt,
49
- });
50
- return events;
51
- }
52
- function validateExperienceEvent(event) {
53
- if (!event || typeof event !== 'object') {
54
- return { valid: false, error: 'Event must be an object' };
55
- }
56
- const e = event;
57
- if (!e.eventId || typeof e.eventId !== 'string')
58
- return { valid: false, error: 'Missing or invalid eventId' };
59
- if (!e.companionId || typeof e.companionId !== 'string')
60
- return { valid: false, error: 'Missing or invalid companionId' };
61
- if (!e.responseId || typeof e.responseId !== 'string')
62
- return { valid: false, error: 'Missing or invalid responseId' };
63
- if (!e.correlationId || typeof e.correlationId !== 'string')
64
- return { valid: false, error: 'Missing or invalid correlationId' };
65
- if (!e.audienceId || typeof e.audienceId !== 'string')
66
- return { valid: false, error: 'Missing or invalid audienceId' };
67
- if (e.approval !== 'APPROVED')
68
- return { valid: false, error: 'Event approval must be APPROVED' };
69
- if (!['voice', 'caption', 'avatar', 'platform_action'].includes(e.kind)) {
70
- return { valid: false, error: `Invalid kind: ${e.kind}` };
71
- }
72
- if (!['STARTED', 'PROGRESS', 'COMPLETED', 'FAILED'].includes(e.lifecycle)) {
73
- return { valid: false, error: `Invalid lifecycle: ${e.lifecycle}` };
74
- }
75
- if (!Array.isArray(e.evidenceIds))
76
- return { valid: false, error: 'Missing or invalid evidenceIds array' };
77
- return { valid: true };
78
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,58 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const experience_1 = require("./experience");
4
- describe('T5 Experience Event Contract Suite', () => {
5
- const baseEventOptions = {
6
- responseId: 'resp-123',
7
- companionId: 'companion-a',
8
- correlationId: 'corr-123',
9
- channel: 'public',
10
- audienceId: 'audience-public',
11
- speech: 'Hello world',
12
- language: 'en',
13
- evidenceIds: ['ev-1'],
14
- expression: 'happy',
15
- action: 'wave',
16
- };
17
- test('creates structured voice and avatar experience events from approved response options', () => {
18
- const events = (0, experience_1.createExperienceEvents)(baseEventOptions);
19
- expect(events.length).toBe(2);
20
- const voiceEvent = events.find((e) => e.kind === 'voice');
21
- expect(voiceEvent).toBeDefined();
22
- expect(voiceEvent?.approval).toBe('APPROVED');
23
- expect(voiceEvent?.companionId).toBe('companion-a');
24
- expect(voiceEvent?.correlationId).toBe('corr-123');
25
- expect(voiceEvent?.text).toBe('Hello world');
26
- expect(voiceEvent?.language).toBe('en');
27
- expect(voiceEvent?.evidenceIds).toEqual(['ev-1']);
28
- const avatarEvent = events.find((e) => e.kind === 'avatar');
29
- expect(avatarEvent).toBeDefined();
30
- expect(avatarEvent?.approval).toBe('APPROVED');
31
- expect(avatarEvent?.expression).toBe('happy');
32
- expect(avatarEvent?.action).toBe('wave');
33
- });
34
- test('validates experience event envelope correctly', () => {
35
- const events = (0, experience_1.createExperienceEvents)(baseEventOptions);
36
- for (const e of events) {
37
- const val = (0, experience_1.validateExperienceEvent)(e);
38
- expect(val.valid).toBe(true);
39
- expect(val.error).toBeUndefined();
40
- }
41
- });
42
- test('rejects unapproved experience event', () => {
43
- const events = (0, experience_1.createExperienceEvents)(baseEventOptions);
44
- const unapproved = { ...events[0], approval: 'STAGED' };
45
- const val = (0, experience_1.validateExperienceEvent)(unapproved);
46
- expect(val.valid).toBe(false);
47
- expect(val.error).toContain('Event approval must be APPROVED');
48
- });
49
- test('rejects missing metadata (companionId, responseId, etc.)', () => {
50
- const events = (0, experience_1.createExperienceEvents)(baseEventOptions);
51
- const missingCompanion = { ...events[0], companionId: '' };
52
- expect((0, experience_1.validateExperienceEvent)(missingCompanion).valid).toBe(false);
53
- const missingCorr = { ...events[0], correlationId: '' };
54
- expect((0, experience_1.validateExperienceEvent)(missingCorr).valid).toBe(false);
55
- const missingAudience = { ...events[0], audienceId: '' };
56
- expect((0, experience_1.validateExperienceEvent)(missingAudience).valid).toBe(false);
57
- });
58
- });
package/dist/gating.d.ts DELETED
@@ -1,45 +0,0 @@
1
- import { RequestContext } from './context';
2
- import { EvidenceRecord, StagedResponsePlan, ResponseGateEvaluation, ResponseCitation } from './evidence';
3
- export interface StageResponseOptions {
4
- requestContext: RequestContext;
5
- candidateSpeech: string;
6
- candidateLanguage: string;
7
- internalMonologue?: string;
8
- memoryProposals?: any[];
9
- behaviorProposals?: any[];
10
- evidenceRecords?: EvidenceRecord[];
11
- citations?: ResponseCitation[];
12
- requiresApproval?: boolean;
13
- ttlMs?: number;
14
- now?: string | Date;
15
- }
16
- export interface ApproveResponseOptions {
17
- responseId: string;
18
- companionId: string;
19
- correlationId: string;
20
- audienceId?: string;
21
- }
22
- export interface RejectResponseOptions {
23
- responseId: string;
24
- companionId: string;
25
- correlationId: string;
26
- reason?: string;
27
- }
28
- export declare class ResponseGatingEngine {
29
- private readonly stagedPlans;
30
- private readonly consumedApprovals;
31
- stageResponse(options: StageResponseOptions): StagedResponsePlan;
32
- evaluateGate(staged: StagedResponsePlan, allEvidence?: EvidenceRecord[], now?: string | Date): ResponseGateEvaluation;
33
- approveResponse(options: ApproveResponseOptions): {
34
- success: boolean;
35
- reason?: string;
36
- plan?: StagedResponsePlan;
37
- };
38
- rejectResponse(options: RejectResponseOptions): {
39
- success: boolean;
40
- reason?: string;
41
- plan?: StagedResponsePlan;
42
- };
43
- getStagedPlan(responseId: string): StagedResponsePlan | undefined;
44
- findStagedPlanByCorrelation(companionId: string, correlationId: string): StagedResponsePlan | undefined;
45
- }
package/dist/gating.js DELETED
@@ -1,189 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ResponseGatingEngine = void 0;
4
- const evidence_1 = require("./evidence");
5
- function generateId(prefix) {
6
- return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
7
- }
8
- class ResponseGatingEngine {
9
- stagedPlans = new Map();
10
- consumedApprovals = new Set();
11
- stageResponse(options) {
12
- const { requestContext } = options;
13
- const nowTime = options.now ? new Date(options.now).getTime() : Date.now();
14
- const ttlMs = options.ttlMs ?? 60_000;
15
- const expiresAt = new Date(nowTime + ttlMs).toISOString();
16
- const evidenceRecords = options.evidenceRecords ?? [];
17
- const evidenceIds = evidenceRecords.map((e) => e.evidenceId);
18
- // Calculate aggregate confidence from evidence records if present
19
- let confidenceSummary = 1.0;
20
- let uncertaintySummary;
21
- if (evidenceRecords.length > 0) {
22
- const confidences = evidenceRecords
23
- .map((e) => e.confidence)
24
- .filter((c) => typeof c === 'number' && !isNaN(c));
25
- if (confidences.length > 0) {
26
- confidenceSummary = confidences.reduce((sum, c) => sum + c, 0) / confidences.length;
27
- }
28
- const uncertainties = evidenceRecords
29
- .map((e) => e.uncertainty)
30
- .filter((u) => typeof u === 'string' && u.trim() !== '');
31
- if (uncertainties.length > 0) {
32
- uncertaintySummary = uncertainties.join('; ');
33
- }
34
- }
35
- const requiresApproval = options.requiresApproval !== undefined
36
- ? options.requiresApproval
37
- : requestContext.conversation.channel === 'operator' ||
38
- evidenceRecords.some((e) => e.origin === 'ocr' || e.trust === 'untrusted');
39
- const staged = {
40
- responseId: generateId('resp'),
41
- companionId: requestContext.companionId,
42
- correlationId: requestContext.conversation.correlationId,
43
- channel: requestContext.conversation.channel,
44
- audienceId: requestContext.conversation.audienceId,
45
- speech: options.candidateSpeech,
46
- language: options.candidateLanguage,
47
- evidenceIds,
48
- citations: options.citations ?? [],
49
- confidenceSummary,
50
- uncertaintySummary,
51
- requiresApproval,
52
- status: 'STAGED',
53
- createdAt: new Date(nowTime).toISOString(),
54
- expiresAt,
55
- memoryProposals: options.memoryProposals,
56
- behaviorProposals: options.behaviorProposals,
57
- internalMonologue: options.internalMonologue,
58
- };
59
- this.stagedPlans.set(staged.responseId, staged);
60
- return staged;
61
- }
62
- evaluateGate(staged, allEvidence = [], now = new Date()) {
63
- const nowTime = new Date(now).getTime();
64
- // 1. Check if empty speech
65
- if (!staged.speech || staged.speech.trim() === '') {
66
- return {
67
- admissible: false,
68
- disposition: 'REJECTED',
69
- reasonCode: 'EMPTY_SPEECH',
70
- stagedPlan: staged,
71
- filteredEvidenceIds: [],
72
- filteredCitations: [],
73
- diagnostics: { detail: 'Speech content is empty' },
74
- };
75
- }
76
- // 2. Check if expired
77
- if (staged.expiresAt && new Date(staged.expiresAt).getTime() <= nowTime) {
78
- staged.status = 'EXPIRED';
79
- return {
80
- admissible: false,
81
- disposition: 'EXPIRED',
82
- reasonCode: 'EVIDENCE_EXPIRED',
83
- stagedPlan: staged,
84
- filteredEvidenceIds: [],
85
- filteredCitations: [],
86
- diagnostics: { detail: 'Staged response plan expired' },
87
- };
88
- }
89
- // 3. Disclosure filter on attached evidence
90
- const attachedEvidence = allEvidence.filter((e) => staged.evidenceIds.includes(e.evidenceId));
91
- const { admitted, excluded } = (0, evidence_1.filterEvidenceRecords)(attachedEvidence, {
92
- companionId: staged.companionId,
93
- channel: staged.channel,
94
- audienceId: staged.audienceId,
95
- now,
96
- });
97
- // If any evidence attached to this plan violated disclosure in this channel, exclude it
98
- const admittedEvidenceIds = admitted.map((e) => e.evidenceId);
99
- const filteredCitations = staged.citations.filter((c) => admitted.some((e) => e.sourceId === c.sourceId || (e.documentId && e.documentId === c.documentId)));
100
- // 4. Explicitly rejected or status check
101
- if (staged.status === 'REJECTED') {
102
- return {
103
- admissible: false,
104
- disposition: 'REJECTED',
105
- reasonCode: 'EXPLICITLY_REJECTED',
106
- stagedPlan: staged,
107
- filteredEvidenceIds: admittedEvidenceIds,
108
- filteredCitations,
109
- };
110
- }
111
- // 5. Staged approval check
112
- if (staged.requiresApproval && staged.status !== 'APPROVED') {
113
- return {
114
- admissible: false,
115
- disposition: staged.status,
116
- reasonCode: 'APPROVAL_REQUIRED',
117
- stagedPlan: staged,
118
- filteredEvidenceIds: admittedEvidenceIds,
119
- filteredCitations,
120
- diagnostics: {
121
- detail: 'Response plan requires operator approval before external emission',
122
- excludedEvidenceCount: String(excluded.length),
123
- },
124
- };
125
- }
126
- // 6. Direct approval / Admissible
127
- return {
128
- admissible: true,
129
- disposition: staged.status === 'APPROVED' ? 'APPROVED' : 'APPROVED',
130
- reasonCode: 'APPROVED_DIRECT',
131
- stagedPlan: staged,
132
- filteredEvidenceIds: admittedEvidenceIds,
133
- filteredCitations,
134
- };
135
- }
136
- approveResponse(options) {
137
- const plan = this.stagedPlans.get(options.responseId);
138
- if (!plan) {
139
- return { success: false, reason: 'UNKNOWN_APPROVAL_ID' };
140
- }
141
- if (this.consumedApprovals.has(options.responseId) || plan.status === 'APPROVED') {
142
- return { success: false, reason: 'APPROVAL_ALREADY_CONSUMED' };
143
- }
144
- if (plan.companionId !== options.companionId) {
145
- return { success: false, reason: 'COMPANION_MISMATCH' };
146
- }
147
- if (plan.correlationId !== options.correlationId) {
148
- return { success: false, reason: 'APPROVAL_ID_MISMATCH' };
149
- }
150
- if (options.audienceId && plan.audienceId !== options.audienceId) {
151
- return { success: false, reason: 'AUDIENCE_MISMATCH' };
152
- }
153
- if (plan.status === 'EXPIRED') {
154
- return { success: false, reason: 'EVIDENCE_EXPIRED' };
155
- }
156
- if (plan.status === 'REJECTED') {
157
- return { success: false, reason: 'EXPLICITLY_REJECTED' };
158
- }
159
- plan.status = 'APPROVED';
160
- this.consumedApprovals.add(options.responseId);
161
- return { success: true, plan };
162
- }
163
- rejectResponse(options) {
164
- const plan = this.stagedPlans.get(options.responseId);
165
- if (!plan) {
166
- return { success: false, reason: 'UNKNOWN_APPROVAL_ID' };
167
- }
168
- if (plan.companionId !== options.companionId) {
169
- return { success: false, reason: 'COMPANION_MISMATCH' };
170
- }
171
- if (plan.correlationId !== options.correlationId) {
172
- return { success: false, reason: 'APPROVAL_ID_MISMATCH' };
173
- }
174
- plan.status = 'REJECTED';
175
- return { success: true, plan };
176
- }
177
- getStagedPlan(responseId) {
178
- return this.stagedPlans.get(responseId);
179
- }
180
- findStagedPlanByCorrelation(companionId, correlationId) {
181
- for (const plan of this.stagedPlans.values()) {
182
- if (plan.companionId === companionId && plan.correlationId === correlationId) {
183
- return plan;
184
- }
185
- }
186
- return undefined;
187
- }
188
- }
189
- exports.ResponseGatingEngine = ResponseGatingEngine;
@@ -1 +0,0 @@
1
- export {};
@@ -1,190 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const gating_1 = require("./gating");
4
- describe('T4 Response Gating & Staged Approval Suite', () => {
5
- let engine;
6
- const validPublicContext = {
7
- companionId: 'companion-a',
8
- actor: {
9
- actorId: 'actor-a',
10
- sessionId: 'session-a',
11
- authorizationRole: 'viewer',
12
- capabilities: ['chat:public'],
13
- authenticated: false,
14
- },
15
- conversation: {
16
- channel: 'public',
17
- audienceId: 'audience-public',
18
- correlationId: 'corr-gate-1',
19
- },
20
- };
21
- beforeEach(() => {
22
- engine = new gating_1.ResponseGatingEngine();
23
- });
24
- test('valid grounded response without special approval requirement is approved/admissible directly', () => {
25
- const evidence = {
26
- evidenceId: 'ev-pub-1',
27
- sourceId: 'src-wiki',
28
- origin: 'knowledge',
29
- trust: 'configured',
30
- sensitivity: 'public',
31
- allowedAudiences: ['audience-public'],
32
- companionId: 'companion-a',
33
- correlationId: 'corr-gate-1',
34
- createdAt: new Date().toISOString(),
35
- };
36
- const staged = engine.stageResponse({
37
- requestContext: validPublicContext,
38
- candidateSpeech: 'Here is grounded public knowledge.',
39
- candidateLanguage: 'en',
40
- evidenceRecords: [evidence],
41
- citations: [{ sourceId: 'src-wiki', revision: 'r1' }],
42
- });
43
- expect(staged.requiresApproval).toBe(false);
44
- expect(staged.status).toBe('STAGED');
45
- const evaluation = engine.evaluateGate(staged, [evidence]);
46
- expect(evaluation.admissible).toBe(true);
47
- expect(evaluation.disposition).toBe('APPROVED');
48
- expect(evaluation.reasonCode).toBe('APPROVED_DIRECT');
49
- expect(evaluation.filteredEvidenceIds).toEqual(['ev-pub-1']);
50
- expect(evaluation.filteredCitations.length).toBe(1);
51
- });
52
- test('response containing untrusted OCR evidence requires approval and remains staged until approved', () => {
53
- const ocrEvidence = {
54
- evidenceId: 'ev-ocr-1',
55
- sourceId: 'vision-ocr',
56
- origin: 'ocr',
57
- trust: 'untrusted',
58
- sensitivity: 'public',
59
- allowedAudiences: ['audience-public'],
60
- companionId: 'companion-a',
61
- correlationId: 'corr-gate-2',
62
- createdAt: new Date().toISOString(),
63
- uncertainty: 'OCR detected prompt injection text',
64
- };
65
- const staged = engine.stageResponse({
66
- requestContext: {
67
- ...validPublicContext,
68
- conversation: { ...validPublicContext.conversation, correlationId: 'corr-gate-2' },
69
- },
70
- candidateSpeech: 'Text extracted from image.',
71
- candidateLanguage: 'en',
72
- evidenceRecords: [ocrEvidence],
73
- });
74
- expect(staged.requiresApproval).toBe(true);
75
- expect(staged.status).toBe('STAGED');
76
- // Before approval -> gating holds it
77
- const beforeApproval = engine.evaluateGate(staged, [ocrEvidence]);
78
- expect(beforeApproval.admissible).toBe(false);
79
- expect(beforeApproval.disposition).toBe('STAGED');
80
- expect(beforeApproval.reasonCode).toBe('APPROVAL_REQUIRED');
81
- // Operator approves the response
82
- const approveResult = engine.approveResponse({
83
- responseId: staged.responseId,
84
- companionId: 'companion-a',
85
- correlationId: 'corr-gate-2',
86
- });
87
- expect(approveResult.success).toBe(true);
88
- // After approval -> gating allows it
89
- const afterApproval = engine.evaluateGate(staged, [ocrEvidence]);
90
- expect(afterApproval.admissible).toBe(true);
91
- expect(afterApproval.disposition).toBe('APPROVED');
92
- expect(afterApproval.reasonCode).toBe('APPROVED_DIRECT');
93
- });
94
- test('unknown or mismatched approval ID is rejected', () => {
95
- const staged = engine.stageResponse({
96
- requestContext: validPublicContext,
97
- candidateSpeech: 'Requires approval',
98
- candidateLanguage: 'en',
99
- requiresApproval: true,
100
- });
101
- // Unknown response ID
102
- const unknownRes = engine.approveResponse({
103
- responseId: 'non-existent-resp',
104
- companionId: 'companion-a',
105
- correlationId: 'corr-gate-1',
106
- });
107
- expect(unknownRes.success).toBe(false);
108
- expect(unknownRes.reason).toBe('UNKNOWN_APPROVAL_ID');
109
- // Companion mismatch
110
- const companionMismatch = engine.approveResponse({
111
- responseId: staged.responseId,
112
- companionId: 'companion-other',
113
- correlationId: 'corr-gate-1',
114
- });
115
- expect(companionMismatch.success).toBe(false);
116
- expect(companionMismatch.reason).toBe('COMPANION_MISMATCH');
117
- // Correlation ID mismatch
118
- const correlationMismatch = engine.approveResponse({
119
- responseId: staged.responseId,
120
- companionId: 'companion-a',
121
- correlationId: 'corr-mismatch',
122
- });
123
- expect(correlationMismatch.success).toBe(false);
124
- expect(correlationMismatch.reason).toBe('APPROVAL_ID_MISMATCH');
125
- });
126
- test('rejection prevents response from ever reaching emission', () => {
127
- const staged = engine.stageResponse({
128
- requestContext: validPublicContext,
129
- candidateSpeech: 'Potentially unsafe response',
130
- candidateLanguage: 'en',
131
- requiresApproval: true,
132
- });
133
- const rejectRes = engine.rejectResponse({
134
- responseId: staged.responseId,
135
- companionId: 'companion-a',
136
- correlationId: 'corr-gate-1',
137
- });
138
- expect(rejectRes.success).toBe(true);
139
- const evalRes = engine.evaluateGate(staged);
140
- expect(evalRes.admissible).toBe(false);
141
- expect(evalRes.disposition).toBe('REJECTED');
142
- expect(evalRes.reasonCode).toBe('EXPLICITLY_REJECTED');
143
- });
144
- test('expired staged response plan cannot be approved or emitted', () => {
145
- const staged = engine.stageResponse({
146
- requestContext: validPublicContext,
147
- candidateSpeech: 'Expired response',
148
- candidateLanguage: 'en',
149
- requiresApproval: true,
150
- ttlMs: 100,
151
- now: new Date(Date.now() - 500),
152
- });
153
- const evalRes = engine.evaluateGate(staged, [], new Date());
154
- expect(evalRes.admissible).toBe(false);
155
- expect(evalRes.disposition).toBe('EXPIRED');
156
- expect(evalRes.reasonCode).toBe('EVIDENCE_EXPIRED');
157
- const approveRes = engine.approveResponse({
158
- responseId: staged.responseId,
159
- companionId: 'companion-a',
160
- correlationId: 'corr-gate-1',
161
- });
162
- expect(approveRes.success).toBe(false);
163
- expect(approveRes.reason).toBe('EVIDENCE_EXPIRED');
164
- });
165
- test('private evidence is filtered out and absent from public citations/evidence list', () => {
166
- const privateEvidence = {
167
- evidenceId: 'ev-priv-1',
168
- sourceId: 'src-secret',
169
- origin: 'knowledge',
170
- trust: 'configured',
171
- sensitivity: 'private',
172
- allowedAudiences: ['audience-direct-a'],
173
- companionId: 'companion-a',
174
- correlationId: 'corr-gate-1',
175
- createdAt: new Date().toISOString(),
176
- };
177
- const staged = engine.stageResponse({
178
- requestContext: validPublicContext, // public channel
179
- candidateSpeech: 'Public response text',
180
- candidateLanguage: 'en',
181
- evidenceRecords: [privateEvidence],
182
- citations: [{ sourceId: 'src-secret' }],
183
- });
184
- const evalRes = engine.evaluateGate(staged, [privateEvidence]);
185
- expect(evalRes.admissible).toBe(true);
186
- // Private evidence and its citation must be stripped from public output metadata
187
- expect(evalRes.filteredEvidenceIds).toEqual([]);
188
- expect(evalRes.filteredCitations).toEqual([]);
189
- });
190
- });