adopt-cli 0.4.2__tar.gz → 0.4.4__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 (55) hide show
  1. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/PKG-INFO +1 -1
  2. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/pyproject.toml +1 -1
  3. adopt_cli-0.4.4/src/adopt_cli/_build_info.py +6 -0
  4. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/_handover_support.py +7 -2
  5. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/_knowledge_support.py +28 -0
  6. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/_pack_support.py +18 -8
  7. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/knowledge.py +87 -13
  8. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/store_option.py +11 -0
  9. adopt_cli-0.4.2/src/adopt_cli/_build_info.py +0 -6
  10. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/.gitignore +0 -0
  11. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/LICENSE +0 -0
  12. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/NOTICE +0 -0
  13. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/__init__.py +0 -0
  14. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/__init__.py +0 -0
  15. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/_ask_support.py +0 -0
  16. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/_ci_sense_probes.py +0 -0
  17. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/_ci_sense_support.py +0 -0
  18. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/_conflict_support.py +0 -0
  19. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/_coverage_support.py +0 -0
  20. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/_draft_support.py +0 -0
  21. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/_drafting.py +0 -0
  22. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/_map_support.py +0 -0
  23. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/_probe_support.py +0 -0
  24. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/_refresh_support.py +0 -0
  25. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/_remote_support.py +0 -0
  26. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/_review_remote.py +0 -0
  27. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/agent.py +0 -0
  28. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/answer.py +0 -0
  29. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/ask.py +0 -0
  30. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/boundary.py +0 -0
  31. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/ci_sense.py +0 -0
  32. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/coverage.py +0 -0
  33. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/detect.py +0 -0
  34. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/doctor.py +0 -0
  35. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/draft.py +0 -0
  36. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/freshness.py +0 -0
  37. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/handover.py +0 -0
  38. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/identity.py +0 -0
  39. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/init.py +0 -0
  40. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/interchange.py +0 -0
  41. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/map_command.py +0 -0
  42. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/pack.py +0 -0
  43. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/policy.py +0 -0
  44. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/probe.py +0 -0
  45. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/pull.py +0 -0
  46. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/refresh.py +0 -0
  47. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/serve.py +0 -0
  48. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/store.py +0 -0
  49. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/commands/version.py +0 -0
  50. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/config.py +0 -0
  51. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/json_out.py +0 -0
  52. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/main.py +0 -0
  53. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/py.typed +0 -0
  54. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/remote.py +0 -0
  55. {adopt_cli-0.4.2 → adopt_cli-0.4.4}/src/adopt_cli/replica.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: adopt-cli
3
- Version: 0.4.2
3
+ Version: 0.4.4
4
4
  Summary: The adopt command surface: config resolution, JSON mode, exit codes.
5
5
  Project-URL: Homepage, https://github.com/onboardux/onboard-core
6
6
  Project-URL: Source, https://github.com/onboardux/onboard-core
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "adopt-cli"
3
- version = "0.4.2"
3
+ version = "0.4.4"
4
4
  description = "The adopt command surface: config resolution, JSON mode, exit codes."
5
5
  requires-python = ">=3.12"
6
6
  license = "Apache-2.0"
