cortexdb-cli 0.7.0__py3-none-any.whl → 0.8.0__py3-none-any.whl

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.
cortexdb_cli/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """CortexDB CLI - command-line interface for the v1 memory API."""
2
2
 
3
- __version__ = "0.7.0"
3
+ __version__ = "0.8.0"
@@ -95,7 +95,7 @@ def admin_layers(ctx: click.Context) -> None:
95
95
 
96
96
  # -- retention (unified WAL) -------------------------------------------------
97
97
  #
98
- # The operator surface over the D1 retention plane. Every write here is
98
+ # The operator surface over the retention plane. Every write here is
99
99
  # irreversible in effect - an applied sweep removes expired captured events,
100
100
  # everything about them under an `erase` rule, their raw data under a
101
101
  # `keep_learned` one - so reporting is the default on both mutating commands
@@ -151,7 +151,11 @@ def tenant_settings(ctx, root, mode, readiness_ticket, clear_readiness):
151
151
  def tenant_binding(ctx, root, from_tenant, apply_):
152
152
  """Bind a root's records and tombstones to the tenant it names (POST /v1/admin/access/tenant-binding).
153
153
 
154
- A DRY RUN unless --apply; running it again is safe.
154
+ A DRY RUN unless --apply; running it again is safe. The dry run says how
155
+ the operator plane decides the real run (operator_plane: whether it
156
+ registers the root, is in-band, the tenant's mode): when it says
157
+ break_glass_required, pass --break-glass REASON --break-glass-ticket
158
+ TICKET with --apply.
155
159
  """
156
160
  cfg = ctx.obj
157
161
  scope = root_or_die(cfg, root)
@@ -169,6 +173,15 @@ def tenant_binding(ctx, root, from_tenant, apply_):
169
173
  + (", root registered" if d.get("root_registered") else ""))
170
174
  if d.get("consistency_token"):
171
175
  console.print(f"[dim] consistency token: {escape(str(d['consistency_token']))}[/dim]")
176
+ plane = d.get("operator_plane")
177
+ if isinstance(plane, dict):
178
+ # 0.14.1 (p6a C): how the operator plane decides the run.
179
+ console.print(f" operator plane: mode {escape(str(plane.get('mode')))}"
180
+ + (", the run registers the root" if plane.get("registers_root") else "")
181
+ + (", an in-band path for the operator key" if plane.get("in_band") else ""))
182
+ if plane.get("break_glass_required"):
183
+ console.print("[warning] The real run needs break-glass: add --break-glass REASON "
184
+ "--break-glass-ticket TICKET to --apply (else 403 BREAK_GLASS_REQUIRED).[/warning]")
172
185
  if d.get("dry_run"):
173
186
  console.print("[dim]Nothing was changed. Run again with --apply.[/dim]")
174
187
 
@@ -5,12 +5,20 @@ from __future__ import annotations
5
5
  import click
6
6
 
7
7
  from cortexdb_cli.client import get_client
8
- from cortexdb_cli.common import api_options, at_least_option
8
+ from cortexdb_cli.common import (
9
+ api_options,
10
+ at_least_option,
11
+ derived_by_option,
12
+ include_option,
13
+ include_param,
14
+ )
9
15
  from cortexdb_cli.errors import handle_errors
10
16
  from cortexdb_cli.output import (
11
17
  console,
18
+ derivation_summary,
12
19
  escape,
13
20
  is_json_mode,
21
+ note_unreported_derivation,
14
22
  print_json,
15
23
  spinner,
16
24
  )
@@ -40,15 +48,30 @@ def _scope_or_die(cfg: dict, scope: str | None) -> str:
40
48
  @beliefs_group.command("list")
41
49
  @click.option("--scope", "-S", help="Scope path (defaults to your default scope)")
42
50
  @click.option("--limit", "-l", default=50, show_default=True)
51
+ @include_option
43
52
  @at_least_option
44
53
  @api_options
45
54
  @click.pass_context
46
55
  @handle_errors
47
- def beliefs_list(ctx: click.Context, scope: str | None, limit: int, at_least: str | None) -> None:
48
- """List active beliefs in a scope."""
56
+ def beliefs_list(
57
+ ctx: click.Context,
58
+ scope: str | None,
59
+ limit: int,
60
+ include: tuple[str, ...],
61
+ at_least: str | None,
62
+ ) -> None:
63
+ """List active beliefs in a scope.
64
+
65
+ With --include derivation each belief also says how it was made,
66
+ computed from its supporting facts (the latest one's; unsynced when any
67
+ of them is). A belief with no fact support, a declared one, has no stamp.
68
+ """
49
69
  cfg = ctx.obj
50
70
  s = _scope_or_die(cfg, scope)
51
71
  params: dict[str, str | int] = {"scope": s, "limit": limit}
72
+ included = include_param(include)
73
+ if included:
74
+ params["include"] = included
52
75
  if at_least:
53
76
  params["at_least"] = at_least
54
77
  with get_client(cfg) as client:
