cortexdbai 0.10.4__tar.gz → 0.11.0__tar.gz

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 (97) hide show
  1. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/.gitignore +7 -0
  2. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/PKG-INFO +2 -2
  3. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/__init__.py +3 -1
  4. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/v1/__init__.py +2 -0
  5. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/v1/client.py +238 -0
  6. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/v1/exceptions.py +21 -0
  7. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/pyproject.toml +1 -1
  8. cortexdbai-0.11.0/tests/test_code_namespace.py +104 -0
  9. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/tests/test_v093_surfaces.py +482 -443
  10. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/README.md +0 -0
  11. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/__init__.py +0 -0
  12. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/ag2/__init__.py +0 -0
  13. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/ag2/agent.py +0 -0
  14. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/ag2/tools.py +0 -0
  15. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/agno/__init__.py +0 -0
  16. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/agno/memory.py +0 -0
  17. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/agno/tools.py +0 -0
  18. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/autogen/__init__.py +0 -0
  19. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/autogen/agent.py +0 -0
  20. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/autogen/tools.py +0 -0
  21. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/beeai/__init__.py +0 -0
  22. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/beeai/memory.py +0 -0
  23. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/beeai/tools.py +0 -0
  24. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/camel/__init__.py +0 -0
  25. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/camel/memory.py +0 -0
  26. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/camel/tools.py +0 -0
  27. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/controlflow/__init__.py +0 -0
  28. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/controlflow/memory.py +0 -0
  29. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/crewai/__init__.py +0 -0
  30. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/crewai/memory.py +0 -0
  31. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/crewai/tools.py +0 -0
  32. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/deepinfra/__init__.py +0 -0
  33. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/deepinfra/config.py +0 -0
  34. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/deepinfra/embeddings.py +0 -0
  35. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/deepinfra/openai_compat.py +0 -0
  36. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/dify/__init__.py +0 -0
  37. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/dify/manifest.yaml +0 -0
  38. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/dify/tool.py +0 -0
  39. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/dspy/__init__.py +0 -0
  40. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/dspy/memory.py +0 -0
  41. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/dspy/retriever.py +0 -0
  42. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/fireworks/__init__.py +0 -0
  43. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/fireworks/config.py +0 -0
  44. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/fireworks/embeddings.py +0 -0
  45. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/fireworks/openai_compat.py +0 -0
  46. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/flowise/__init__.py +0 -0
  47. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/flowise/node.py +0 -0
  48. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/google_adk/__init__.py +0 -0
  49. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/google_adk/tools.py +0 -0
  50. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/groq/__init__.py +0 -0
  51. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/groq/config.py +0 -0
  52. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/groq/embeddings.py +0 -0
  53. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/groq/openai_compat.py +0 -0
  54. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/hermes/__init__.py +0 -0
  55. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/hermes/provider.py +0 -0
  56. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/instructor/__init__.py +0 -0
  57. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/instructor/memory.py +0 -0
  58. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/langchain/__init__.py +0 -0
  59. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/langchain/memory.py +0 -0
  60. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/langchain/retriever.py +0 -0
  61. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/langchain/tools.py +0 -0
  62. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/langgraph/__init__.py +0 -0
  63. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/langgraph/memory.py +0 -0
  64. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/letta/__init__.py +0 -0
  65. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/letta/storage.py +0 -0
  66. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/llamaindex/__init__.py +0 -0
  67. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/llamaindex/memory.py +0 -0
  68. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/nemo_guardrails/__init__.py +0 -0
  69. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/nemo_guardrails/actions.py +0 -0
  70. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/nemo_guardrails/knowledge_base.py +0 -0
  71. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/ollama/__init__.py +0 -0
  72. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/ollama/config.py +0 -0
  73. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/ollama/embeddings.py +0 -0
  74. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/openai_agents/__init__.py +0 -0
  75. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/openai_agents/tools.py +0 -0
  76. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/prefect/__init__.py +0 -0
  77. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/prefect/blocks.py +0 -0
  78. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/prefect/tasks.py +0 -0
  79. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/pydanticai/__init__.py +0 -0
  80. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/pydanticai/dependencies.py +0 -0
  81. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/pydanticai/tools.py +0 -0
  82. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/smolagents/__init__.py +0 -0
  83. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/smolagents/tools.py +0 -0
  84. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/temporal/__init__.py +0 -0
  85. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/temporal/activities.py +0 -0
  86. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/temporal/interceptor.py +0 -0
  87. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/together/__init__.py +0 -0
  88. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/together/config.py +0 -0
  89. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/together/embeddings.py +0 -0
  90. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/together/openai_compat.py +0 -0
  91. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/vllm/__init__.py +0 -0
  92. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/vllm/config.py +0 -0
  93. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/vllm/embeddings.py +0 -0
  94. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/integrations/vllm/openai_compat.py +0 -0
  95. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/cortexdb/py.typed +0 -0
  96. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/tests/__init__.py +0 -0
  97. {cortexdbai-0.10.4 → cortexdbai-0.11.0}/tests/test_conflicts.py +0 -0