@@ -0,0 +1,6 @@
1
+ """Generated by scripts/embed_build_info.py; do not edit in a release build."""
2
+
3
+ from typing import Final
4
+
5
+ SBOM_SHA256: Final[str | None] = '7ac34a2179a12abe098bc85d0bb92f3bfaf3b739137331be994bd2e5cdd9ffe9'
6
+ BUILD_ID: Final[str | None] = 'github:onboardux/onboard-core:37424931824:1:4e9cd060543afa8eed40b405d4f8c8d73379ffd4'
@@ -447,9 +447,14 @@ def opening_position(handle: Any, *, system: ResolvedSystem, now: _dt.datetime)
447
447
  accumulated before it (v6.1 §6's sales-motion note), and a closure that
448
448
  cannot say where it started cannot say what it added.
449
449
  """
450
+ from adopt_knowledge import is_live
451
+
450
452
  coverage = _coverage(handle, system)
451
- identities = len(coverage.identities)
452
- covered = sum(1 for row in coverage.identities if row.covered)
453
+ # Live referents only, as `adopt gaps` counts them: a retired identity is
454
+ # not part of the system being handed over.
455
+ live = [row for row in coverage.identities if is_live(row)]
456
+ identities = len(live)
457
+ covered = sum(1 for row in live if row.covered)
453
458
 
454
459
  confirmed = 0
455
460
  unverified = 0
@@ -50,6 +50,7 @@ __all__ = [
50
50
  "identity_views",
51
51
  "pending_items",
52
52
  "presented_revisions",
53
+ "presented_texts",
53
54
  "rebind_target",
54
55
  "refresh_population",
55
56
  "resolve_identity",
@@ -327,6 +328,33 @@ def presented_revisions(handle: KnowledgeStoreView) -> frozenset[str]:
327
328
  )
328
329
 
329
330
 
331
+ def presented_texts(handle: KnowledgeStoreView) -> frozenset[tuple[str, str]]:
332
+ """`(item_id, document digest)` for every text an ingest suggestion batch carried.
333
+
334
+ `presented_revisions`' own rule -- "new text means a new revision" -- stated
335
+ the other way round, because one writer breaks it: `confirm-current`
336
+ appends a revision with the same text and the same digest, and the next
337
+ ingest used to re-propose every suggestion the reviewer had rejected for
338
+ that text. Suggestion batches only: a verification batch asked whether the
339
+ text is true, not what it is about.
340
+ """
341
+ suggestion_batches = {
342
+ row.id
343
+ for row in _rows(handle, "review_batch", ReviewBatch)
344
+ if source_of(row.batch_key) == SOURCE_INGEST
345
+ }
346
+ carried = {
347
+ row.proposed_revision_id
348
+ for row in _rows(handle, "review_item", ReviewItem)
349
+ if row.review_batch_id in suggestion_batches and row.proposed_revision_id is not None
350
+ }
351
+ return frozenset(
352
+ (row.item_id, row.source_version)
353
+ for row in _rows(handle, "knowledge_revision", KnowledgeRevision)
354
+ if row.id in carried and row.source_version is not None
355
+ )
356
+
357
+
330
358
  def resolve_identity(handle: KnowledgeStoreView, uri: str) -> Identity | None:
331
359
  """An identity by URI, following an alias to the identity that replaced it.
332
360
 
@@ -130,19 +130,28 @@ def build_drafts(handle: Any, *, system_id: str, environment_id: str | None) ->
130
130
 
131
131
 
132
132
  def build_identities(
133
- handle: Any, *, system_id: str, environment_id: str | None, covered: frozenset[str]
133
+ handle: Any,
134
+ *,
135
+ system_id: str,
136
+ environment_id: str | None,
137
+ covered: frozenset[str],
138
+ live: frozenset[str],
134
139
  ) -> _Reader:
135
140
  """The inventory, with coverage taken from the recompute rather than the cache.
136
141
 
137
142
  `identity.covered_cache` is a cache that alarms rather than self-heals, and
138
143
  a pack must not be the thing that reads it: the recompute result the caller
139
- already holds is the authority (Build 0's rule, unchanged).
144
+ already holds is the authority (Build 0's rule, unchanged). Only `live`
145
+ referents are listed: a pack describes the system as it is, and a retired
146
+ identity counted into "covered N of M" makes the handover read worse with
147
+ every rename (`adopt_knowledge.is_live`).
140
148
  """
141
149
  rows = [
142
150
  PackIdentity(uri=str(row.uri), kind=str(row.identity_kind), covered=str(row.id) in covered)
143
151
  for row in handle.pack_records().identities_in_scope(
144
152
  system_id=system_id, environment_id=environment_id
145
153
  )
154
+ if str(row.id) in live
146
155
  ]
147
156
  return _Reader(tuple(rows))
148
157
 
@@ -238,23 +247,24 @@ def assemble_pack(handle: Any, *, audience: str, system_id: str, environment_id:
238
247
  rendering it is a pure function and the caller may close the store first.
239
248
  """
240
249
  from adopt_handover import assemble
241
- from adopt_knowledge import rank_gaps
250
+ from adopt_knowledge import is_live, rank_gaps
242
251
 
243
252
  from adopt_coverage import recompute_coverage
244
253
 
245
254
  coverage = recompute_coverage(handle.coverage_records(), system_id, environment_id)
246
255
  covered = frozenset(row.identity_id for row in coverage.identities if row.covered)
256
+ live = frozenset(row.identity_id for row in coverage.identities if is_live(row))
247
257
  ranked = rank_gaps(coverage.identities)
248
- # Every identity the recompute evaluated, by URI. The conflict join needs
249
- # it, and it is the same population the inventory renders -- so a conflict
250
- # can never name an identity this pack does not list.
251
- uris = {row.identity_id: row.uri for row in coverage.identities}
258
+ # Every live identity the recompute evaluated, by URI. The conflict join
259
+ # needs it, and it is the same population the inventory renders -- so a
260
+ # conflict can never name an identity this pack does not list.
261
+ uris = {row.identity_id: row.uri for row in coverage.identities if row.identity_id in live}
252
262
 
253
263
  return assemble(
254
264
  audience=audience,
255
265
  knowledge=build_knowledge(handle, system_id=system_id, environment_id=environment_id),
256
266
  identities=build_identities(
257
- handle, system_id=system_id, environment_id=environment_id, covered=covered
267
+ handle, system_id=system_id, environment_id=environment_id, covered=covered, live=live
258
268
  ),
259
269
  freshness=FreshnessCache(handle),
260
270
  boundary=build_boundary(handle, system_id=system_id, environment_id=environment_id),
@@ -45,6 +45,19 @@ ActorOption = Annotated[
45
45
  str | None,
46
46
  typer.Option("--actor", help="Who is running this. Recorded on every revision written."),
47
47
  ]
48
+ UnverifiedOption = Annotated[
49
+ bool,
50
+ typer.Option(
51
+ "--unverified",
52
+ help=(
53
+ "Land these documents unverified, each queued for a person to confirm in "
54
+ "`adopt review`. Use it for text nobody has vouched for -- anything an agent "
55
+ "wrote or helped write. Until confirmed it counts toward no coverage, serves "
56
+ "no answer and reaches no pack. Its name-match suggestions are held back "
57
+ "(`suggestions_deferred`) and queued by the next ingest of the confirmed text."
58
+ ),
59
+ ),
60
+ ]
48
61
  StoreOption = Annotated[Path | None, typer.Option("--store", help="Store path override.")]
49
62
  JsonOption = Annotated[bool, typer.Option("--json", help="Emit the strict JSON envelope only.")]
50
63
 
@@ -54,6 +67,7 @@ def ingest(
54
67
  scope: ScopeOption = None,
55
68
  audience: AudienceOption = None,
56
69
  actor: ActorOption = None,
70
+ unverified: UnverifiedOption = False,
57
71
  store: StoreOption = None,
58
72
  json_output: JsonOption = False,
59
73
  ) -> None:
@@ -66,6 +80,7 @@ def ingest(
66
80
  bound_pairs,
67
81
  identity_views,
68
82
  presented_revisions,
83
+ presented_texts,
69
84
  stored_documents,
70
85
  )
71
86
  from adopt_cli.commands._map_support import resolve_scope
@@ -87,7 +102,9 @@ def ingest(
87
102
  unit=StoreUnitOfWork(handle),
88
103
  bound_pairs=bound_pairs(handle),
89
104
  presented_revisions=presented_revisions(handle),
105
+ presented_texts=presented_texts(handle),
90
106
  actor_id=actor,
107
+ unverified=unverified,
91
108
  )
92
109
  payload = _ingest_payload(report)
93
110
  finally:
@@ -107,6 +124,13 @@ def _ingest_payload(report: Any) -> dict[str, Any]:
107
124
  "suggestions": report.suggestions,
108
125
  "review_batch": report.review_batch_id,
109
126
  "review_items": list(report.review_item_ids),
127
+ # Additive (`--unverified`): what this run wrote unverified, and the
128
+ # batch a person confirms it in. Present on every run so the envelope
129
+ # is one shape whichever way the flag was set.
130
+ "unverified": report.unverified,
131
+ "verification_batch": report.verification_batch_id,
132
+ "verification_items": list(report.verification_item_ids),
133
+ "suggestions_deferred": report.suggestions_deferred,
110
134
  "unknown_audiences": list(report.unknown_audiences),
111
135
  "ingested": [
112
136
  {
@@ -286,10 +310,17 @@ def bind(
286
310
 
287
311
  For the links no heuristic finds. A binding made here is human-justified by
288
312
  construction, which is the same standing a confirmed suggestion has.
313
+
314
+ **A pair bound before is revived, not refused.** One item and one identity
315
+ have one binding for ever, so a link a `rebind` superseded or a retirement
316
+ withdrew cannot be created again -- and until this path existed, nothing
317
+ could restore it. Binding such a pair appends an `active` revision to its
318
+ chain and restarts its freshness, and the payload says `reactivated`. A pair
319
+ that is live already is still refused.
289
320
  """
290
321
  from adopt_knowledge.ingest import EXTRACTOR_MANUAL, INGEST_EXTRACTOR_VERSION
291
322
 
292
- from adopt_cli.commands._knowledge_support import resolve_identity
323
+ from adopt_cli.commands._knowledge_support import StoreUnitOfWork, resolve_identity
293
324
  from adopt_obs import AdoptError, ErrorCode
294
325
 
295
326
  handle = open_configured_store(store, read_only=False, verb="bind")
@@ -312,22 +343,50 @@ def bind(
312
343
  )
313
344
  from adopt_cli.commands._coverage_support import coverage_cache_kept_current
314
345
 
346
+ bindings = handle.bindings()
347
+ earlier = next(
348
+ (row for row in bindings.for_identity(identity.id) if row.item_id == knowledge_id),
349
+ None,
350
+ )
351
+ head_status = (
352
+ None
353
+ if earlier is None
354
+ else handle.freshness_records().head_binding_statuses([earlier.id]).get(earlier.id)
355
+ )
315
356
  with coverage_cache_kept_current(handle, identity.system_id):
316
- binding_id, revision_id = handle.bindings().bind(
317
- item_id=knowledge_id,
318
- identity_id=identity.id,
319
- is_load_bearing=not not_load_bearing,
320
- extractor=EXTRACTOR_MANUAL,
321
- extractor_version=INGEST_EXTRACTOR_VERSION,
322
- actor_id=actor,
323
- )
357
+ if earlier is not None and head_status in _REVIVABLE_BINDING_STATUSES:
358
+ with StoreUnitOfWork(handle).transaction():
359
+ revision_id = bindings.reactivate(
360
+ binding_id=earlier.id,
361
+ expected_head_id=earlier.current_revision_id,
362
+ extractor=EXTRACTOR_MANUAL,
363
+ extractor_version=INGEST_EXTRACTOR_VERSION,
364
+ actor_id=actor,
365
+ )
366
+ handle.changes().restart_bindings([earlier.id])
367
+ binding_id = earlier.id
368
+ # The pair's row keeps the load-bearing flag it was created with;
369
+ # report what is true of it rather than what was asked for.
370
+ is_load_bearing = bool(earlier.is_load_bearing)
371
+ else:
372
+ # A new pair, or the facade's refusal of one that is live already.
373
+ binding_id, revision_id = bindings.bind(
374
+ item_id=knowledge_id,
375
+ identity_id=identity.id,
376
+ is_load_bearing=not not_load_bearing,
377
+ extractor=EXTRACTOR_MANUAL,
378
+ extractor_version=INGEST_EXTRACTOR_VERSION,
379
+ actor_id=actor,
380
+ )
381
+ is_load_bearing = not not_load_bearing
324
382
  payload = {
325
383
  "binding": binding_id,
326
384
  "revision": revision_id,
327
385
  "item": knowledge_id,
328
386
  "identity": identity.id,
329
387
  "uri": identity.uri,
330
- "is_load_bearing": not not_load_bearing,
388
+ "is_load_bearing": is_load_bearing,
389
+ "reactivated": earlier is not None and head_status in _REVIVABLE_BINDING_STATUSES,
331
390
  # Stated because the alias is silent otherwise: binding to a moved
332
391
  # identity's old address is correct and surprising, and the operator
333
392
  # should see which referent they actually bound.
@@ -339,6 +398,11 @@ def bind(
339
398
  emit(payload, as_json=json_output, title="adopt bind")
340
399
 
341
400
 
401
+ #: A binding head a person may take up again: replaced by a `rebind`, or
402
+ #: withdrawn. `active` is absent on purpose -- that pair is bound already.
403
+ _REVIVABLE_BINDING_STATUSES: Final[frozenset[str]] = frozenset({"moved", "retired"})
404
+
405
+
342
406
  AckOption = Annotated[
343
407
  str | None,
344
408
  typer.Option("--ack", help="Acknowledge one gap by its gap-key: someone owns closing it."),
@@ -550,10 +614,19 @@ def _parse_until(value: str | None) -> _dt.datetime | None:
550
614
  def _gaps_payload(
551
615
  result: Any, ranked: tuple[Any, ...], dispositions: dict[str, Any]
552
616
  ) -> dict[str, Any]:
617
+ from adopt_knowledge import is_live
618
+
619
+ # Counted over live referents, the population the gap list is drawn from,
620
+ # so `uncovered` is the length of `gaps`. `not_active` is what was set
621
+ # aside, so the numbers still reconcile with `adopt coverage recompute`.
622
+ evaluated = () if result is None else tuple(result.identities)
623
+ live = [row for row in evaluated if is_live(row)]
624
+ covered = sum(1 for row in live if row.covered)
553
625
  return {
554
- "identities": 0 if result is None else len(result.identities),
555
- "covered": 0 if result is None else result.covered,
556
- "uncovered": 0 if result is None else result.uncovered,
626
+ "identities": len(live),
627
+ "covered": covered,
628
+ "uncovered": len(live) - covered,
629
+ "not_active": len(evaluated) - len(live),
557
630
  "gaps": [
558
631
  {
559
632
  "kind": gap.kind,
@@ -959,6 +1032,7 @@ def _resolve_change(
959
1032
  target_identity_id=target_id,
960
1033
  target_uri=target_uri,
961
1034
  actor_id=actor,
1035
+ freshener=handle.changes(),
962
1036
  )
963
1037
  else:
964
1038
  outcome = confirm_current_item(
@@ -101,6 +101,17 @@ def open_configured_store(
101
101
  **capture** paths are refused separately, where they write canon.
102
102
  """
103
103
  path = configured_store_path(override)
104
+ if not path.exists():
105
+ # The library's own hint names `migrate=True`, an argument no operator can
106
+ # pass. Missing here means `init` has not run, or the command ran from
107
+ # somewhere other than the repository -- say which, and how to tell.
108
+ raise AdoptError(
109
+ ErrorCode.SCHEMA_MIGRATION_PENDING,
110
+ message=f"no store exists at {path}",
111
+ hint="Run `adopt init` in the repository first, or run this from the directory "
112
+ "that holds `.adopt/`, or name the store with --store or ADOPT_STORE_PATH. "
113
+ "`adopt doctor` shows which store path resolved and where it came from.",
114
+ )
104
115
  if not read_only and non_canon_reason is None:
105
116
  refuse_write_to_replica(path, verb=verb)
106
117
  return open_store(path, read_only=read_only)
@@ -1,6 +0,0 @@
1
- """Generated by scripts/embed_build_info.py; do not edit in a release build."""
2
-
3
- from typing import Final
4
-
5
- SBOM_SHA256: Final[str | None] = 'd9c82e3f0197e2d81b8ef92be4bdbb2f9f38710ee54d8db9ca3baa099047fda8'
6
- BUILD_ID: Final[str | None] = 'github:onboardux/onboard-core:37286165801:1:b50f2ec2a85b4271c0d445d8432b105574a8978a'
File without changes
File without changes
File without changes