@@ -79,19 +102,26 @@ def beliefs_list(ctx: click.Context, scope: str | None, limit: int, at_least: st
79
102
  table.add_column("Object", style="bold")
80
103
  table.add_column("Conf", justify="right", width=6)
81
104
  table.add_column("Support", justify="right", width=8)
105
+ if included:
106
+ table.add_column("Why", style="dim", max_width=48)
82
107
  for b in items:
83
108
  # v1 beliefs carry their triple under `claim` ({subject, predicate,
84
109
  # object}, each term {type, value|id}); the columns used to read
85
110
  # top-level `predicate`/`object`, which no belief has.
86
111
  claim = b.get("claim") if isinstance(b.get("claim"), dict) else b
87
- table.add_row(
112
+ row = [
88
113
  escape(str(b.get("belief_id") or b.get("id", ""))[:16]),
89
114
  escape(_term(claim.get("predicate"))),
90
115
  escape(_term(claim.get("object"))),
91
116
  f"{(b.get('confidence') or 0):.0%}",
92
117
  str(b.get("support_count") or len(b.get("supports") or [])),
93
- )
118
+ ]
119
+ if included:
120
+ row.append(escape(derivation_summary(b.get("derivation"))))
121
+ table.add_row(*row)
94
122
  console.print(table)
123
+ if included:
124
+ note_unreported_derivation(items)
95
125
  if isinstance(data, dict) and data.get("rollups"):
96
126
  console.print(
97
127
  f"[dim]{len(data['rollups'])} rollup(s) on this page; "
@@ -108,19 +138,30 @@ def beliefs_list(ctx: click.Context, scope: str | None, limit: int, at_least: st
108
138
  help="Return the belief only if it lies at or under this scope (?scope=; else 404). It "
109
139
  "routes the read on a cluster, and a break-glass read of a private region must name it.",
110
140
  )
141
+ @include_option
111
142
  @api_options
112
143
  @click.pass_context
113
144
  @handle_errors
114
- def beliefs_why(ctx: click.Context, belief_id: str, scope: str | None) -> None:
145
+ def beliefs_why(
146
+ ctx: click.Context, belief_id: str, scope: str | None, include: tuple[str, ...]
147
+ ) -> None:
115
148
  """Show the facts + events that ground a belief.
116
149
 
150
+ \b
117
151
  cortexdb beliefs why belief_01HX...
118
152
  cortexdb beliefs why belief_01HX... --scope org:acme/team:eng
153
+ cortexdb beliefs why belief_01HX... --include derivation
154
+
155
+ With --include derivation the belief also carries its derivation, and
156
+ the terminal says it on one line under the answer.
119
157
  """
120
158
  cfg = ctx.obj
121
159
  params = {"belief_id": belief_id}
122
160
  if scope:
123
161
  params["scope"] = scope
162
+ included = include_param(include)
163
+ if included:
164
+ params["include"] = included
124
165
  with get_client(cfg) as client:
125
166
  if not is_json_mode(cfg):
126
167
  with spinner("Tracing provenance..."):
@@ -128,7 +169,13 @@ def beliefs_why(ctx: click.Context, belief_id: str, scope: str | None) -> None:
128
169
  else:
129
170
  resp = client.get("/v1/beliefs/why", params=params)
130
171
  resp.raise_for_status()
131
- print_json(resp.json())
172
+ data = resp.json()
173
+ print_json(data)
174
+ if included and not is_json_mode(cfg):
175
+ belief = data.get("belief") if isinstance(data, dict) else None
176
+ if isinstance(belief, dict):
177
+ console.print(f"[dim]why: {escape(derivation_summary(belief.get('derivation')))}[/dim]")
178
+ note_unreported_derivation([belief])
132
179
 
133
180
 
134
181
  @beliefs_group.command("create")
@@ -165,6 +212,10 @@ def beliefs_why(ctx: click.Context, belief_id: str, scope: str | None) -> None:
165
212
  "key is 409 IDEMPOTENCY_CONFLICT).",
166
213
  )
167
214
  @click.option("--scope", "-S", help="Scope path (defaults to your default scope)")
215
+ @derived_by_option(
216
+ "A declared belief carries it in its record (below apply format 10 the server answers 503 "
217
+ "FORMAT_UPGRADE_PENDING), and include=derivation reads it back."
218
+ )
168
219
  @api_options
169
220
  @click.pass_context
170
221
  @handle_errors
@@ -179,16 +230,20 @@ def beliefs_create(
179
230
  provenance: tuple[str, ...],
180
231
  idempotency_key: str | None,
181
232
  scope: str | None,
233
+ derived_by: dict[str, str] | None,
182
234
  ) -> None:
183
235
  """Declare a belief directly (POST /v1/beliefs).
184
236
 
185
237
  \b
186
238
  cortexdb beliefs create "Acme is on the enterprise plan" \\
187
239
  --confidence 0.9 --support evt_01HX... --subject Acme --predicate plan
240
+ cortexdb beliefs create "Bob reads at level B2" --confidence 0.8 \\
241
+ --subject Bob --predicate level --derived-by acme-level@1.1.0
188
242
 
189
243
  Declaring a newer belief over the same (subject, predicate) closes the
190
244
  previous one. Pass --idempotency-key to retry safely after an outcome
191
- you did not learn (WAL_OUTCOME_UNKNOWN, a lost connection).
245
+ you did not learn (WAL_OUTCOME_UNKNOWN, a lost connection); the same key
246
+ with another --derived-by stamp, or none, is 409 IDEMPOTENCY_CONFLICT.
192
247
  """
193
248
  cfg = ctx.obj
194
249
  s = _scope_or_die(cfg, scope)
@@ -210,6 +265,8 @@ def beliefs_create(
210
265
  if not idempotency_key.strip() or len(idempotency_key.encode("utf-8")) > 256:
211
266
  raise click.UsageError("--idempotency-key is 1 to 256 bytes, not blank")
212
267
  body["idempotency_key"] = idempotency_key
268
+ if derived_by:
269
+ body["derived_by"] = derived_by
213
270
  with get_client(cfg) as client:
214
271
  resp = client.post("/v1/beliefs", json=body)
215
272
  resp.raise_for_status()