@diyor28/context 1.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.
Files changed (280) hide show
  1. package/README.md +270 -0
  2. package/dist/__tests__/attachment-selector.test.d.ts +11 -0
  3. package/dist/__tests__/attachment-selector.test.d.ts.map +1 -0
  4. package/dist/__tests__/attachment-selector.test.js +449 -0
  5. package/dist/__tests__/attachment-selector.test.js.map +1 -0
  6. package/dist/__tests__/cache-breakpoints.test.d.ts +11 -0
  7. package/dist/__tests__/cache-breakpoints.test.d.ts.map +1 -0
  8. package/dist/__tests__/cache-breakpoints.test.js +398 -0
  9. package/dist/__tests__/cache-breakpoints.test.js.map +1 -0
  10. package/dist/__tests__/codecs.test.d.ts +7 -0
  11. package/dist/__tests__/codecs.test.d.ts.map +1 -0
  12. package/dist/__tests__/codecs.test.js +331 -0
  13. package/dist/__tests__/codecs.test.js.map +1 -0
  14. package/dist/__tests__/compactor.test.d.ts +11 -0
  15. package/dist/__tests__/compactor.test.d.ts.map +1 -0
  16. package/dist/__tests__/compactor.test.js +519 -0
  17. package/dist/__tests__/compactor.test.js.map +1 -0
  18. package/dist/__tests__/context-graph.test.d.ts +7 -0
  19. package/dist/__tests__/context-graph.test.d.ts.map +1 -0
  20. package/dist/__tests__/context-graph.test.js +262 -0
  21. package/dist/__tests__/context-graph.test.js.map +1 -0
  22. package/dist/__tests__/hash.test.d.ts +7 -0
  23. package/dist/__tests__/hash.test.d.ts.map +1 -0
  24. package/dist/__tests__/hash.test.js +228 -0
  25. package/dist/__tests__/hash.test.js.map +1 -0
  26. package/dist/__tests__/integration.test.d.ts +15 -0
  27. package/dist/__tests__/integration.test.d.ts.map +1 -0
  28. package/dist/__tests__/integration.test.js +728 -0
  29. package/dist/__tests__/integration.test.js.map +1 -0
  30. package/dist/__tests__/kind-order.test.d.ts +7 -0
  31. package/dist/__tests__/kind-order.test.d.ts.map +1 -0
  32. package/dist/__tests__/kind-order.test.js +243 -0
  33. package/dist/__tests__/kind-order.test.js.map +1 -0
  34. package/dist/__tests__/phase2-integration.test.d.ts +5 -0
  35. package/dist/__tests__/phase2-integration.test.d.ts.map +1 -0
  36. package/dist/__tests__/phase2-integration.test.js +222 -0
  37. package/dist/__tests__/phase2-integration.test.js.map +1 -0
  38. package/dist/__tests__/queries.test.d.ts +7 -0
  39. package/dist/__tests__/queries.test.d.ts.map +1 -0
  40. package/dist/__tests__/queries.test.js +254 -0
  41. package/dist/__tests__/queries.test.js.map +1 -0
  42. package/dist/__tests__/token-estimator.test.d.ts +7 -0
  43. package/dist/__tests__/token-estimator.test.d.ts.map +1 -0
  44. package/dist/__tests__/token-estimator.test.js +267 -0
  45. package/dist/__tests__/token-estimator.test.js.map +1 -0
  46. package/dist/adapters/anthropic-estimator.d.ts +38 -0
  47. package/dist/adapters/anthropic-estimator.d.ts.map +1 -0
  48. package/dist/adapters/anthropic-estimator.js +108 -0
  49. package/dist/adapters/anthropic-estimator.js.map +1 -0
  50. package/dist/adapters/attachment-resolver.d.ts +96 -0
  51. package/dist/adapters/attachment-resolver.d.ts.map +1 -0
  52. package/dist/adapters/attachment-resolver.js +176 -0
  53. package/dist/adapters/attachment-resolver.js.map +1 -0
  54. package/dist/adapters/attachment-selector.d.ts +59 -0
  55. package/dist/adapters/attachment-selector.d.ts.map +1 -0
  56. package/dist/adapters/attachment-selector.js +163 -0
  57. package/dist/adapters/attachment-selector.js.map +1 -0
  58. package/dist/adapters/gemini-estimator.d.ts +27 -0
  59. package/dist/adapters/gemini-estimator.d.ts.map +1 -0
  60. package/dist/adapters/gemini-estimator.js +80 -0
  61. package/dist/adapters/gemini-estimator.js.map +1 -0
  62. package/dist/adapters/index.d.ts +12 -0
  63. package/dist/adapters/index.d.ts.map +1 -0
  64. package/dist/adapters/index.js +28 -0
  65. package/dist/adapters/index.js.map +1 -0
  66. package/dist/adapters/memory-store.d.ts +139 -0
  67. package/dist/adapters/memory-store.d.ts.map +1 -0
  68. package/dist/adapters/memory-store.js +187 -0
  69. package/dist/adapters/memory-store.js.map +1 -0
  70. package/dist/adapters/openai-estimator.d.ts +35 -0
  71. package/dist/adapters/openai-estimator.d.ts.map +1 -0
  72. package/dist/adapters/openai-estimator.js +89 -0
  73. package/dist/adapters/openai-estimator.js.map +1 -0
  74. package/dist/adapters/summarizer.d.ts +121 -0
  75. package/dist/adapters/summarizer.d.ts.map +1 -0
  76. package/dist/adapters/summarizer.js +121 -0
  77. package/dist/adapters/summarizer.js.map +1 -0
  78. package/dist/adapters/token-estimator.d.ts +63 -0
  79. package/dist/adapters/token-estimator.d.ts.map +1 -0
  80. package/dist/adapters/token-estimator.js +37 -0
  81. package/dist/adapters/token-estimator.js.map +1 -0
  82. package/dist/builder/context-builder.d.ts +186 -0
  83. package/dist/builder/context-builder.d.ts.map +1 -0
  84. package/dist/builder/context-builder.js +305 -0
  85. package/dist/builder/context-builder.js.map +1 -0
  86. package/dist/builder/context-fork.d.ts +166 -0
  87. package/dist/builder/context-fork.d.ts.map +1 -0
  88. package/dist/builder/context-fork.js +282 -0
  89. package/dist/builder/context-fork.js.map +1 -0
  90. package/dist/builder/index.d.ts +6 -0
  91. package/dist/builder/index.d.ts.map +1 -0
  92. package/dist/builder/index.js +22 -0
  93. package/dist/builder/index.js.map +1 -0
  94. package/dist/codecs/base.d.ts +18 -0
  95. package/dist/codecs/base.d.ts.map +1 -0
  96. package/dist/codecs/base.js +39 -0
  97. package/dist/codecs/base.js.map +1 -0
  98. package/dist/codecs/conversation-history.codec.d.ts +81 -0
  99. package/dist/codecs/conversation-history.codec.d.ts.map +1 -0
  100. package/dist/codecs/conversation-history.codec.js +89 -0
  101. package/dist/codecs/conversation-history.codec.js.map +1 -0
  102. package/dist/codecs/index.d.ts +31 -0
  103. package/dist/codecs/index.d.ts.map +1 -0
  104. package/dist/codecs/index.js +71 -0
  105. package/dist/codecs/index.js.map +1 -0
  106. package/dist/codecs/redacted-stub.codec.d.ts +32 -0
  107. package/dist/codecs/redacted-stub.codec.d.ts.map +1 -0
  108. package/dist/codecs/redacted-stub.codec.js +64 -0
  109. package/dist/codecs/redacted-stub.codec.js.map +1 -0
  110. package/dist/codecs/structured-reference.codec.d.ts +40 -0
  111. package/dist/codecs/structured-reference.codec.d.ts.map +1 -0
  112. package/dist/codecs/structured-reference.codec.js +81 -0
  113. package/dist/codecs/structured-reference.codec.js.map +1 -0
  114. package/dist/codecs/system-rules.codec.d.ts +32 -0
  115. package/dist/codecs/system-rules.codec.d.ts.map +1 -0
  116. package/dist/codecs/system-rules.codec.js +62 -0
  117. package/dist/codecs/system-rules.codec.js.map +1 -0
  118. package/dist/codecs/tool-output.codec.d.ts +66 -0
  119. package/dist/codecs/tool-output.codec.d.ts.map +1 -0
  120. package/dist/codecs/tool-output.codec.js +95 -0
  121. package/dist/codecs/tool-output.codec.js.map +1 -0
  122. package/dist/codecs/tool-schema.codec.d.ts +36 -0
  123. package/dist/codecs/tool-schema.codec.d.ts.map +1 -0
  124. package/dist/codecs/tool-schema.codec.js +74 -0
  125. package/dist/codecs/tool-schema.codec.js.map +1 -0
  126. package/dist/codecs/unsafe-text.codec.d.ts +28 -0
  127. package/dist/codecs/unsafe-text.codec.d.ts.map +1 -0
  128. package/dist/codecs/unsafe-text.codec.js +63 -0
  129. package/dist/codecs/unsafe-text.codec.js.map +1 -0
  130. package/dist/graph/context-graph.d.ts +121 -0
  131. package/dist/graph/context-graph.d.ts.map +1 -0
  132. package/dist/graph/context-graph.js +166 -0
  133. package/dist/graph/context-graph.js.map +1 -0
  134. package/dist/graph/index.d.ts +8 -0
  135. package/dist/graph/index.d.ts.map +1 -0
  136. package/dist/graph/index.js +24 -0
  137. package/dist/graph/index.js.map +1 -0
  138. package/dist/graph/kind-order.d.ts +60 -0
  139. package/dist/graph/kind-order.d.ts.map +1 -0
  140. package/dist/graph/kind-order.js +113 -0
  141. package/dist/graph/kind-order.js.map +1 -0
  142. package/dist/graph/queries.d.ts +68 -0
  143. package/dist/graph/queries.d.ts.map +1 -0
  144. package/dist/graph/queries.js +240 -0
  145. package/dist/graph/queries.js.map +1 -0
  146. package/dist/graph/views.d.ts +90 -0
  147. package/dist/graph/views.d.ts.map +1 -0
  148. package/dist/graph/views.js +173 -0
  149. package/dist/graph/views.js.map +1 -0
  150. package/dist/index.d.ts +16 -0
  151. package/dist/index.d.ts.map +1 -0
  152. package/dist/index.js +40 -0
  153. package/dist/index.js.map +1 -0
  154. package/dist/pipeline/compactor.d.ts +128 -0
  155. package/dist/pipeline/compactor.d.ts.map +1 -0
  156. package/dist/pipeline/compactor.js +346 -0
  157. package/dist/pipeline/compactor.js.map +1 -0
  158. package/dist/pipeline/index.d.ts +6 -0
  159. package/dist/pipeline/index.d.ts.map +1 -0
  160. package/dist/pipeline/index.js +22 -0
  161. package/dist/pipeline/index.js.map +1 -0
  162. package/dist/pipeline/summarizer.d.ts +18 -0
  163. package/dist/pipeline/summarizer.d.ts.map +1 -0
  164. package/dist/pipeline/summarizer.js +68 -0
  165. package/dist/pipeline/summarizer.js.map +1 -0
  166. package/dist/policies/default-policy.d.ts +29 -0
  167. package/dist/policies/default-policy.d.ts.map +1 -0
  168. package/dist/policies/default-policy.js +58 -0
  169. package/dist/policies/default-policy.js.map +1 -0
  170. package/dist/policies/index.d.ts +5 -0
  171. package/dist/policies/index.d.ts.map +1 -0
  172. package/dist/policies/index.js +21 -0
  173. package/dist/policies/index.js.map +1 -0
  174. package/dist/providers/anthropic-compiler.d.ts +58 -0
  175. package/dist/providers/anthropic-compiler.d.ts.map +1 -0
  176. package/dist/providers/anthropic-compiler.js +182 -0
  177. package/dist/providers/anthropic-compiler.js.map +1 -0
  178. package/dist/providers/capabilities.d.ts +54 -0
  179. package/dist/providers/capabilities.d.ts.map +1 -0
  180. package/dist/providers/capabilities.js +87 -0
  181. package/dist/providers/capabilities.js.map +1 -0
  182. package/dist/providers/gemini-compiler.d.ts +51 -0
  183. package/dist/providers/gemini-compiler.d.ts.map +1 -0
  184. package/dist/providers/gemini-compiler.js +206 -0
  185. package/dist/providers/gemini-compiler.js.map +1 -0
  186. package/dist/providers/index.d.ts +8 -0
  187. package/dist/providers/index.d.ts.map +1 -0
  188. package/dist/providers/index.js +24 -0
  189. package/dist/providers/index.js.map +1 -0
  190. package/dist/providers/openai-compiler.d.ts +46 -0
  191. package/dist/providers/openai-compiler.d.ts.map +1 -0
  192. package/dist/providers/openai-compiler.js +149 -0
  193. package/dist/providers/openai-compiler.js.map +1 -0
  194. package/dist/types/attachment.d.ts +62 -0
  195. package/dist/types/attachment.d.ts.map +1 -0
  196. package/dist/types/attachment.js +6 -0
  197. package/dist/types/attachment.js.map +1 -0
  198. package/dist/types/block.d.ts +61 -0
  199. package/dist/types/block.d.ts.map +1 -0
  200. package/dist/types/block.js +8 -0
  201. package/dist/types/block.js.map +1 -0
  202. package/dist/types/codec.d.ts +58 -0
  203. package/dist/types/codec.d.ts.map +1 -0
  204. package/dist/types/codec.js +6 -0
  205. package/dist/types/codec.js.map +1 -0
  206. package/dist/types/compiled.d.ts +91 -0
  207. package/dist/types/compiled.d.ts.map +1 -0
  208. package/dist/types/compiled.js +6 -0
  209. package/dist/types/compiled.js.map +1 -0
  210. package/dist/types/hash.d.ts +24 -0
  211. package/dist/types/hash.d.ts.map +1 -0
  212. package/dist/types/hash.js +49 -0
  213. package/dist/types/hash.js.map +1 -0
  214. package/dist/types/index.d.ts +10 -0
  215. package/dist/types/index.d.ts.map +1 -0
  216. package/dist/types/index.js +26 -0
  217. package/dist/types/index.js.map +1 -0
  218. package/dist/types/policy.d.ts +128 -0
  219. package/dist/types/policy.d.ts.map +1 -0
  220. package/dist/types/policy.js +55 -0
  221. package/dist/types/policy.js.map +1 -0
  222. package/package.json +55 -0
  223. package/postcss.config.js +4 -0
  224. package/src/__tests__/attachment-selector.test.ts +559 -0
  225. package/src/__tests__/cache-breakpoints.test.ts +566 -0
  226. package/src/__tests__/codecs.test.ts +417 -0
  227. package/src/__tests__/compactor.test.ts +608 -0
  228. package/src/__tests__/context-graph.test.ts +383 -0
  229. package/src/__tests__/hash.test.ts +274 -0
  230. package/src/__tests__/integration.test.ts +866 -0
  231. package/src/__tests__/kind-order.test.ts +312 -0
  232. package/src/__tests__/phase2-integration.test.ts +253 -0
  233. package/src/__tests__/queries.test.ts +387 -0
  234. package/src/__tests__/token-estimator.test.ts +326 -0
  235. package/src/adapters/anthropic-estimator.ts +125 -0
  236. package/src/adapters/attachment-resolver.ts +295 -0
  237. package/src/adapters/attachment-selector.ts +218 -0
  238. package/src/adapters/gemini-estimator.ts +93 -0
  239. package/src/adapters/index.ts +12 -0
  240. package/src/adapters/memory-store.ts +299 -0
  241. package/src/adapters/openai-estimator.ts +105 -0
  242. package/src/adapters/summarizer.ts +250 -0
  243. package/src/adapters/token-estimator.ts +74 -0
  244. package/src/builder/context-builder.ts +467 -0
  245. package/src/builder/context-fork.ts +471 -0
  246. package/src/builder/index.ts +6 -0
  247. package/src/codecs/base.ts +36 -0
  248. package/src/codecs/conversation-history.codec.ts +108 -0
  249. package/src/codecs/index.ts +57 -0
  250. package/src/codecs/redacted-stub.codec.ts +76 -0
  251. package/src/codecs/structured-reference.codec.ts +96 -0
  252. package/src/codecs/system-rules.codec.ts +74 -0
  253. package/src/codecs/tool-output.codec.ts +109 -0
  254. package/src/codecs/tool-schema.codec.ts +87 -0
  255. package/src/codecs/unsafe-text.codec.ts +74 -0
  256. package/src/graph/context-graph.ts +205 -0
  257. package/src/graph/index.ts +8 -0
  258. package/src/graph/kind-order.ts +125 -0
  259. package/src/graph/queries.ts +306 -0
  260. package/src/graph/views.ts +255 -0
  261. package/src/index.ts +31 -0
  262. package/src/pipeline/compactor.ts +563 -0
  263. package/src/pipeline/index.ts +6 -0
  264. package/src/pipeline/summarizer.ts +76 -0
  265. package/src/policies/default-policy.ts +69 -0
  266. package/src/policies/index.ts +5 -0
  267. package/src/providers/anthropic-compiler.ts +294 -0
  268. package/src/providers/capabilities.ts +144 -0
  269. package/src/providers/gemini-compiler.ts +272 -0
  270. package/src/providers/index.ts +8 -0
  271. package/src/providers/openai-compiler.ts +191 -0
  272. package/src/types/attachment.ts +86 -0
  273. package/src/types/block.ts +84 -0
  274. package/src/types/codec.ts +68 -0
  275. package/src/types/compiled.ts +109 -0
  276. package/src/types/hash.ts +58 -0
  277. package/src/types/index.ts +10 -0
  278. package/src/types/policy.ts +194 -0
  279. package/tsconfig.json +21 -0
  280. package/vitest.config.ts +21 -0
