principles-disciple 1.52.0 → 1.53.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.
@@ -0,0 +1,223 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ selectPrinciplesForInjection,
4
+ formatPrinciple,
5
+ DEFAULT_PRINCIPLE_BUDGET,
6
+ type InjectablePrinciple,
7
+ type PrincipleSelectionResult,
8
+ } from '../../src/core/principle-injection.js';
9
+ import type { PrinciplePriority } from '../../src/types/principle-tree-schema.js';
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Test Fixtures
13
+ // ---------------------------------------------------------------------------
14
+
15
+ function makePrinciple(overrides: Partial<{
16
+ id: string;
17
+ text: string;
18
+ priority: PrinciplePriority;
19
+ createdAt: string;
20
+ }> = {}): InjectablePrinciple {
21
+ return {
22
+ id: overrides.id ?? 'P_001',
23
+ text: overrides.text ?? 'Always verify file content before editing',
24
+ priority: overrides.priority ?? 'P1',
25
+ createdAt: overrides.createdAt ?? '2026-04-01T00:00:00.000Z',
26
+ };
27
+ }
28
+
29
+ function makePrinciples(configs: Array<{ id: string; priority: PrinciplePriority; text?: string; createdAt?: string }>): InjectablePrinciple[] {
30
+ return configs.map(c => makePrinciple({
31
+ id: c.id,
32
+ priority: c.priority,
33
+ text: c.text ?? `Principle ${c.id}`,
34
+ createdAt: c.createdAt ?? '2026-04-01T00:00:00.000Z',
35
+ }));
36
+ }
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Tests: formatPrinciple
40
+ // ---------------------------------------------------------------------------
41
+
42
+ describe('formatPrinciple', () => {
43
+ it('formats a principle as "- [ID] text"', () => {
44
+ const p = makePrinciple({ id: 'P_001', text: 'Always verify before editing' });
45
+ expect(formatPrinciple(p)).toBe('- [P_001] Always verify before editing');
46
+ });
47
+
48
+ it('includes the full text even when long', () => {
49
+ const longText = 'A'.repeat(200);
50
+ const p = makePrinciple({ id: 'P_100', text: longText });
51
+ expect(formatPrinciple(p)).toBe(`- [P_100] ${longText}`);
52
+ });
53
+ });
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Tests: selectPrinciplesForInjection — priority ordering
57
+ // ---------------------------------------------------------------------------
58
+
59
+ describe('selectPrinciplesForInjection — priority ordering', () => {
60
+ it('selects P0 principles before P1 and P2', () => {
61
+ const principles = makePrinciples([
62
+ { id: 'P1', priority: 'P2' },
63
+ { id: 'P2', priority: 'P0' },
64
+ { id: 'P3', priority: 'P1' },
65
+ ]);
66
+
67
+ const result = selectPrinciplesForInjection(principles, 10000);
68
+
69
+ expect(result.selected[0].id).toBe('P2'); // P0 first
70
+ expect(result.selected[1].id).toBe('P3'); // P1 second
71
+ expect(result.selected[2].id).toBe('P1'); // P2 third
72
+ });
73
+
74
+ it('selects newer principles first within same priority', () => {
75
+ const principles = makePrinciples([
76
+ { id: 'OLD', priority: 'P1', createdAt: '2026-03-01T00:00:00.000Z' },
77
+ { id: 'NEW', priority: 'P1', createdAt: '2026-04-15T00:00:00.000Z' },
78
+ ]);
79
+
80
+ const result = selectPrinciplesForInjection(principles, 10000);
81
+
82
+ expect(result.selected[0].id).toBe('NEW'); // Newer first
83
+ expect(result.selected[1].id).toBe('OLD');
84
+ });
85
+ });
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // Tests: selectPrinciplesForInjection — budget enforcement
89
+ // ---------------------------------------------------------------------------
90
+
91
+ describe('selectPrinciplesForInjection — budget enforcement', () => {
92
+ it('respects the character budget', () => {
93
+ const principles = makePrinciples(
94
+ Array.from({ length: 20 }, (_, i) => ({
95
+ id: `P_${String(i).padStart(3, '0')}`,
96
+ priority: 'P1' as PrinciplePriority,
97
+ text: `Principle with a reasonably long text description number ${i}`,
98
+ }))
99
+ );
100
+
101
+ const budget = 500;
102
+ const result = selectPrinciplesForInjection(principles, budget);
103
+
104
+ expect(result.totalChars).toBeLessThanOrEqual(budget + 200); // Allow some slack for P0 force-include
105
+ expect(result.selected.length).toBeLessThan(20);
106
+ expect(result.wasTruncated).toBe(true);
107
+ });
108
+
109
+ it('includes all principles when budget is large enough', () => {
110
+ const principles = makePrinciples([
111
+ { id: 'P_001', priority: 'P0' },
112
+ { id: 'P_002', priority: 'P1' },
113
+ { id: 'P_003', priority: 'P2' },
114
+ ]);
115
+
116
+ const result = selectPrinciplesForInjection(principles, 10000);
117
+
118
+ expect(result.selected).toHaveLength(3);
119
+ expect(result.wasTruncated).toBe(false);
120
+ });
121
+
122
+ it('returns empty selection for empty principles array', () => {
123
+ const result = selectPrinciplesForInjection([], 10000);
124
+
125
+ expect(result.selected).toHaveLength(0);
126
+ expect(result.totalChars).toBe(0);
127
+ expect(result.hasP0).toBe(false);
128
+ expect(result.wasTruncated).toBe(false);
129
+ });
130
+ });
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // Tests: selectPrinciplesForInjection — P0 guarantee
134
+ // ---------------------------------------------------------------------------
135
+
136
+ describe('selectPrinciplesForInjection — P0 guarantee', () => {
137
+ it('ensures at least one P0 principle is included even when over budget', () => {
138
+ // Fill budget with P1 principles, then have a P0 that would exceed budget
139
+ const principles: InjectablePrinciple[] = [];
140
+
141
+ // Add many P1 principles that fill the budget
142
+ for (let i = 0; i < 10; i++) {
143
+ principles.push(makePrinciple({
144
+ id: `P1_${i}`,
145
+ priority: 'P1',
146
+ text: `P1 principle with enough text to consume budget space ${i}`,
147
+ createdAt: '2026-04-01T00:00:00.000Z',
148
+ }));
149
+ }
150
+
151
+ // Add a P0 principle
152
+ principles.push(makePrinciple({
153
+ id: 'P0_CRITICAL',
154
+ priority: 'P0',
155
+ text: 'Critical P0 principle that must always be included',
156
+ createdAt: '2026-04-01T00:00:00.000Z',
157
+ }));
158
+
159
+ const budget = 200; // Very small budget
160
+ const result = selectPrinciplesForInjection(principles, budget);
161
+
162
+ expect(result.hasP0).toBe(true);
163
+ expect(result.selected.some(p => p.priority === 'P0')).toBe(true);
164
+ });
165
+
166
+ it('sets hasP0=true when P0 principles are naturally selected', () => {
167
+ const principles = makePrinciples([
168
+ { id: 'P0_1', priority: 'P0' },
169
+ { id: 'P1_1', priority: 'P1' },
170
+ ]);
171
+
172
+ const result = selectPrinciplesForInjection(principles, 10000);
173
+
174
+ expect(result.hasP0).toBe(true);
175
+ expect(result.breakdown.p0).toBe(1);
176
+ });
177
+
178
+ it('sets hasP0=false when no P0 principles exist', () => {
179
+ const principles = makePrinciples([
180
+ { id: 'P1_1', priority: 'P1' },
181
+ { id: 'P2_1', priority: 'P2' },
182
+ ]);
183
+
184
+ const result = selectPrinciplesForInjection(principles, 10000);
185
+
186
+ expect(result.hasP0).toBe(false);
187
+ expect(result.breakdown.p0).toBe(0);
188
+ });
189
+ });
190
+
191
+ // ---------------------------------------------------------------------------
192
+ // Tests: selectPrinciplesForInjection — breakdown
193
+ // ---------------------------------------------------------------------------
194
+
195
+ describe('selectPrinciplesForInjection — breakdown', () => {
196
+ it('counts principles by priority tier correctly', () => {
197
+ const principles = makePrinciples([
198
+ { id: 'P0_1', priority: 'P0' },
199
+ { id: 'P0_2', priority: 'P0' },
200
+ { id: 'P1_1', priority: 'P1' },
201
+ { id: 'P1_2', priority: 'P1' },
202
+ { id: 'P1_3', priority: 'P1' },
203
+ { id: 'P2_1', priority: 'P2' },
204
+ ]);
205
+
206
+ const result = selectPrinciplesForInjection(principles, 10000);
207
+
208
+ expect(result.breakdown.p0).toBe(2);
209
+ expect(result.breakdown.p1).toBe(3);
210
+ expect(result.breakdown.p2).toBe(1);
211
+ expect(result.selected).toHaveLength(6);
212
+ });
213
+ });
214
+
215
+ // ---------------------------------------------------------------------------
216
+ // Tests: DEFAULT_PRINCIPLE_BUDGET
217
+ // ---------------------------------------------------------------------------
218
+
219
+ describe('DEFAULT_PRINCIPLE_BUDGET', () => {
220
+ it('is set to 4000 characters', () => {
221
+ expect(DEFAULT_PRINCIPLE_BUDGET).toBe(4000);
222
+ });
223
+ });
@@ -0,0 +1,90 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import type { PrincipleInjector, InjectionContext } from '../../src/core/principle-injector.js';
3
+ import { DefaultPrincipleInjector } from '../../src/core/principle-injector.js';
4
+ import type { InjectablePrinciple } from '../../src/core/principle-injection.js';
5
+ import { selectPrinciplesForInjection, formatPrinciple } from '../../src/core/principle-injection.js';
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Helpers
9
+ // ---------------------------------------------------------------------------
10
+
11
+ function makePrinciple(overrides: Partial<InjectablePrinciple> = {}): InjectablePrinciple {
12
+ return {
13
+ id: overrides.id ?? 'P_001',
14
+ text: overrides.text ?? 'Always verify file content before editing',
15
+ priority: overrides.priority ?? 'P1',
16
+ createdAt: overrides.createdAt ?? '2026-04-01T00:00:00.000Z',
17
+ };
18
+ }
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Tests
22
+ // ---------------------------------------------------------------------------
23
+
24
+ describe('DefaultPrincipleInjector', () => {
25
+ const injector: PrincipleInjector = new DefaultPrincipleInjector();
26
+
27
+ it('getRelevantPrinciples delegates to selectPrinciplesForInjection', () => {
28
+ const principles = [
29
+ makePrinciple({ id: 'P0', priority: 'P0', text: 'Critical rule' }),
30
+ makePrinciple({ id: 'P1', priority: 'P1', text: 'Standard rule' }),
31
+ makePrinciple({ id: 'P2', priority: 'P2', text: 'Low priority rule' }),
32
+ makePrinciple({ id: 'P1b', priority: 'P1', text: 'Another standard rule' }),
33
+ makePrinciple({ id: 'P0b', priority: 'P0', text: 'Another critical rule' }),
34
+ ];
35
+ const context: InjectionContext = { domain: 'coding', sessionId: 's-1', budgetChars: 4000 };
36
+
37
+ const result = injector.getRelevantPrinciples(principles, context);
38
+ const expected = selectPrinciplesForInjection(principles, 4000).selected;
39
+
40
+ expect(result).toEqual(expected);
41
+ });
42
+
43
+ it('formatForInjection delegates to formatPrinciple', () => {
44
+ const principle = makePrinciple({ id: 'P_001', text: 'Test' });
45
+
46
+ const result = injector.formatForInjection(principle);
47
+ const expected = formatPrinciple(makePrinciple({ id: 'P_001', text: 'Test' }));
48
+
49
+ expect(result).toBe(expected);
50
+ });
51
+
52
+ it('formatForInjection returns "- [ID] text" format', () => {
53
+ const result = injector.formatForInjection(
54
+ makePrinciple({ id: 'P_001', text: 'Verify before edit' }),
55
+ );
56
+ expect(result).toBe('- [P_001] Verify before edit');
57
+ });
58
+
59
+ it('getRelevantPrinciples respects budget constraint', () => {
60
+ const principles = Array.from({ length: 20 }, (_, i) =>
61
+ makePrinciple({
62
+ id: `P_${i.toString().padStart(3, '0')}`,
63
+ text: 'A'.repeat(300), // 300 chars each
64
+ priority: i === 0 ? 'P0' : 'P2',
65
+ }),
66
+ );
67
+ const context: InjectionContext = { domain: 'coding', sessionId: 's-1', budgetChars: 500 };
68
+
69
+ const result = injector.getRelevantPrinciples(principles, context);
70
+
71
+ // Should include at least the forced P0, total chars may exceed budget slightly
72
+ expect(result.length).toBeGreaterThan(0);
73
+ expect(result.some(p => p.priority === 'P0')).toBe(true);
74
+ });
75
+
76
+ it('getRelevantPrinciples returns empty array for empty input', () => {
77
+ const context: InjectionContext = { domain: 'coding', sessionId: 's-1', budgetChars: 4000 };
78
+ const result = injector.getRelevantPrinciples([], context);
79
+ expect(result).toEqual([]);
80
+ });
81
+ });
82
+
83
+ describe('InjectionContext', () => {
84
+ it('has domain, sessionId, and budgetChars fields', () => {
85
+ const ctx: InjectionContext = { domain: 'coding', sessionId: 's-1', budgetChars: 4000 };
86
+ expect(ctx.domain).toBe('coding');
87
+ expect(ctx.sessionId).toBe('s-1');
88
+ expect(ctx.budgetChars).toBe(4000);
89
+ });
90
+ });