@@ -88,7 +88,14 @@ benchmarks/data_*/
88
88
  benchmarks/**/fast_results/
89
89
  data_*/
90
90
  *_data/
91
+ *_data_20*/
91
92
  cortexdb_data*/
92
93
  server_results/
93
94
  *.log
94
95
  docker_build.log
96
+
97
+ # local gate/bench data stores
98
+ gate*_data_*/
99
+
100
+ # Code-plane benchmark local run outputs (official results are committed deliberately)
101
+ benchmarks/codeplane/runs/
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: cortexdbai
3
- Version: 0.10.4
3
+ Version: 0.11.0
4
4
  Summary: The Long-Term Memory Layer for AI Systems
5
5
  Project-URL: Homepage, https://cortexdb.ai
6
6
  Project-URL: Documentation, https://docs.cortexdb.ai
@@ -36,6 +36,7 @@ from cortexdb.v1 import V1Client, AsyncV1Client
36
36
  from cortexdb.v1.exceptions import (
37
37
  V1APIError,
38
38
  V1AuthError,
39
+ V1InvalidRequestError,
39
40
  V1ConnectionError,
40
41
  V1Error,
41
42
  V1NotConfiguredError,
@@ -49,7 +50,7 @@ from cortexdb import v1 # the explicit submodule namespace
49
50
  Cortex = V1Client
50
51
  AsyncCortex = AsyncV1Client
51
52
 
52
- __version__ = "0.10.4"
53
+ __version__ = "0.11.0"
53
54
  __all__ = [
54
55
  # Clients (preferred top-level names)
55
56
  "Cortex",
@@ -63,6 +64,7 @@ __all__ = [
63
64
  "V1Error",
64
65
  "V1APIError",
65
66
  "V1AuthError",
67
+ "V1InvalidRequestError",
66
68
  "V1ConnectionError",
67
69
  "V1TimeoutError",
68
70
  "V1RateLimitError",
@@ -38,6 +38,7 @@ from cortexdb.v1.client import AsyncV1Client, V1Client
38
38
  from cortexdb.v1.exceptions import (
39
39
  V1APIError,
40
40
  V1AuthError,
41
+ V1InvalidRequestError,
41
42
  V1ConnectionError,
42
43
  V1Error,
43
44
  V1NotConfiguredError,
@@ -52,6 +53,7 @@ __all__ = [
52
53
  "V1Error",
53
54
  "V1APIError",
54
55
  "V1AuthError",
56
+ "V1InvalidRequestError",
55
57
  "V1ConnectionError",
56
58
  "V1TimeoutError",
57
59
  "V1RateLimitError",
@@ -28,6 +28,7 @@ except ImportError:
28
28
  from cortexdb.v1.exceptions import (
29
29
  V1APIError,
30
30
  V1ConnectionError,
31
+ V1InvalidRequestError,
31
32
  V1TimeoutError,
32
33
  )
33
34
 
@@ -237,6 +238,21 @@ def _forget_body(
237
238
  if memory_ids:
238
239
  selector["memory_ids"] = list(memory_ids)
239
240
  if selector:
241
+ # KAN-146: `confirm_all` authorizes a SCOPE-WIDE operation; combined
242
+ # with a selector it is ambiguous ("delete these ids" vs "delete
243
+ # everything") and the server refuses it with
244
+ # AMBIGUOUS_SELECTOR_CONFIRM_ALL. Refuse it here instead, so a
245
+ # caller that wires both up never reaches a destructive endpoint and
246
+ # gets a message that names the two supported modes.
247
+ if confirm_all:
248
+ raise V1InvalidRequestError(
249
+ "AMBIGUOUS_SELECTOR_CONFIRM_ALL",
250
+ "selector + confirm_all is ambiguous: confirm_all authorizes "
251
+ "a scope-wide operation and would ignore the selector. Send "
252
+ "confirm_all=False (or omit it) to delete exactly the "
253
+ "selected memories, or pass no selector with "
254
+ "confirm_all=True for a scope-wide forget",
255
+ )
240
256
  body["selector"] = selector
241
257
  if reason:
242
258
  body["reason"] = reason
@@ -256,6 +272,17 @@ def _erasure_body(
256
272
  and the server refuses them here."""
257
273
  body: dict = {"scope": scope, "confirm_all": confirm_all}
258
274
  if memory_ids:
275
+ # KAN-146: same rule as /v1/forget — confirm_all authorizes a
276
+ # WHOLE-SCOPE erasure and is only valid with an empty selector. The
277
+ # server refuses the combination (AMBIGUOUS_SELECTOR); refuse it
278
+ # before the irreversible call is made.
279
+ if confirm_all:
280
+ raise V1InvalidRequestError(
281
+ "AMBIGUOUS_SELECTOR",
282
+ "confirm_all=True is only valid with an EMPTY selector "
283
+ "(whole-scope erasure); to erase specific records pass "
284
+ "memory_ids WITHOUT confirm_all",
285
+ )
259
286
  body["selector"] = {"memory_ids": list(memory_ids)}
260
287
  if audit_note:
261
288
  body["audit_note"] = audit_note
@@ -418,6 +445,8 @@ class V1Client:
418
445
  self.timeout = timeout
419
446
  self._session = requests.Session()
420
447
  self._session.headers.update(_build_headers(actor, bearer))
448
+ #: ``client.code.*`` — the Code Intelligence Plane (T-906).
449
+ self.code = CodeNamespace(self)
421
450
 
422
451
  # ── auth ──────────────────────────────────────────────────────────
423
452
 
@@ -1917,3 +1946,212 @@ class AsyncV1Client:
1917
1946
 
1918
1947
  async def __aexit__(self, *_) -> None:
1919
1948
  await self.aclose()
1949
+
1950
+
1951
+ class CodeNamespace:
1952
+ """``client.code.*`` — the Code Intelligence Plane (T-906).
1953
+
1954
+ Thin typed wrappers over ``/v1/code/*``. Requires the "Code memory"
1955
+ feature bundle on the server; with it off every call raises the
1956
+ server's 503. Envelopes and certificates pass through verbatim —
1957
+ coverage, uncertainty and warnings reach your code unedited.
1958
+
1959
+ Example::
1960
+
1961
+ client = V1Client("http://localhost:3141", bearer=token)
1962
+ client.code.register_repo("shop", "/path/to/checkout")
1963
+ ctx = client.code.context("shop", "how does settlement work?")
1964
+ for item in ctx["envelope"]["items"]:
1965
+ print(item["citation"])
1966
+ """
1967
+
1968
+ def __init__(self, client: "V1Client") -> None:
1969
+ self._client = client
1970
+
1971
+ def register_repo(self, name: str, path: str) -> dict:
1972
+ """``POST /v1/code/repos`` — register + index a local checkout."""
1973
+ return self._client._post("/v1/code/repos", {"name": name, "path": path})
1974
+
1975
+ def repos(self) -> dict:
1976
+ """``GET /v1/code/repos`` — every repo this tenant registered."""
1977
+ return self._client._get("/v1/code/repos")
1978
+
1979
+ def repo_status(self, name: str) -> dict:
1980
+ """``GET /v1/code/repos/{name}`` — snapshot, stats, scope."""
1981
+ return self._client._get(f"/v1/code/repos/{name}")
1982
+
1983
+ def remove_repo(self, name: str) -> dict:
1984
+ """``DELETE /v1/code/repos/{name}``."""
1985
+ client = self._client
1986
+ try:
1987
+ r = client._session.delete(
1988
+ f"{client.api_url}/v1/code/repos/{name}", timeout=client.timeout
1989
+ )
1990
+ except requests.Timeout as e:
1991
+ raise V1TimeoutError(str(e)) from e
1992
+ except requests.ConnectionError as e:
1993
+ raise V1ConnectionError(str(e)) from e
1994
+ _raise_for_response(r.status_code, r.text)
1995
+ return r.json() if r.text else {}
1996
+
1997
+ def resolve_symbol(self, repo: str, name: str, *, limit: int = 50) -> dict:
1998
+ """``GET /v1/code/symbol/resolve`` — every in-scope match; an
1999
+ ambiguous name returns ALL candidates, never a guess."""
2000
+ return self._client._get(
2001
+ "/v1/code/symbol/resolve",
2002
+ params={"repo": repo, "name": name, "limit": limit},
2003
+ )
2004
+
2005
+ def calls(
2006
+ self,
2007
+ repo: str,
2008
+ symbol: str,
2009
+ *,
2010
+ direction: str = "out",
2011
+ depth: int = 1,
2012
+ include_dynamic: bool = False,
2013
+ ) -> dict:
2014
+ """``GET /v1/code/calls`` — call edges, tier-labeled; dynamic
2015
+ dispatch fan-out only with ``include_dynamic=True``."""
2016
+ return self._client._get(
2017
+ "/v1/code/calls",
2018
+ params={
2019
+ "repo": repo,
2020
+ "symbol": symbol,
2021
+ "direction": direction,
2022
+ "depth": depth,
2023
+ "include_dynamic": str(include_dynamic).lower(),
2024
+ },
2025
+ )
2026
+
2027
+ def impact(self, repo: str, symbol: str, *, depth: int = 3) -> dict:
2028
+ """``GET /v1/code/impact`` — reverse closure; each reached symbol
2029
+ reports hops and the WEAKEST precision tier on its path."""
2030
+ return self._client._get(
2031
+ "/v1/code/impact", params={"repo": repo, "symbol": symbol, "depth": depth}
2032
+ )
2033
+
2034
+ def graph_query(
2035
+ self,
2036
+ repo: str,
2037
+ roots: list[str],
2038
+ *,
2039
+ predicates: Optional[list[str]] = None,
2040
+ direction: str = "outgoing",
2041
+ depth: int = 1,
2042
+ max_nodes: int = 250,
2043
+ max_edges: int = 500,
2044
+ include_dynamic: bool = False,
2045
+ ) -> dict:
2046
+ """``POST /v1/code/graph/query`` — a bounded multi-predicate
2047
+ traversal with tier and status retained on every edge."""
2048
+ return self._client._post(
2049
+ "/v1/code/graph/query",
2050
+ {
2051
+ "repo": repo,
2052
+ "roots": roots,
2053
+ "predicates": predicates or ["calls"],
2054
+ "direction": direction,
2055
+ "depth": depth,
2056
+ "max_nodes": max_nodes,
2057
+ "max_edges": max_edges,
2058
+ "include_dynamic": include_dynamic,
2059
+ },
2060
+ )
2061
+
2062
+ def inventory(
2063
+ self,
2064
+ repo: str,
2065
+ kind: str,
2066
+ *,
2067
+ limit: int = 200,
2068
+ cursor: Optional[int] = None,
2069
+ ) -> dict:
2070
+ """``POST /v1/code/inventory`` — bounded definitions, routes,
2071
+ architecture, or unreferenced-definition candidates without a full
2072
+ database scan."""
2073
+ body = {"repo": repo, "kind": kind, "limit": limit}
2074
+ if cursor is not None:
2075
+ body["cursor"] = cursor
2076
+ return self._client._post("/v1/code/inventory", body)
2077
+
2078
+ def context(
2079
+ self,
2080
+ repo: str,
2081
+ task: str,
2082
+ *,
2083
+ changed_paths: Optional[list] = None,
2084
+ token_budget: int = 2000,
2085
+ ) -> dict:
2086
+ """``POST /v1/code/context`` — the task/diff evidence envelope:
2087
+ verbatim snippets with citations, coverage, uncertainty notes and
2088
+ pre-action warnings, under a hard token budget."""
2089
+ return self._client._post(
2090
+ "/v1/code/context",
2091
+ {
2092
+ "repo": repo,
2093
+ "task": task,
2094
+ "changed_paths": changed_paths or [],
2095
+ "token_budget": token_budget,
2096
+ },
2097
+ )
2098
+
2099
+ def workspace_context(
2100
+ self,
2101
+ task: str,
2102
+ *,
2103
+ repos: Optional[list[str]] = None,
2104
+ changed_paths: Optional[dict[str, list[str]]] = None,
2105
+ token_budget: int = 2000,
2106
+ ) -> dict:
2107
+ """``POST /v1/code/workspace/context`` — one tenant-scoped,
2108
+ token-budgeted context query across selected repositories. Empty
2109
+ ``repos`` searches every repository visible to the caller."""
2110
+ return self._client._post(
2111
+ "/v1/code/workspace/context",
2112
+ {
2113
+ "repos": repos or [],
2114
+ "task": task,
2115
+ "changed_paths": changed_paths or {},
2116
+ "token_budget": token_budget,
2117
+ },
2118
+ )
2119
+
2120
+ def coverage_status(self, repo: str) -> dict:
2121
+ """``GET /v1/code/coverage/status``."""
2122
+ return self._client._get("/v1/code/coverage/status", params={"repo": repo})
2123
+
2124
+ def history_for_symbol(
2125
+ self, repo: str, name: str, *, cursor: Optional[int] = None, limit: int = 50
2126
+ ) -> dict:
2127
+ """``GET /v1/code/history/for-symbol`` — lineage-merged events,
2128
+ newest first, cursor-paginated."""
2129
+ params: dict = {"repo": repo, "name": name, "limit": limit}
2130
+ if cursor is not None:
2131
+ params["cursor"] = cursor
2132
+ return self._client._get("/v1/code/history/for-symbol", params=params)
2133
+
2134
+ def tests_for_change(self, repo: str, name: str) -> dict:
2135
+ """``GET /v1/code/tests/for-change`` — rows labeled static
2136
+ (reachability) vs observed (recorded coverage); absent coverage
2137
+ is disclosed, never upgraded."""
2138
+ return self._client._get(
2139
+ "/v1/code/tests/for-change", params={"repo": repo, "name": name}
2140
+ )
2141
+
2142
+ def certify(self, repo: str, question: str, symbol: str) -> dict:
2143
+ """``POST /v1/code/certify`` — ``list-implementations`` or
2144
+ ``count-call-sites``; returns a hash-verified certificate or
2145
+ ``incomplete`` with reasons."""
2146
+ return self._client._post(
2147
+ "/v1/code/certify", {"repo": repo, "question": question, "symbol": symbol}
2148
+ )
2149
+
2150
+ def gc(self, repo: str) -> dict:
2151
+ """``POST /v1/code/gc`` — one two-phase GC cycle; the report
2152
+ enumerates every destructive step."""
2153
+ return self._client._post("/v1/code/gc", {"repo": repo})
2154
+
2155
+ def matrix(self) -> dict:
2156
+ """``GET /v1/code/matrix`` — the earned capability matrix."""
2157
+ return self._client._get("/v1/code/matrix")
@@ -63,6 +63,27 @@ class V1APIError(V1Error):
63
63
  )
64
64
 
65
65
 
66
+ class V1InvalidRequestError(V1APIError):
67
+ """Client-side refusal, raised BEFORE any network call.
68
+
69
+ The request is self-contradictory in a way the server also refuses, so
70
+ the SDK stops it locally rather than sending it — nothing is mutated and
71
+ the caller gets the same ``error_code`` the server would return.
72
+ Subclasses :class:`V1APIError` (with ``status=400``) so existing
73
+ ``except V1APIError`` handlers keep working.
74
+ """
75
+
76
+ def __init__(self, error_code: str, message: str):
77
+ super().__init__(
78
+ status=400,
79
+ error_code=error_code,
80
+ message=message,
81
+ request_id=None,
82
+ details=None,
83
+ retriable=False,
84
+ )
85
+
86
+
66
87
  class V1AuthError(V1APIError):
67
88
  """401 — missing/invalid token, actor mismatch, revoked token, wrong tenant."""
68
89
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "cortexdbai"
7
- version = "0.10.4"
7
+ version = "0.11.0"
8
8
  description = "The Long-Term Memory Layer for AI Systems"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -0,0 +1,104 @@
1
+ """T-906: ``client.code.*`` mirrors the /v1/code surface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from unittest.mock import patch
6
+
7
+ from cortexdb.v1.client import V1Client
8
+
9
+
10
+ def _client() -> V1Client:
11
+ return V1Client("http://localhost:3141", actor="user:test")
12
+
13
+
14
+ class TestCodeNamespace:
15
+ def test_namespace_is_attached(self) -> None:
16
+ client = _client()
17
+ assert hasattr(client, "code")
18
+
19
+ def test_reads_route_to_the_right_paths(self) -> None:
20
+ client = _client()
21
+ calls: list[tuple] = []
22
+
23
+ def fake_get(path, *, params=None):
24
+ calls.append(("GET", path, dict(params or {})))
25
+ return {"ok": True}
26
+
27
+ with patch.object(client, "_get", side_effect=fake_get):
28
+ client.code.repos()
29
+ client.code.repo_status("shop")
30
+ client.code.resolve_symbol("shop", "settle")
31
+ client.code.calls("shop", "settle", direction="in", depth=2)
32
+ client.code.impact("shop", "settle", depth=4)
33
+ client.code.coverage_status("shop")
34
+ client.code.history_for_symbol("shop", "settle", cursor=3)
35
+ client.code.tests_for_change("shop", "settle")
36
+ client.code.matrix()
37
+ paths = [c[1] for c in calls]
38
+ assert paths == [
39
+ "/v1/code/repos",
40
+ "/v1/code/repos/shop",
41
+ "/v1/code/symbol/resolve",
42
+ "/v1/code/calls",
43
+ "/v1/code/impact",
44
+ "/v1/code/coverage/status",
45
+ "/v1/code/history/for-symbol",
46
+ "/v1/code/tests/for-change",
47
+ "/v1/code/matrix",
48
+ ]
49
+ calls_params = calls[3][2]
50
+ assert calls_params["direction"] == "in"
51
+ assert calls_params["depth"] == 2
52
+ assert calls[6][2]["cursor"] == 3
53
+
54
+ def test_writes_route_with_bodies(self) -> None:
55
+ client = _client()
56
+ posts: list[tuple] = []
57
+
58
+ def fake_post(path, body):
59
+ posts.append((path, body))
60
+ return {"ok": True}
61
+
62
+ with patch.object(client, "_post", side_effect=fake_post):
63
+ client.code.register_repo("shop", "/src/shop")
64
+ client.code.context(
65
+ "shop", "how does settle work?", changed_paths=["a.ts"], token_budget=500
66
+ )
67
+ client.code.workspace_context(
68
+ "trace the request",
69
+ repos=["api", "worker"],
70
+ changed_paths={"api": ["src/client.ts"]},
71
+ token_budget=900,
72
+ )
73
+ client.code.graph_query(
74
+ "shop",
75
+ ["invoice.created"],
76
+ predicates=["handles_event", "emits_event"],
77
+ direction="incoming",
78
+ include_dynamic=True,
79
+ )
80
+ client.code.inventory("shop", "architecture", limit=50, cursor=7)
81
+ client.code.certify("shop", "count-call-sites", "sym")
82
+ client.code.gc("shop")
83
+ assert [p[0] for p in posts] == [
84
+ "/v1/code/repos",
85
+ "/v1/code/context",
86
+ "/v1/code/workspace/context",
87
+ "/v1/code/graph/query",
88
+ "/v1/code/inventory",
89
+ "/v1/code/certify",
90
+ "/v1/code/gc",
91
+ ]
92
+ assert posts[1][1]["token_budget"] == 500
93
+ assert posts[1][1]["changed_paths"] == ["a.ts"]
94
+ assert posts[2][1]["repos"] == ["api", "worker"]
95
+ assert posts[2][1]["changed_paths"] == {"api": ["src/client.ts"]}
96
+ assert posts[2][1]["token_budget"] == 900
97
+ assert posts[3][1]["predicates"] == ["handles_event", "emits_event"]
98
+ assert posts[3][1]["include_dynamic"] is True
99
+ assert posts[4][1] == {
100
+ "repo": "shop",
101
+ "kind": "architecture",
102
+ "limit": 50,
103
+ "cursor": 7,
104
+ }