@@ -0,0 +1,866 @@
1
+ /**
2
+ * Integration tests for @foundry/context library.
3
+ *
4
+ * Tests end-to-end behavior including:
5
+ * - Pipeline purity (compilation determinism)
6
+ * - Compaction provenance
7
+ * - Fork sensitivity redaction
8
+ * - Execution hash reproducibility
9
+ * - Token estimation confidence
10
+ * - Cache breakpoint resolution
11
+ * - Tool output pruning
12
+ * - Attachment budget enforcement
13
+ */
14
+
15
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
16
+ import { ContextBuilder } from '../builder/context-builder.js';
17
+ import { ContextFork, computeExecutionHash, computeSchemaHash, filterBySensitivity } from '../builder/context-fork.js';
18
+ import { compactView, type PipelineCompactionConfig } from '../pipeline/compactor.js';
19
+ import { compileAnthropicContext, type CacheBreakpointSelector } from '../providers/anthropic-compiler.js';
20
+ import { OpenAITokenEstimator } from '../adapters/openai-estimator.js';
21
+ import { heuristicTokenCount, LOW_CONFIDENCE_MULTIPLIER, serializeBlockForEstimation, type TokenEstimator, type TokenEstimate } from '../adapters/token-estimator.js';
22
+ import { SystemRulesCodec } from '../codecs/system-rules.codec.js';
23
+ import { ToolOutputCodec } from '../codecs/tool-output.codec.js';
24
+ import { RedactedStubCodec } from '../codecs/redacted-stub.codec.js';
25
+ import type { ContextPolicy } from '../types/policy.js';
26
+ import type { ContextBlock } from '../types/block.js';
27
+ import type { BlockCodec } from '../types/codec.js';
28
+ import type { ResolvedAttachment, AttachmentRef } from '../types/attachment.js';
29
+ import { AttachmentSelector, type RankedAttachment } from '../adapters/attachment-selector.js';
30
+ import type { AttachmentPolicy } from '../types/policy.js';
31
+ import { z } from 'zod';
32
+
33
+ // Mock token estimator for tests
34
+ class MockTokenEstimator implements TokenEstimator {
35
+ async estimate(blocks: ContextBlock<unknown>[]): Promise<TokenEstimate> {
36
+ const totalTokens = blocks.reduce((sum, block) => {
37
+ const text = serializeBlockForEstimation(block);
38
+ return sum + heuristicTokenCount(text);
39
+ }, 0);
40
+ return { tokens: totalTokens, confidence: 'low' };
41
+ }
42
+
43
+ async estimateBlock(block: ContextBlock<unknown>): Promise<TokenEstimate> {
44
+ const text = serializeBlockForEstimation(block);
45
+ return { tokens: heuristicTokenCount(text), confidence: 'low' };
46
+ }
47
+ }
48
+
49
+ describe('Integration Tests', () => {
50
+ describe('Pipeline purity', () => {
51
+ it('should produce identical output for repeated compilation with same policy/provider', async () => {
52
+ // Build context with multiple blocks
53
+ const builder = new ContextBuilder();
54
+ builder
55
+ .system({ text: 'You are a helpful assistant.' })
56
+ .history([
57
+ { role: 'user', content: 'Hello' },
58
+ { role: 'assistant', content: 'Hi there!' },
59
+ ])
60
+ .turn('What is the capital of France?');
61
+
62
+ const policy: ContextPolicy = {
63
+ provider: 'anthropic',
64
+ modelId: 'claude-sonnet-4-5',
65
+ contextWindow: 200000,
66
+ completionReserve: 8000,
67
+ overflowStrategy: 'truncate',
68
+ kindPriorities: [],
69
+ sensitivity: {
70
+ maxSensitivity: 'public',
71
+ redactRestricted: true,
72
+ },
73
+ };
74
+
75
+ // Create codec registry
76
+ const codecRegistry = new Map();
77
+ codecRegistry.set('system-rules', SystemRulesCodec);
78
+ codecRegistry.set('conversation-history', {
79
+ codecId: 'conversation-history',
80
+ version: '1.0.0',
81
+ payloadSchema: {},
82
+ canonicalize: (p: any) => p,
83
+ hash: () => 'history-hash',
84
+ render: (block: any) => ({
85
+ anthropic: block.payload.messages.map((m: any) => ({
86
+ role: m.role,
87
+ content: m.content,
88
+ })),
89
+ openai: {},
90
+ gemini: {},
91
+ }),
92
+ validate: (p: any) => p,
93
+ });
94
+ codecRegistry.set('user-turn', {
95
+ codecId: 'user-turn',
96
+ version: '1.0.0',
97
+ payloadSchema: {},
98
+ canonicalize: (p: any) => p,
99
+ hash: () => 'turn-hash',
100
+ render: (block: any) => ({
101
+ anthropic: { role: 'user', content: block.payload.text },
102
+ openai: {},
103
+ gemini: {},
104
+ }),
105
+ validate: (p: any) => p,
106
+ });
107
+
108
+ // Compile twice
109
+ const graph = builder.getGraph();
110
+ const view1 = await graph.createView({});
111
+ const view2 = await graph.createView({});
112
+
113
+ const compiled1 = compileAnthropicContext([...view1.blocks], policy, { codecRegistry });
114
+ const compiled2 = compileAnthropicContext([...view2.blocks], policy, { codecRegistry });
115
+
116
+ // Verify identical outputs
117
+ expect(compiled1.messages).toEqual(compiled2.messages);
118
+ expect(compiled1.system).toEqual(compiled2.system);
119
+ expect(compiled1.modelId).toBe(compiled2.modelId);
120
+ expect(compiled1.provider).toBe(compiled2.provider);
121
+
122
+ // Verify stable prefix hash
123
+ expect(view1.stablePrefixHash).toBe(view2.stablePrefixHash);
124
+ });
125
+
126
+ it('should produce different output when blocks change', async () => {
127
+ const builder1 = new ContextBuilder();
128
+ builder1.system({ text: 'You are a helpful assistant.' });
129
+
130
+ const builder2 = new ContextBuilder();
131
+ builder2.system({ text: 'You are a coding expert.' });
132
+
133
+ const view1 = await builder1.getGraph().createView({});
134
+ const view2 = await builder2.getGraph().createView({});
135
+
136
+ // Verify different hashes
137
+ expect(view1.stablePrefixHash).not.toBe(view2.stablePrefixHash);
138
+ });
139
+ });
140
+
141
+ describe('Compaction provenance', () => {
142
+ it('should produce valid provenance for compacted blocks', async () => {
143
+ const builder = new ContextBuilder();
144
+
145
+ // Add tool output blocks with large content
146
+ for (let i = 0; i < 5; i++) {
147
+ const toolBlock: ContextBlock<any> = {
148
+ blockHash: `tool-hash-${i}`,
149
+ meta: {
150
+ kind: 'tool_output',
151
+ sensitivity: 'public',
152
+ codecId: 'tool-output',
153
+ codecVersion: '1.0.0',
154
+ createdAt: Math.floor(Date.now() / 1000) + i,
155
+ },
156
+ payload: {
157
+ tool_name: 'bash',
158
+ output: 'x'.repeat(1000), // Large output
159
+ status: 'success',
160
+ },
161
+ };
162
+
163
+ builder.getGraph().addBlock(toolBlock);
164
+ }
165
+
166
+ const view = await builder.getGraph().createView({});
167
+
168
+ // Compact with tool output pruning
169
+ const compactionConfig: PipelineCompactionConfig = {
170
+ steps: ['tool_output_prune'],
171
+ toolOutputPruning: {
172
+ maxRawTailChars: 100,
173
+ preserveErrorTail: true,
174
+ maxOutputsPerTool: 3,
175
+ },
176
+ };
177
+
178
+ const estimator = new MockTokenEstimator();
179
+ const result = await compactView(view, compactionConfig, estimator);
180
+
181
+ // Verify compaction happened
182
+ expect(result.blocks.length).toBeLessThanOrEqual(3);
183
+ expect(result.removedBlocks.length).toBeGreaterThan(0);
184
+
185
+ // Verify replaced blocks have provenance tags
186
+ const compactedBlocks = result.blocks.filter((b) =>
187
+ b.meta.tags?.some((tag) => tag.startsWith('compacted:'))
188
+ );
189
+
190
+ expect(compactedBlocks.length).toBeGreaterThan(0);
191
+
192
+ for (const block of compactedBlocks) {
193
+ // Verify compaction tag
194
+ expect(block.meta.tags).toContain('compacted:tool_output_prune');
195
+
196
+ // Verify source indicates compaction
197
+ expect(block.meta.source).toContain(':compacted');
198
+
199
+ // Verify truncated payload
200
+ if (block.payload && typeof block.payload === 'object' && 'output' in block.payload) {
201
+ const output = (block.payload as any).output;
202
+ expect(output.length).toBeLessThanOrEqual(100 + 50); // Max tail + truncation marker
203
+ }
204
+ }
205
+ });
206
+
207
+ it('should track deduplication correctly', async () => {
208
+ // NOTE: ContextGraph.addBlock is idempotent, so we need to create
209
+ // duplicates in the view directly to test deduplication
210
+ const duplicateBlock: ContextBlock<string> = {
211
+ blockHash: 'duplicate-hash',
212
+ meta: {
213
+ kind: 'memory',
214
+ sensitivity: 'public',
215
+ codecId: 'test',
216
+ codecVersion: '1.0.0',
217
+ createdAt: Math.floor(Date.now() / 1000),
218
+ },
219
+ payload: 'duplicate content',
220
+ };
221
+
222
+ // Create view with duplicates manually (since graph is idempotent)
223
+ const view: any = {
224
+ blocks: [duplicateBlock, { ...duplicateBlock }, { ...duplicateBlock }],
225
+ stablePrefixHash: 'test-hash',
226
+ createdAt: Math.floor(Date.now() / 1000),
227
+ };
228
+
229
+ // Compact with deduplication
230
+ const compactionConfig: PipelineCompactionConfig = {
231
+ steps: ['dedupe'],
232
+ };
233
+
234
+ const estimator = new MockTokenEstimator();
235
+ const result = await compactView(view, compactionConfig, estimator);
236
+
237
+ // Verify only one instance remains
238
+ const duplicates = result.blocks.filter((b) => b.blockHash === 'duplicate-hash');
239
+ expect(duplicates.length).toBe(1);
240
+
241
+ // Verify 2 blocks were removed
242
+ expect(result.removedBlocks.length).toBe(2);
243
+
244
+ // Verify report
245
+ expect(result.report.stepsApplied).toContain('dedupe');
246
+ const dedupeReport = result.report.stepReports.find((r) => r.step === 'dedupe');
247
+ expect(dedupeReport?.blocksRemoved).toBe(2);
248
+ expect(dedupeReport?.lossy).toBe(false);
249
+ });
250
+ });
251
+
252
+ describe('Fork sensitivity redaction', () => {
253
+ it('should replace sensitive blocks with RedactedStubs', async () => {
254
+ const builder = new ContextBuilder();
255
+
256
+ // Add public block
257
+ builder.system({ text: 'Public system prompt' }, { sensitivity: 'public' });
258
+
259
+ // Add internal blocks
260
+ const internalBlock: ContextBlock<string> = {
261
+ blockHash: 'internal-hash',
262
+ meta: {
263
+ kind: 'memory',
264
+ sensitivity: 'internal',
265
+ codecId: 'test',
266
+ codecVersion: '1.0.0',
267
+ createdAt: Math.floor(Date.now() / 1000),
268
+ tags: ['secret'],
269
+ },
270
+ payload: 'Internal company information',
271
+ };
272
+ builder.getGraph().addBlock(internalBlock);
273
+
274
+ // Add restricted block
275
+ const restrictedBlock: ContextBlock<string> = {
276
+ blockHash: 'restricted-hash',
277
+ meta: {
278
+ kind: 'state',
279
+ sensitivity: 'restricted',
280
+ codecId: 'test',
281
+ codecVersion: '1.0.0',
282
+ createdAt: Math.floor(Date.now() / 1000),
283
+ },
284
+ payload: 'Highly sensitive data',
285
+ };
286
+ builder.getGraph().addBlock(restrictedBlock);
287
+
288
+ const view = await builder.getGraph().createView({});
289
+
290
+ // Filter by 'public' sensitivity
291
+ const filteredBlocks = filterBySensitivity(view, 'public');
292
+
293
+ // Verify public block is unchanged
294
+ const publicBlocks = filteredBlocks.filter(
295
+ (b) => b.meta.codecId === 'system-rules'
296
+ );
297
+ expect(publicBlocks.length).toBe(1);
298
+ expect(publicBlocks[0].meta.sensitivity).toBe('public');
299
+
300
+ // Verify internal/restricted blocks are redacted
301
+ const redactedBlocks = filteredBlocks.filter(
302
+ (b) => b.meta.codecId === RedactedStubCodec.codecId
303
+ );
304
+ expect(redactedBlocks.length).toBe(2);
305
+
306
+ for (const block of redactedBlocks) {
307
+ expect(block.meta.sensitivity).toBe('public');
308
+ expect(block.payload).toHaveProperty('originalBlockHash');
309
+ expect(block.payload).toHaveProperty('reason');
310
+ expect((block.payload as any).reason).toContain('exceeds maximum');
311
+ }
312
+ });
313
+
314
+ it('should allow internal blocks when maxSensitivity is internal', async () => {
315
+ const builder = new ContextBuilder();
316
+
317
+ builder.system({ text: 'Public' }, { sensitivity: 'public' });
318
+
319
+ const internalBlock: ContextBlock<string> = {
320
+ blockHash: 'internal-hash',
321
+ meta: {
322
+ kind: 'memory',
323
+ sensitivity: 'internal',
324
+ codecId: 'test',
325
+ codecVersion: '1.0.0',
326
+ createdAt: Math.floor(Date.now() / 1000),
327
+ },
328
+ payload: 'Internal',
329
+ };
330
+ builder.getGraph().addBlock(internalBlock);
331
+
332
+ const view = await builder.getGraph().createView({});
333
+ const filteredBlocks = filterBySensitivity(view, 'internal');
334
+
335
+ // Verify both blocks are kept
336
+ const redactedBlocks = filteredBlocks.filter(
337
+ (b) => b.meta.codecId === RedactedStubCodec.codecId
338
+ );
339
+ expect(redactedBlocks.length).toBe(0);
340
+ });
341
+ });
342
+
343
+ describe('Execution hash reproducibility', () => {
344
+ it('should produce identical executionHash for same inputs', () => {
345
+ const model = { provider: 'anthropic' as const, model: 'claude-sonnet-4-5' };
346
+ const viewHash = 'view-hash-123';
347
+ const instruction = 'Analyze this code';
348
+ const schema = z.object({ result: z.string() });
349
+ const schemaHash = computeSchemaHash(schema);
350
+
351
+ const hash1 = computeExecutionHash(model, viewHash, instruction, schemaHash);
352
+ const hash2 = computeExecutionHash(model, viewHash, instruction, schemaHash);
353
+
354
+ expect(hash1).toBe(hash2);
355
+ expect(hash1).toMatch(/^[a-f0-9]{64}$/); // Valid SHA-256 hex
356
+ });
357
+
358
+ it('should produce different executionHash when inputs change', () => {
359
+ const model1 = { provider: 'anthropic' as const, model: 'claude-sonnet-4-5' };
360
+ const model2 = { provider: 'anthropic' as const, model: 'claude-opus-4-5' };
361
+ const viewHash = 'view-hash-123';
362
+ const instruction = 'Analyze this code';
363
+ const schema = z.object({ result: z.string() });
364
+ const schemaHash = computeSchemaHash(schema);
365
+
366
+ const hash1 = computeExecutionHash(model1, viewHash, instruction, schemaHash);
367
+ const hash2 = computeExecutionHash(model2, viewHash, instruction, schemaHash);
368
+
369
+ expect(hash1).not.toBe(hash2);
370
+ });
371
+
372
+ it('should include toolset version in hash computation', () => {
373
+ const model = { provider: 'anthropic' as const, model: 'claude-sonnet-4-5' };
374
+ const viewHash = 'view-hash-123';
375
+ const instruction = 'Analyze this code';
376
+ const schema = z.object({ result: z.string() });
377
+ const schemaHash = computeSchemaHash(schema);
378
+
379
+ const hashWithoutToolset = computeExecutionHash(model, viewHash, instruction, schemaHash);
380
+ const hashWithToolset = computeExecutionHash(
381
+ model,
382
+ viewHash,
383
+ instruction,
384
+ schemaHash,
385
+ 'v1.2.3'
386
+ );
387
+
388
+ expect(hashWithoutToolset).not.toBe(hashWithToolset);
389
+ });
390
+ });
391
+
392
+ describe('Token estimation confidence', () => {
393
+ it('should apply safety multiplier for low-confidence estimates', () => {
394
+ const text = 'a'.repeat(100); // 100 chars
395
+
396
+ const heuristicTokens = heuristicTokenCount(text);
397
+
398
+ // Verify multiplier is applied
399
+ const baseEstimate = Math.ceil(100 / 4); // 25 tokens
400
+ const expectedWithMultiplier = Math.ceil(baseEstimate * LOW_CONFIDENCE_MULTIPLIER);
401
+
402
+ expect(heuristicTokens).toBe(expectedWithMultiplier);
403
+ expect(heuristicTokens).toBeGreaterThan(baseEstimate);
404
+ });
405
+
406
+ it('should use 1.2x safety multiplier', () => {
407
+ expect(LOW_CONFIDENCE_MULTIPLIER).toBe(1.2);
408
+ });
409
+
410
+ it('should estimate tokens consistently for same input', () => {
411
+ const text = 'Test input for token estimation';
412
+
413
+ const estimate1 = heuristicTokenCount(text);
414
+ const estimate2 = heuristicTokenCount(text);
415
+
416
+ expect(estimate1).toBe(estimate2);
417
+ });
418
+ });
419
+
420
+ describe('Cache breakpoint resolution', () => {
421
+ it('should resolve cache breakpoint to "after last match"', async () => {
422
+ const builder = new ContextBuilder();
423
+
424
+ // Add multiple system blocks
425
+ builder.system({ text: 'System 1' }, { tags: ['cacheable'] });
426
+ builder.system({ text: 'System 2' }, { tags: ['cacheable'] });
427
+ builder.system({ text: 'System 3' }, { tags: ['other'] });
428
+ builder.system({ text: 'System 4' }, { tags: ['cacheable'] });
429
+
430
+ const view = await builder.getGraph().createView({});
431
+ const policy: ContextPolicy = {
432
+ provider: 'anthropic',
433
+ modelId: 'claude-sonnet-4-5',
434
+ contextWindow: 200000,
435
+ completionReserve: 8000,
436
+ overflowStrategy: 'truncate',
437
+ kindPriorities: [],
438
+ sensitivity: {
439
+ maxSensitivity: 'public',
440
+ redactRestricted: true,
441
+ },
442
+ };
443
+
444
+ // Create codec registry
445
+ const codecRegistry = new Map();
446
+ codecRegistry.set('system-rules', SystemRulesCodec);
447
+
448
+ const cacheBreakpoint: CacheBreakpointSelector = {
449
+ tag: 'cacheable',
450
+ };
451
+
452
+ const compiled = compileAnthropicContext([...view.blocks], policy, {
453
+ codecRegistry,
454
+ cacheBreakpoint,
455
+ });
456
+
457
+ // Verify cache_control is on the last cacheable block (index 3)
458
+ expect(compiled.system).toBeDefined();
459
+ if (compiled.system) {
460
+ const cacheControlBlocks = compiled.system.filter((msg) => msg.cache_control);
461
+ expect(cacheControlBlocks.length).toBe(1);
462
+
463
+ // Last cacheable block should have cache control
464
+ expect(compiled.system[3]).toHaveProperty('cache_control');
465
+ expect(compiled.system[3].cache_control).toEqual({ type: 'ephemeral' });
466
+ }
467
+ });
468
+
469
+ it('should handle no matching blocks gracefully', async () => {
470
+ const builder = new ContextBuilder();
471
+ builder.system({ text: 'System 1' });
472
+
473
+ const view = await builder.getGraph().createView({});
474
+ const policy: ContextPolicy = {
475
+ provider: 'anthropic',
476
+ modelId: 'claude-sonnet-4-5',
477
+ contextWindow: 200000,
478
+ completionReserve: 8000,
479
+ overflowStrategy: 'truncate',
480
+ kindPriorities: [],
481
+ sensitivity: {
482
+ maxSensitivity: 'public',
483
+ redactRestricted: true,
484
+ },
485
+ };
486
+
487
+ const codecRegistry = new Map();
488
+ codecRegistry.set('system-rules', SystemRulesCodec);
489
+
490
+ const cacheBreakpoint: CacheBreakpointSelector = {
491
+ tag: 'nonexistent',
492
+ };
493
+
494
+ const compiled = compileAnthropicContext([...view.blocks], policy, {
495
+ codecRegistry,
496
+ cacheBreakpoint,
497
+ });
498
+
499
+ // Verify no cache control when no blocks match
500
+ if (compiled.system) {
501
+ const cacheControlBlocks = compiled.system.filter((msg) => msg.cache_control);
502
+ expect(cacheControlBlocks.length).toBe(0);
503
+ }
504
+ });
505
+ });
506
+
507
+ describe('Tool output pruning', () => {
508
+ it('should enforce maxOutputsPerTool', async () => {
509
+ const builder = new ContextBuilder();
510
+
511
+ // Add 10 tool outputs from same tool
512
+ for (let i = 0; i < 10; i++) {
513
+ const toolBlock: ContextBlock<any> = {
514
+ blockHash: `tool-hash-${i}`,
515
+ meta: {
516
+ kind: 'tool_output',
517
+ sensitivity: 'public',
518
+ codecId: 'tool-bash',
519
+ codecVersion: '1.0.0',
520
+ createdAt: Math.floor(Date.now() / 1000) + i,
521
+ },
522
+ payload: {
523
+ tool_name: 'bash',
524
+ output: `output ${i}`,
525
+ status: 'success',
526
+ },
527
+ };
528
+
529
+ builder.getGraph().addBlock(toolBlock);
530
+ }
531
+
532
+ const view = await builder.getGraph().createView({});
533
+
534
+ const compactionConfig: PipelineCompactionConfig = {
535
+ steps: ['tool_output_prune'],
536
+ toolOutputPruning: {
537
+ maxRawTailChars: 500,
538
+ preserveErrorTail: true,
539
+ maxOutputsPerTool: 3,
540
+ },
541
+ };
542
+
543
+ const estimator = new MockTokenEstimator();
544
+ const result = await compactView(view, compactionConfig, estimator);
545
+
546
+ // Verify only 3 tool outputs remain
547
+ const toolOutputs = result.blocks.filter((b) => b.meta.kind === 'tool_output');
548
+ expect(toolOutputs.length).toBe(3);
549
+
550
+ // Verify 7 were removed
551
+ expect(result.removedBlocks.length).toBe(7);
552
+
553
+ // Verify kept blocks are the most recent
554
+ const keptHashes = toolOutputs.map((b) => b.blockHash).sort();
555
+ expect(keptHashes).toContain('tool-hash-7');
556
+ expect(keptHashes).toContain('tool-hash-8');
557
+ expect(keptHashes).toContain('tool-hash-9');
558
+ });
559
+
560
+ it('should preserve error outputs even if over maxOutputsPerTool', async () => {
561
+ // NOTE: The current implementation of pruneToolOutputs keeps the most
562
+ // recent N outputs per tool, but doesn't preserve errors separately.
563
+ // This test verifies that large error outputs preserve the tail.
564
+ const view: any = {
565
+ blocks: [
566
+ {
567
+ blockHash: 'error-hash',
568
+ meta: {
569
+ kind: 'tool_output',
570
+ sensitivity: 'public',
571
+ codecId: 'tool-bash',
572
+ codecVersion: '1.0.0',
573
+ createdAt: Math.floor(Date.now() / 1000) + 3, // Most recent
574
+ },
575
+ payload: {
576
+ tool_name: 'bash',
577
+ output: 'x'.repeat(1000), // Large output
578
+ status: 'error',
579
+ error: true,
580
+ },
581
+ },
582
+ ],
583
+ stablePrefixHash: 'test-hash',
584
+ createdAt: Math.floor(Date.now() / 1000),
585
+ };
586
+
587
+ const compactionConfig: PipelineCompactionConfig = {
588
+ steps: ['tool_output_prune'],
589
+ toolOutputPruning: {
590
+ maxRawTailChars: 100,
591
+ preserveErrorTail: true,
592
+ maxOutputsPerTool: 3,
593
+ },
594
+ };
595
+
596
+ const estimator = new MockTokenEstimator();
597
+ const result = await compactView(view, compactionConfig, estimator);
598
+
599
+ // Error block should be kept (recent enough)
600
+ const errorBlocks = result.blocks.filter((b) => b.blockHash === 'error-hash');
601
+ expect(errorBlocks.length).toBe(1);
602
+
603
+ // Verify error tail is NOT truncated (preserveErrorTail = true)
604
+ const errorBlock = errorBlocks[0];
605
+ if (errorBlock.payload && typeof errorBlock.payload === 'object' && 'output' in errorBlock.payload) {
606
+ const output = (errorBlock.payload as any).output;
607
+ expect(output.length).toBe(1000); // Not truncated
608
+ }
609
+ });
610
+ });
611
+
612
+ describe('Attachment budget enforcement', () => {
613
+ it('should select attachments within budget', async () => {
614
+ const attachments: RankedAttachment[] = [
615
+ {
616
+ attachmentId: 'att1',
617
+ filename: 'file1.txt',
618
+ mimeType: 'text/plain',
619
+ sizeBytes: 200,
620
+ storage: 'local',
621
+ storagePath: '/tmp/file1.txt',
622
+ createdAt: Math.floor(Date.now() / 1000),
623
+ text: 'a'.repeat(200), // ~50 tokens
624
+ purpose: 'evidence',
625
+ userMention: true,
626
+ rankScore: 0,
627
+ },
628
+ {
629
+ attachmentId: 'att2',
630
+ filename: 'file2.txt',
631
+ mimeType: 'text/plain',
632
+ sizeBytes: 200,
633
+ storage: 'local',
634
+ storagePath: '/tmp/file2.txt',
635
+ createdAt: Math.floor(Date.now() / 1000),
636
+ text: 'b'.repeat(200), // ~50 tokens
637
+ purpose: 'context',
638
+ userMention: false,
639
+ rankScore: 0,
640
+ },
641
+ {
642
+ attachmentId: 'att3',
643
+ filename: 'file3.txt',
644
+ mimeType: 'text/plain',
645
+ sizeBytes: 200,
646
+ storage: 'local',
647
+ storagePath: '/tmp/file3.txt',
648
+ createdAt: Math.floor(Date.now() / 1000),
649
+ text: 'c'.repeat(200), // ~50 tokens
650
+ purpose: 'input',
651
+ userMention: false,
652
+ rankScore: 0,
653
+ },
654
+ ];
655
+
656
+ const policy: AttachmentPolicy = {
657
+ maxTokensTotal: 100, // Only room for 2 attachments
658
+ selectionStrategy: {
659
+ rankBy: ['purpose', 'user_mention', 'recency'],
660
+ purposePriority: {
661
+ evidence: 1,
662
+ input: 2,
663
+ context: 3,
664
+ artifact: 4,
665
+ },
666
+ },
667
+ };
668
+
669
+ const selector = new AttachmentSelector(policy, {} as any);
670
+ const result = await selector.selectAttachments(attachments);
671
+
672
+ // Verify only 2 attachments selected (within budget)
673
+ expect(result.selected.length).toBe(2);
674
+ expect(result.tokensUsed).toBeLessThanOrEqual(100);
675
+
676
+ // Verify highest priority attachments selected
677
+ // evidence + input should be selected (user_mention breaks tie)
678
+ const selectedFilenames = result.selected.map((a) => a.filename);
679
+ expect(selectedFilenames).toContain('file1.txt'); // evidence + user mention
680
+ expect(selectedFilenames).toContain('file3.txt'); // input
681
+
682
+ // Verify excluded
683
+ expect(result.excluded.length).toBe(1);
684
+ expect(result.excluded[0].attachmentId).toBe('att2');
685
+ });
686
+
687
+ it('should respect maxTokensTotal strictly', async () => {
688
+ const largeAttachments: RankedAttachment[] = [
689
+ {
690
+ attachmentId: 'large1',
691
+ filename: 'large1.txt',
692
+ mimeType: 'text/plain',
693
+ sizeBytes: 1000,
694
+ storage: 'local',
695
+ storagePath: '/tmp/large1.txt',
696
+ createdAt: Math.floor(Date.now() / 1000),
697
+ text: 'x'.repeat(1000), // ~250 tokens (1000 chars / 4)
698
+ purpose: 'evidence',
699
+ userMention: true,
700
+ rankScore: 0,
701
+ },
702
+ {
703
+ attachmentId: 'large2',
704
+ filename: 'large2.txt',
705
+ mimeType: 'text/plain',
706
+ sizeBytes: 1000,
707
+ storage: 'local',
708
+ storagePath: '/tmp/large2.txt',
709
+ createdAt: Math.floor(Date.now() / 1000),
710
+ text: 'y'.repeat(1000), // ~250 tokens (1000 chars / 4)
711
+ purpose: 'evidence',
712
+ userMention: false,
713
+ rankScore: 0,
714
+ },
715
+ ];
716
+
717
+ const policy: AttachmentPolicy = {
718
+ maxTokensTotal: 300, // Only room for 1 large attachment (250 tokens)
719
+ selectionStrategy: {
720
+ rankBy: ['user_mention'],
721
+ },
722
+ };
723
+
724
+ const selector = new AttachmentSelector(policy, {} as any);
725
+ const result = await selector.selectAttachments(largeAttachments);
726
+
727
+ // Verify only 1 selected
728
+ expect(result.selected.length).toBe(1);
729
+ expect(result.tokensUsed).toBeLessThanOrEqual(300);
730
+
731
+ // Verify user-mentioned one is selected
732
+ expect(result.selected[0].filename).toBe('large1.txt');
733
+ expect(result.excluded.length).toBe(1);
734
+ });
735
+ });
736
+
737
+ describe('End-to-end pipeline', () => {
738
+ it('should handle complete build → fork → compile workflow', async () => {
739
+ // Build context
740
+ const builder = new ContextBuilder();
741
+
742
+ // Create a test codec with validate method
743
+ const testCodec: BlockCodec<any> = {
744
+ codecId: 'test',
745
+ version: '1.0.0',
746
+ payloadSchema: z.object({ data: z.string() }),
747
+ canonicalize: (p: any) => p,
748
+ hash: () => 'test-hash',
749
+ render: (block: any) => ({
750
+ anthropic: { role: 'user', content: JSON.stringify(block.payload) },
751
+ openai: {},
752
+ gemini: {},
753
+ }),
754
+ validate: (p: any) => p,
755
+ };
756
+
757
+ builder
758
+ .system({ text: 'You are a helpful assistant.' })
759
+ .memory(
760
+ testCodec,
761
+ { data: 'Internal knowledge' },
762
+ { sensitivity: 'internal' }
763
+ )
764
+ .history([
765
+ { role: 'user', content: 'Hello' },
766
+ { role: 'assistant', content: 'Hi!' },
767
+ ])
768
+ .turn('Summarize our conversation');
769
+
770
+ // Create view
771
+ const view = await builder.getGraph().createView({});
772
+ expect(view.blocks.length).toBeGreaterThan(0);
773
+
774
+ // Fork with public sensitivity
775
+ const fork = new ContextFork(builder.getGraph(), view);
776
+ const forkedView = await fork.createFork({
777
+ agentId: 'summarizer',
778
+ name: 'Conversation Summarizer',
779
+ model: { provider: 'anthropic', model: 'claude-haiku-4-5' },
780
+ maxSensitivity: 'public',
781
+ includeHistory: true,
782
+ includeState: false,
783
+ });
784
+
785
+ // Verify internal blocks are redacted
786
+ const redactedBlocks = forkedView.blocks.filter(
787
+ (b) => b.meta.codecId === RedactedStubCodec.codecId
788
+ );
789
+ expect(redactedBlocks.length).toBeGreaterThan(0);
790
+
791
+ // Verify history is included
792
+ const historyBlocks = forkedView.blocks.filter((b) => b.meta.kind === 'history');
793
+ expect(historyBlocks.length).toBeGreaterThan(0);
794
+
795
+ // Compile for Anthropic
796
+ const policy: ContextPolicy = {
797
+ provider: 'anthropic',
798
+ modelId: 'claude-haiku-4-5',
799
+ contextWindow: 200000,
800
+ completionReserve: 4000,
801
+ overflowStrategy: 'truncate',
802
+ kindPriorities: [],
803
+ sensitivity: {
804
+ maxSensitivity: 'public',
805
+ redactRestricted: true,
806
+ },
807
+ };
808
+
809
+ const codecRegistry = new Map();
810
+ codecRegistry.set('system-rules', SystemRulesCodec);
811
+ codecRegistry.set('redacted-stub', RedactedStubCodec);
812
+ codecRegistry.set('conversation-history', {
813
+ codecId: 'conversation-history',
814
+ version: '1.0.0',
815
+ payloadSchema: {},
816
+ canonicalize: (p: any) => p,
817
+ hash: () => 'history-hash',
818
+ render: (block: any) => ({
819
+ anthropic: block.payload.messages.map((m: any) => ({
820
+ role: m.role,
821
+ content: m.content,
822
+ })),
823
+ openai: {},
824
+ gemini: {},
825
+ }),
826
+ validate: (p: any) => p,
827
+ });
828
+ codecRegistry.set('user-turn', {
829
+ codecId: 'user-turn',
830
+ version: '1.0.0',
831
+ payloadSchema: {},
832
+ canonicalize: (p: any) => p,
833
+ hash: () => 'turn-hash',
834
+ render: (block: any) => ({
835
+ anthropic: { role: 'user', content: block.payload.text },
836
+ openai: {},
837
+ gemini: {},
838
+ }),
839
+ validate: (p: any) => p,
840
+ });
841
+ codecRegistry.set('test', {
842
+ codecId: 'test',
843
+ version: '1.0.0',
844
+ payloadSchema: {},
845
+ canonicalize: (p: any) => p,
846
+ hash: () => 'test-hash',
847
+ render: (block: any) => ({
848
+ anthropic: { role: 'user', content: JSON.stringify(block.payload) },
849
+ openai: {},
850
+ gemini: {},
851
+ }),
852
+ validate: (p: any) => p,
853
+ });
854
+
855
+ const compiled = compileAnthropicContext([...forkedView.blocks], policy, {
856
+ codecRegistry,
857
+ });
858
+
859
+ // Verify compiled output
860
+ expect(compiled.provider).toBe('anthropic');
861
+ expect(compiled.modelId).toBe('claude-haiku-4-5');
862
+ expect(compiled.messages.length).toBeGreaterThan(0);
863
+ expect(compiled.system).toBeDefined();
864
+ });
865
+ });
866
+ });