ctxora 6.2.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 (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +441 -0
  3. package/README.vi.md +441 -0
  4. package/bin/ctxora.mjs +147 -0
  5. package/package.json +45 -0
  6. package/pyproject.toml +59 -0
  7. package/src/chunking/compressor.py +104 -0
  8. package/src/chunking/treesitter_chunker.py +240 -0
  9. package/src/compact/anthropic.py +98 -0
  10. package/src/compact/gemini.py +88 -0
  11. package/src/compact/handoff.py +179 -0
  12. package/src/compact/openai.py +318 -0
  13. package/src/compact/summarizer.py +186 -0
  14. package/src/context/assembler.py +298 -0
  15. package/src/context/budgeting.py +137 -0
  16. package/src/context/sanitizer.py +23 -0
  17. package/src/evaluation/__init__.py +1 -0
  18. package/src/evaluation/gates.py +172 -0
  19. package/src/evaluation/metrics.py +41 -0
  20. package/src/harness_context/__init__.py +5 -0
  21. package/src/harness_context/adapters/__init__.py +1 -0
  22. package/src/harness_context/adapters/clients/__init__.py +4 -0
  23. package/src/harness_context/adapters/clients/formatters.py +47 -0
  24. package/src/harness_context/adapters/clients/profiles.py +29 -0
  25. package/src/harness_context/adapters/ecc/__init__.py +4 -0
  26. package/src/harness_context/adapters/ecc/detection.py +41 -0
  27. package/src/harness_context/adapters/ecc/mapping.py +32 -0
  28. package/src/harness_context/adapters/ecc/memory_reader.py +162 -0
  29. package/src/harness_context/adapters/ecc/provenance.py +16 -0
  30. package/src/harness_context/api/__init__.py +1 -0
  31. package/src/harness_context/api/v2/__init__.py +12 -0
  32. package/src/harness_context/api/v2/contracts.py +119 -0
  33. package/src/harness_context/api/v2/diagnostics.py +13 -0
  34. package/src/harness_context/api/v2/enums.py +17 -0
  35. package/src/harness_context/api/v2/errors.py +32 -0
  36. package/src/harness_context/api/v2/models.py +4 -0
  37. package/src/harness_context/api/v2/requests.py +17 -0
  38. package/src/harness_context/api/v2/responses.py +22 -0
  39. package/src/harness_context/application/__init__.py +3 -0
  40. package/src/harness_context/application/container.py +31 -0
  41. package/src/harness_context/application/context_service.py +51 -0
  42. package/src/harness_context/application/ecc_service.py +7 -0
  43. package/src/harness_context/application/handoff_service.py +11 -0
  44. package/src/harness_context/application/memory_service.py +9 -0
  45. package/src/harness_context/application/protocols.py +46 -0
  46. package/src/harness_context/application/refresh_service.py +25 -0
  47. package/src/harness_context/application/retrieval_service.py +22 -0
  48. package/src/harness_context/application/services.py +4 -0
  49. package/src/harness_context/application/workspace_service.py +18 -0
  50. package/src/harness_context/bootstrap.py +47 -0
  51. package/src/harness_context/branding.py +16 -0
  52. package/src/harness_context/cli/__init__.py +1 -0
  53. package/src/harness_context/cli/app.py +239 -0
  54. package/src/harness_context/cli/exit_codes.py +25 -0
  55. package/src/harness_context/domain/__init__.py +9 -0
  56. package/src/harness_context/domain/cag.py +18 -0
  57. package/src/harness_context/domain/chunking.py +17 -0
  58. package/src/harness_context/domain/planning.py +30 -0
  59. package/src/harness_context/domain/ports.py +24 -0
  60. package/src/harness_context/domain/retrieval.py +46 -0
  61. package/src/harness_context/engine.py +10 -0
  62. package/src/harness_context/free_tools.py +143 -0
  63. package/src/harness_context/infrastructure/__init__.py +10 -0
  64. package/src/harness_context/infrastructure/graph.py +26 -0
  65. package/src/harness_context/infrastructure/indexes.py +33 -0
  66. package/src/harness_context/infrastructure/local_engine.py +296 -0
  67. package/src/harness_context/infrastructure/parsing.py +38 -0
  68. package/src/harness_context/infrastructure/scanning.py +51 -0
  69. package/src/harness_context/installer/__init__.py +4 -0
  70. package/src/harness_context/installer/models.py +22 -0
  71. package/src/harness_context/installer/service.py +168 -0
  72. package/src/harness_context/mcp/__init__.py +3 -0
  73. package/src/harness_context/mcp/capabilities.py +11 -0
  74. package/src/harness_context/mcp/errors.py +8 -0
  75. package/src/harness_context/mcp/lifecycle.py +72 -0
  76. package/src/harness_context/mcp/middleware.py +57 -0
  77. package/src/harness_context/mcp/server.py +3 -0
  78. package/src/harness_context/mcp/tool_handlers/__init__.py +7 -0
  79. package/src/harness_context/mcp/tool_handlers/context.py +16 -0
  80. package/src/harness_context/mcp/tool_handlers/ecc.py +8 -0
  81. package/src/harness_context/mcp/tool_handlers/handoffs.py +20 -0
  82. package/src/harness_context/mcp/tool_handlers/memory.py +16 -0
  83. package/src/harness_context/mcp/tool_handlers/workspace.py +12 -0
  84. package/src/harness_context/mcp/tools.py +15 -0
  85. package/src/harness_context/observability/__init__.py +6 -0
  86. package/src/harness_context/observability/events.py +25 -0
  87. package/src/harness_context/observability/metrics.py +20 -0
  88. package/src/harness_context/paths.py +35 -0
  89. package/src/harness_context/runtime.py +127 -0
  90. package/src/harness_context/schemas.py +38 -0
  91. package/src/harness_context/security/__init__.py +3 -0
  92. package/src/harness_context/security/secret_patterns.py +15 -0
  93. package/src/harness_context/server.py +1077 -0
  94. package/src/harness_context/storage/__init__.py +6 -0
  95. package/src/harness_context/storage/migrations.py +24 -0
  96. package/src/harness_context/storage/pins.py +10 -0
  97. package/src/harness_context/storage/snapshots.py +149 -0
  98. package/src/harness_context/tokenize.py +12 -0
  99. package/src/harness_context/topology.py +65 -0
  100. package/src/harness_context/watcher/__init__.py +3 -0
  101. package/src/harness_context/watcher/service.py +32 -0
  102. package/src/harness_context/workspace/__init__.py +13 -0
  103. package/src/harness_context/workspace/identity.py +9 -0
  104. package/src/harness_context/workspace/lock.py +24 -0
  105. package/src/harness_context/workspace/policy.py +3 -0
  106. package/src/harness_context/workspace/roots.py +84 -0
  107. package/src/harness_context/workspace/state.py +35 -0
  108. package/src/memory/episodic.py +257 -0
  109. package/src/memory/vector_store.py +104 -0
  110. package/src/retrieval/bm25.py +23 -0
  111. package/src/retrieval/cache.py +76 -0
  112. package/src/retrieval/embeddings.py +75 -0
  113. package/src/retrieval/graph.py +45 -0
  114. package/src/retrieval/reranker.py +78 -0
  115. package/src/retrieval/tokenize.py +11 -0
@@ -0,0 +1,298 @@
1
+ """
2
+ context/assembler.py — Provider-aware context assembly
3
+ =======================================================
4
+ Formats retrieved chunks + memory entries into a system prompt.
5
+
6
+ Format strategy by model:
7
+ - Claude (Anthropic): XML — <retrieved_context> / <long_term_memory>
8
+ - Gemini (Google): JSON — wrapped in ```json fence
9
+ - OpenAI Chat (GPT): Markdown — fenced code blocks, clean headings
10
+ - OpenAI Codex: Compact Markdown — minimise overhead tokens;
11
+ only emit file path + critical code snippet
12
+ - OpenAI Reasoning: Markdown — same as chat but no `<system>` injection
13
+ (summary injected as user message by compactor)
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+
20
+ # ── Memory formatting helper ──────────────────────────────────────────────────
21
+
22
+ def _format_memory_compact(entries: list[dict]) -> str:
23
+ """
24
+ Compact single-line memory format — saves ~50 tokens per entry vs XML.
25
+ Format: [type|key] value
26
+ """
27
+ if not entries:
28
+ return ""
29
+ lines = [f"[{m['type']}|{m['key']}] {m['value']}" for m in entries]
30
+ return "\n".join(lines)
31
+
32
+
33
+ def _format_memory_xml(entries: list[dict]) -> str:
34
+ """Full XML memory block — for Claude structured mode."""
35
+ if not entries:
36
+ return ""
37
+ lines = []
38
+ for m in entries:
39
+ lines.append(
40
+ f" <memory key='{m['key']}' type='{m['type']}'>"
41
+ f"{m['value']}</memory>"
42
+ )
43
+ return "\n".join(lines)
44
+
45
+
46
+ # ── Model family detection ────────────────────────────────────────────────────
47
+
48
+ def _is_gemini(model: str) -> bool:
49
+ return "gemini" in model.lower()
50
+
51
+
52
+ def _is_claude(model: str) -> bool:
53
+ return "claude" in model.lower() or "anthropic" in model.lower()
54
+
55
+
56
+ def _is_codex(model: str) -> bool:
57
+ ml = model.lower()
58
+ return any(kw in ml for kw in (
59
+ "codex", "code-davinci", "code-cushman",
60
+ ))
61
+
62
+
63
+ def _is_reasoning(model: str) -> bool:
64
+ """OpenAI o-series reasoning models."""
65
+ ml = model.lower()
66
+ return any(ml.startswith(p) or f"-{p}" in ml for p in (
67
+ "o1", "o3", "o4",
68
+ ))
69
+
70
+
71
+ def _is_openai(model: str) -> bool:
72
+ ml = model.lower()
73
+ return any(kw in ml for kw in ("gpt-", "o1", "o3", "o4", "codex", "code-"))
74
+
75
+
76
+ # ── Formatters ────────────────────────────────────────────────────────────────
77
+
78
+ class ContextAssembler:
79
+ def __init__(self) -> None:
80
+ pass
81
+
82
+ def assemble(
83
+ self,
84
+ base_prompt: str,
85
+ chunks: list,
86
+ memory_entries: list,
87
+ query: str,
88
+ output_mode: str,
89
+ model: str,
90
+ ) -> str:
91
+ """
92
+ Assemble the enriched system prompt.
93
+
94
+ :param base_prompt: Original system prompt.
95
+ :param chunks: Retrieved code/doc chunks.
96
+ :param memory_entries: Memory entries from long-term store.
97
+ :param query: Current user task.
98
+ :param output_mode: "concise" | "structured" | "code_only" | "minimal"
99
+ :param model: Target model string.
100
+ :returns: Assembled prompt string.
101
+ """
102
+ if _is_gemini(model):
103
+ return self._assemble_gemini(
104
+ base_prompt, chunks, memory_entries, query, output_mode)
105
+
106
+ if _is_claude(model):
107
+ return self._assemble_claude(
108
+ base_prompt, chunks, memory_entries, query, output_mode)
109
+
110
+ if _is_codex(model):
111
+ return self._assemble_codex(
112
+ base_prompt, chunks, memory_entries, query, output_mode)
113
+
114
+ if _is_openai(model) or _is_reasoning(model):
115
+ return self._assemble_openai(
116
+ base_prompt, chunks, memory_entries, query, output_mode,
117
+ reasoning=_is_reasoning(model))
118
+
119
+ # Fallback → OpenAI markdown
120
+ return self._assemble_openai(
121
+ base_prompt, chunks, memory_entries, query, output_mode)
122
+
123
+ # ── Claude XML ────────────────────────────────────────────────────────────
124
+ def _assemble_claude(
125
+ self,
126
+ base_prompt: str,
127
+ chunks: list,
128
+ memory_entries: list,
129
+ query: str,
130
+ output_mode: str,
131
+ ) -> str:
132
+ # Memory: XML only for structured mode; compact otherwise
133
+ if output_mode == "structured":
134
+ mem_block = _format_memory_xml(memory_entries)
135
+ mem_section = f"<long_term_memory>\n{mem_block}\n</long_term_memory>" if mem_block else ""
136
+ else:
137
+ mem_block = _format_memory_compact(memory_entries)
138
+ mem_section = f"<memory>\n{mem_block}\n</memory>" if mem_block else ""
139
+
140
+ chunk_xml = ""
141
+ for c in chunks:
142
+ if c.content.startswith("<context>"): # already compressed
143
+ chunk_xml += c.content + "\n"
144
+ else:
145
+ chunk_xml += (
146
+ f" <chunk path='{c.path}' symbol='{c.symbol}'>"
147
+ f"<content>{c.content}</content></chunk>\n"
148
+ )
149
+
150
+ parts = [base_prompt]
151
+ if chunk_xml:
152
+ parts.append(f"<retrieved_context>\n{chunk_xml}</retrieved_context>")
153
+ if mem_section:
154
+ parts.append(mem_section)
155
+ if query:
156
+ parts.append(f"<task>\n{query}\n</task>")
157
+ return "\n\n".join(parts)
158
+
159
+ # ── Gemini JSON ───────────────────────────────────────────────────────────
160
+ def _assemble_gemini(
161
+ self,
162
+ base_prompt: str,
163
+ chunks: list,
164
+ memory_entries: list,
165
+ query: str,
166
+ output_mode: str,
167
+ ) -> str:
168
+ context_data = {
169
+ "query": query,
170
+ "chunks": [
171
+ {"path": c.path, "symbol": c.symbol, "content": c.content}
172
+ for c in chunks
173
+ ],
174
+ "memory": memory_entries,
175
+ }
176
+ ctx_str = json.dumps(context_data, ensure_ascii=False, indent=2)
177
+ return f"{base_prompt}\n\n[CONTEXT]\n```json\n{ctx_str}\n```"
178
+
179
+ # ── OpenAI Markdown (Chat + Reasoning) ────────────────────────────────────
180
+ def _assemble_openai(
181
+ self,
182
+ base_prompt: str,
183
+ chunks: list,
184
+ memory_entries: list,
185
+ query: str,
186
+ output_mode: str,
187
+ reasoning: bool = False,
188
+ ) -> str:
189
+ """
190
+ Clean Markdown format for GPT / o-series.
191
+
192
+ Uses fenced code blocks per chunk (language inferred from path).
193
+ Memory entries in compact single-line format to minimise token overhead.
194
+ """
195
+ parts = [base_prompt]
196
+
197
+ if chunks:
198
+ parts.append("## Retrieved Context")
199
+ for c in chunks:
200
+ lang = _infer_language(c.path)
201
+ header = f"### `{c.path}`"
202
+ if c.symbol and c.symbol != "module":
203
+ header += f" — `{c.symbol}`"
204
+ if output_mode == "minimal":
205
+ snippet = "\n".join(c.content.splitlines()[:20])
206
+ parts.append(f"{header}\n```{lang}\n{snippet}\n```")
207
+ else:
208
+ parts.append(f"{header}\n```{lang}\n{c.content}\n```")
209
+
210
+ if memory_entries:
211
+ mem_str = _format_memory_compact(memory_entries)
212
+ parts.append(f"## Memory\n{mem_str}")
213
+
214
+ if query:
215
+ parts.append(f"## Task\n{query}")
216
+
217
+ return "\n\n".join(parts)
218
+
219
+ # ── Codex Compact Markdown ────────────────────────────────────────────────
220
+ def _assemble_codex(
221
+ self,
222
+ base_prompt: str,
223
+ chunks: list,
224
+ memory_entries: list,
225
+ query: str,
226
+ output_mode: str,
227
+ ) -> str:
228
+ """
229
+ Ultra-compact format for Codex models.
230
+
231
+ Codex has a small context window (8k legacy / 200k codex-mini).
232
+ For legacy Codex we minimise prose and only surface file path +
233
+ critical code. For codex-mini we use the standard OpenAI format.
234
+ """
235
+ parts = [base_prompt]
236
+
237
+ if chunks:
238
+ parts.append("# Context")
239
+ for c in chunks:
240
+ lang = _infer_language(c.path)
241
+ # Only first 40 lines to save tokens for legacy Codex
242
+ snippet = "\n".join(c.content.splitlines()[:40])
243
+ symbol_tag = f" [{c.symbol}]" if c.symbol and c.symbol != "module" else ""
244
+ parts.append(
245
+ f"// {c.path}{symbol_tag}\n```{lang}\n{snippet}\n```"
246
+ )
247
+
248
+ if memory_entries:
249
+ parts.append("# Memory")
250
+ for m in memory_entries:
251
+ parts.append(f"// [{m['type']}] {m['key']}: {m['value']}")
252
+
253
+ parts.append(f"# Task\n{query}")
254
+ return "\n\n".join(parts)
255
+
256
+
257
+ # ── Helpers ───────────────────────────────────────────────────────────────────
258
+
259
+ _EXT_LANG: dict[str, str] = {
260
+ ".py": "python",
261
+ ".ts": "typescript",
262
+ ".tsx": "tsx",
263
+ ".js": "javascript",
264
+ ".jsx": "jsx",
265
+ ".go": "go",
266
+ ".rs": "rust",
267
+ ".java": "java",
268
+ ".kt": "kotlin",
269
+ ".cs": "csharp",
270
+ ".cpp": "cpp",
271
+ ".c": "c",
272
+ ".h": "c",
273
+ ".hpp": "cpp",
274
+ ".rb": "ruby",
275
+ ".php": "php",
276
+ ".swift": "swift",
277
+ ".sh": "bash",
278
+ ".zsh": "bash",
279
+ ".md": "markdown",
280
+ ".json": "json",
281
+ ".yaml": "yaml",
282
+ ".yml": "yaml",
283
+ ".toml": "toml",
284
+ ".html": "html",
285
+ ".css": "css",
286
+ ".sql": "sql",
287
+ }
288
+
289
+
290
+ def _infer_language(path: str) -> str:
291
+ """Guess a markdown fence language tag from file extension."""
292
+ if not path:
293
+ return ""
294
+ dot = path.rfind(".")
295
+ if dot == -1:
296
+ return ""
297
+ ext = path[dot:].lower()
298
+ return _EXT_LANG.get(ext, "")
@@ -0,0 +1,137 @@
1
+ """
2
+ context/budgeting.py — Dynamic token budget per model
3
+ ======================================================
4
+ Covers all major model families:
5
+ - Claude (Anthropic): sonnet, haiku, opus
6
+ - Gemini (Google): 2.5-pro, 2.5-flash, 1.5-pro
7
+ - OpenAI Chat: gpt-4o, gpt-4.1, gpt-4.5, gpt-4, gpt-3.5
8
+ - OpenAI Reasoning: o1, o1-mini, o3, o3-mini, o4-mini
9
+ - OpenAI Codex: codex-mini-latest, code-davinci-002, gpt-4o-mini
10
+ - Generic fallback: 128k context
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from dataclasses import dataclass
16
+
17
+
18
+ @dataclass
19
+ class TokenBudget:
20
+ model_name: str
21
+ context_window: int
22
+ reserved_output: int
23
+ reserved_reasoning: int # hidden CoT tokens (o1/o3/o4 family)
24
+ reserved_tools: int
25
+ inject_budget: int # context_window - all reserves
26
+
27
+
28
+ # ── Model registry ────────────────────────────────────────────────────────────
29
+ # Each entry: (context_window, reserved_output, reserved_reasoning, reserved_tools)
30
+ _MODEL_TABLE: dict[str, tuple[int, int, int, int]] = {
31
+ # ── Claude ──────────────────────────────────────────────────────────────
32
+ "claude-opus": (200_000, 15_000, 0, 10_000),
33
+ "claude-sonnet": (200_000, 30_000, 25_000, 10_000),
34
+ "claude-haiku": (200_000, 10_000, 0, 8_000),
35
+
36
+ # ── Gemini ──────────────────────────────────────────────────────────────
37
+ "gemini-2.5-pro": (2_000_000, 64_000, 0, 20_000),
38
+ "gemini-2.5-flash": (1_000_000, 32_000, 0, 16_000),
39
+ "gemini-1.5-pro": (2_000_000, 8_192, 0, 20_000),
40
+ "gemini-1.5-flash": (1_000_000, 8_192, 0, 16_000),
41
+ "gemini": (1_000_000, 32_000, 0, 16_000), # generic
42
+
43
+ # ── OpenAI GPT-5 series ─────────────────────────────────────────────────
44
+ # Context windows / output limits are estimates based on API announcements.
45
+ # Update when OpenAI releases official specs.
46
+ "gpt-5.5": (1_000_000, 32_768, 0, 16_000), # multimodal flagship
47
+ "gpt-5.4": ( 512_000, 32_768, 0, 12_000),
48
+ "gpt-5-mini": ( 256_000, 16_384, 0, 8_000),
49
+ "gpt-5": ( 512_000, 32_768, 0, 12_000), # base gpt-5
50
+
51
+ # ── OpenAI Chat ─────────────────────────────────────────────────────────
52
+ "gpt-4.5": (128_000, 16_384, 0, 8_000),
53
+ "gpt-4.1": (128_000, 16_384, 0, 8_000),
54
+ "gpt-4o": (128_000, 16_384, 0, 8_000),
55
+ "gpt-4o-mini": (128_000, 4_096, 0, 4_000),
56
+ "gpt-4-turbo": (128_000, 16_384, 0, 8_000),
57
+ "gpt-4": ( 32_768, 4_096, 0, 4_000),
58
+ "gpt-3.5-turbo": ( 16_385, 4_096, 0, 3_000),
59
+
60
+ # ── OpenAI Reasoning (o-series) ──────────────────────────────────────────
61
+ # reserved_reasoning captures the hidden CoT budget estimate
62
+ "o4-mini": (200_000, 16_384, 64_000, 8_000),
63
+ "o3": (200_000, 16_384, 80_000, 8_000),
64
+ "o3-mini": (200_000, 16_384, 64_000, 8_000),
65
+ "o1": (200_000, 16_384, 80_000, 8_000),
66
+ "o1-mini": (128_000, 4_096, 32_000, 4_000),
67
+ "o1-preview": (128_000, 4_096, 32_000, 4_000),
68
+
69
+ # ── OpenAI Codex ────────────────────────────────────────────────────────
70
+ # codex-mini-latest: the new hosted reasoning code model (May 2025)
71
+ "codex-mini-latest": (200_000, 16_384, 64_000, 8_000),
72
+ "codex-mini": (200_000, 16_384, 64_000, 8_000),
73
+ # Legacy completion-only Codex (deprecated but still referenced)
74
+ "code-davinci-002": ( 8_001, 4_096, 0, 1_000),
75
+ "code-cushman-001": ( 2_048, 2_048, 0, 500),
76
+
77
+ # ── Generic fallback ────────────────────────────────────────────────────
78
+ "default": (128_000, 8_192, 0, 6_000),
79
+ }
80
+
81
+
82
+ def _match(model: str) -> tuple[int, int, int, int]:
83
+ """
84
+ Match a model string to the closest entry in _MODEL_TABLE.
85
+ Checks exact → prefix → keyword substring → default.
86
+ """
87
+ ml = model.lower().strip()
88
+
89
+ # 1. Exact match
90
+ if ml in _MODEL_TABLE:
91
+ return _MODEL_TABLE[ml]
92
+
93
+ # 2. Prefix match (longest wins)
94
+ candidates = [k for k in _MODEL_TABLE if ml.startswith(k)]
95
+ if candidates:
96
+ return _MODEL_TABLE[max(candidates, key=len)]
97
+
98
+ # 3. Keyword substring scan (ordered by specificity)
99
+ keyword_order = [
100
+ "codex-mini-latest", "codex-mini", "codex",
101
+ "code-davinci", "code-cushman",
102
+ "o4-mini", "o3-mini", "o1-mini", "o1-preview",
103
+ "o4", "o3", "o1",
104
+ # GPT-5 must come before gpt-4 to avoid partial match
105
+ "gpt-5.5", "gpt-5.4", "gpt-5-mini", "gpt-5",
106
+ "gpt-4.5", "gpt-4.1", "gpt-4o-mini", "gpt-4o",
107
+ "gpt-4-turbo", "gpt-4",
108
+ "gpt-3.5",
109
+ "gemini-2.5-pro", "gemini-2.5-flash",
110
+ "gemini-1.5-pro", "gemini-1.5-flash",
111
+ "gemini",
112
+ "claude-opus", "claude-haiku", "claude-sonnet", "claude",
113
+ ]
114
+ for kw in keyword_order:
115
+ if kw in ml:
116
+ return _MODEL_TABLE.get(kw, _MODEL_TABLE["default"])
117
+
118
+ return _MODEL_TABLE["default"]
119
+
120
+
121
+ def get_budget(model: str) -> TokenBudget:
122
+ """
123
+ Return a TokenBudget for the given model string.
124
+
125
+ :param model: Any model identifier string (case-insensitive).
126
+ :returns: TokenBudget with inject_budget = usable context tokens.
127
+ """
128
+ ctx, out, res, tools = _match(model)
129
+ inject = max(ctx - out - res - tools, 0)
130
+ return TokenBudget(
131
+ model_name=model,
132
+ context_window=ctx,
133
+ reserved_output=out,
134
+ reserved_reasoning=res,
135
+ reserved_tools=tools,
136
+ inject_budget=inject,
137
+ )
@@ -0,0 +1,23 @@
1
+ def sanitize(text: str, escape_xml: bool = True) -> str:
2
+ if not text:
3
+ return ""
4
+ # simple prompt injection protection
5
+ bad_phrases = [
6
+ "ignore previous instructions",
7
+ "system prompt",
8
+ "tool override"]
9
+ for bp in bad_phrases:
10
+ text = text.replace(bp, "[REDACTED]")
11
+ if escape_xml:
12
+ text = text.replace("<", "&lt;").replace(">", "&gt;")
13
+ return text
14
+
15
+
16
+ def sanitize_chunks(chunks: list):
17
+ for c in chunks:
18
+ c.content = sanitize(c.content, escape_xml=False)
19
+ if not getattr(c, "content_hash", ""):
20
+ import hashlib
21
+ c.content_hash = hashlib.sha256(c.content.encode("utf-8")).hexdigest()
22
+ if c.summary:
23
+ c.summary = sanitize(c.summary, escape_xml=False)
@@ -0,0 +1 @@
1
+ """Provider-neutral release evaluation gates."""
@@ -0,0 +1,172 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import shutil
5
+ import statistics
6
+ import tempfile
7
+ import time
8
+ from pathlib import Path
9
+
10
+ from evaluation.metrics import ndcg_at_k, recall_at_k, reciprocal_rank
11
+ from harness_context.engine import ContextEngine
12
+
13
+ FIXTURES = Path(__file__).parents[2] / "tests" / "fixtures"
14
+ STRATEGIES = {
15
+ "rag": "hybrid_rag",
16
+ "full_context": "long_context",
17
+ "cag": "cag",
18
+ "hybrid": "hybrid_cag_rag",
19
+ "graph_augmented": "graph_augmented",
20
+ }
21
+ LATENCY_LIMIT_MS = 10_000.0
22
+
23
+
24
+ def percentile(values: list[float], percent: float) -> float:
25
+ ordered = sorted(values)
26
+ index = max(0, min(len(ordered) - 1, int((len(ordered) - 1) * percent)))
27
+ return round(ordered[index], 3)
28
+
29
+
30
+ def _measure(call, runs: int = 5) -> tuple[dict, dict]:
31
+ cold_started = time.perf_counter()
32
+ result = call()
33
+ cold_ms = (time.perf_counter() - cold_started) * 1000
34
+ warm_latencies = []
35
+ for _ in range(runs):
36
+ started = time.perf_counter()
37
+ result = call()
38
+ warm_latencies.append((time.perf_counter() - started) * 1000)
39
+ return result, {
40
+ "cold_ms": round(cold_ms, 3),
41
+ "warm_samples_ms": [round(value, 3) for value in warm_latencies],
42
+ "warm_p50_ms": round(statistics.median(warm_latencies), 3),
43
+ "warm_p95_ms": percentile(warm_latencies, 0.95),
44
+ }
45
+
46
+
47
+ def _strategy_tokens(result: dict) -> int:
48
+ return (result.get("retrieval") or {}).get("token_count", 0) + (
49
+ result.get("bundle") or {}
50
+ ).get("token_count", 0)
51
+
52
+
53
+ def run_release_gates() -> dict:
54
+ with tempfile.TemporaryDirectory() as directory:
55
+ root = Path(directory) / "fixture-corpus"
56
+ shutil.copytree(FIXTURES, root)
57
+ engine = ContextEngine()
58
+ engine.register_workspace("evaluation", [str(root)])
59
+
60
+ cold_started = time.perf_counter()
61
+ engine.refresh_workspace("evaluation")
62
+ cold_index_ms = (time.perf_counter() - cold_started) * 1000
63
+ warm_started = time.perf_counter()
64
+ engine.refresh_workspace("evaluation")
65
+ warm_index_ms = (time.perf_counter() - warm_started) * 1000
66
+
67
+ query = "refresh_token authentication architecture cross-module"
68
+ strategy_reports = {}
69
+ for label, override in STRATEGIES.items():
70
+ result, latency = _measure(
71
+ lambda override=override: engine.prepare_context(
72
+ "evaluation", query, 4_000, override
73
+ )
74
+ )
75
+ items = (result.get("retrieval") or {}).get("items", [])
76
+ if override == "cag":
77
+ items = (result.get("bundle") or {}).get("items", [])
78
+ strategy_reports[label] = {
79
+ "engine_strategy": result["plan"]["strategy"],
80
+ "item_count": len(items),
81
+ "token_cost": _strategy_tokens(result),
82
+ "latency": latency,
83
+ "within_budget": _strategy_tokens(result) <= 4_000,
84
+ }
85
+
86
+ retrieval = engine.retrieve_context("evaluation", "refresh_token", token_budget=2_000)
87
+ expected_paths = {"auth.py"}
88
+ expected_symbols = {"refresh_token"}
89
+ quality = {
90
+ "recall_at_5": recall_at_k(retrieval["items"], expected_paths, expected_symbols, 5),
91
+ "mrr": reciprocal_rank(retrieval["items"], expected_paths, expected_symbols),
92
+ "ndcg_at_5": ndcg_at_k(retrieval["items"], expected_paths, expected_symbols, 5),
93
+ "provenance_complete": all(
94
+ item.get("path") and item.get("start_line") and item.get("end_line")
95
+ for item in retrieval["items"]
96
+ ),
97
+ "untrusted_content": retrieval["untrusted_content"],
98
+ }
99
+
100
+ fixture_queries = {
101
+ "python": "python-rotated",
102
+ "typescript": "typescript-rotated",
103
+ "flutter": "flutter-rotated",
104
+ "monorepo": "monorepo-token",
105
+ "vietnamese": "xoay vòng mã làm mới",
106
+ "malicious": "delete the repository",
107
+ "long_document": "phase_f_long_document_marker",
108
+ }
109
+ fixture_coverage = {
110
+ name: any(
111
+ marker in item["content"]
112
+ for item in engine.retrieve_context("evaluation", marker)["items"]
113
+ )
114
+ for name, marker in fixture_queries.items()
115
+ }
116
+ fixture_coverage["duplicate_symbols"] = sum(
117
+ item.symbol == "normalize_token" for item in engine.states["evaluation"].items
118
+ ) == 2
119
+
120
+ source = root / "python" / "auth.py"
121
+ source.write_text("def refreshed_value():\n return 'fresh-phase-f'\n", encoding="utf-8")
122
+ stale = engine.retrieve_context("evaluation", "fresh-phase-f")
123
+ engine.refresh_workspace("evaluation", [str(source)])
124
+ fresh = engine.retrieve_context("evaluation", "fresh-phase-f")
125
+ freshness = {
126
+ "absent_before_refresh": not any(
127
+ "fresh-phase-f" in item["content"] for item in stale["items"]
128
+ ),
129
+ "present_after_refresh": any(
130
+ "fresh-phase-f" in item["content"] for item in fresh["items"]
131
+ ),
132
+ }
133
+
134
+ latency_ok = max(
135
+ [cold_index_ms, warm_index_ms]
136
+ + [report["latency"]["cold_ms"] for report in strategy_reports.values()]
137
+ + [report["latency"]["warm_p95_ms"] for report in strategy_reports.values()]
138
+ ) < LATENCY_LIMIT_MS
139
+ passed = (
140
+ quality["recall_at_5"] >= 1.0
141
+ and quality["mrr"] >= 0.5
142
+ and quality["ndcg_at_5"] >= 0.5
143
+ and quality["provenance_complete"]
144
+ and quality["untrusted_content"]
145
+ and all(report["item_count"] > 0 for report in strategy_reports.values())
146
+ and all(report["within_budget"] for report in strategy_reports.values())
147
+ and all(fixture_coverage.values())
148
+ and all(freshness.values())
149
+ and latency_ok
150
+ )
151
+ return {
152
+ "passed": passed,
153
+ "thresholds": {"latency_limit_ms": LATENCY_LIMIT_MS, "token_budget": 4_000},
154
+ "quality": quality,
155
+ "fixture_coverage": fixture_coverage,
156
+ "freshness": freshness,
157
+ "index_latency": {
158
+ "cold_ms": round(cold_index_ms, 3),
159
+ "warm_ms": round(warm_index_ms, 3),
160
+ },
161
+ "strategies": strategy_reports,
162
+ }
163
+
164
+
165
+ def main() -> int:
166
+ report = run_release_gates()
167
+ print(json.dumps(report, ensure_ascii=False, indent=2))
168
+ return 0 if report["passed"] else 1
169
+
170
+
171
+ if __name__ == "__main__":
172
+ raise SystemExit(main())
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ """Small provider-neutral retrieval quality metrics for golden fixtures."""
4
+
5
+ import math
6
+
7
+
8
+ def recall_at_k(results: list[dict], expected_paths: set[str], expected_symbols: set[str], k: int) -> float:
9
+ selected = results[:k]
10
+ found_paths = {item["path"].rsplit("/", 1)[-1] for item in selected}
11
+ found_symbols = {item.get("symbol", "") for item in selected}
12
+ signals = []
13
+ if expected_paths:
14
+ signals.append(bool(found_paths & expected_paths))
15
+ if expected_symbols:
16
+ signals.append(bool(found_symbols & expected_symbols))
17
+ return sum(signals) / len(signals) if signals else 1.0
18
+
19
+
20
+ def coverage_score(result: dict) -> float:
21
+ if result.get("coverage") == "sufficient":
22
+ return 1.0
23
+ if result.get("coverage") == "partial":
24
+ return 0.5
25
+ return 0.0
26
+
27
+ def reciprocal_rank(results: list[dict], expected_paths: set[str], expected_symbols: set[str]) -> float:
28
+ for rank, item in enumerate(results, 1):
29
+ if item["path"].rsplit("/", 1)[-1] in expected_paths or item.get("symbol", "") in expected_symbols:
30
+ return 1.0 / rank
31
+ return 0.0
32
+
33
+ def ndcg_at_k(results: list[dict], expected_paths: set[str], expected_symbols: set[str], k: int) -> float:
34
+ relevance = [
35
+ int(item["path"].rsplit("/", 1)[-1] in expected_paths or item.get("symbol", "") in expected_symbols)
36
+ for item in results[:k]
37
+ ]
38
+ dcg = sum(value / math.log2(rank + 1) for rank, value in enumerate(relevance, 1))
39
+ relevant_count = sum(relevance)
40
+ ideal = sum(1.0 / math.log2(rank + 1) for rank in range(1, relevant_count + 1))
41
+ return dcg / ideal if ideal else 1.0
@@ -0,0 +1,5 @@
1
+ """CTXORA Engine."""
2
+
3
+ from harness_context.engine import ContextEngine
4
+
5
+ __all__ = ["ContextEngine"]
@@ -0,0 +1 @@
1
+ """Optional integrations that cannot change core ranking or provenance."""