boost-skill-cli 1.0.332__py3-none-any.whl → 1.0.334__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.
boost_cli/_version.py CHANGED
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '1.0.332'
22
- __version_tuple__ = version_tuple = (1, 0, 332)
21
+ __version__ = version = '1.0.334'
22
+ __version_tuple__ = version_tuple = (1, 0, 334)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -194,6 +194,38 @@ def _hint_semantic_search(engine: str) -> None:
194
194
  out.info(out.role(line, "muted"))
195
195
 
196
196
 
197
+ def _shard_io(args) -> int:
198
+ """`--export-shard` / `--import-shard`, the two halves of a prebuilt shard.
199
+
200
+ Embedding is ~1.2 s/chunk on CPU — 74 minutes for 743 entries — so without
201
+ a way to move vectors between machines the keyless tier is available in
202
+ principle and unreachable in practice. Import validates rather than trusts;
203
+ see `dense.import_shard` for why a mismatched shard must be refused instead
204
+ of merged.
205
+ """
206
+ from ..core import dense
207
+ if args.export_shard:
208
+ shard = dense.export_shard(args.export_shard)
209
+ if not shard.get("chunks"):
210
+ raise BoostError("no vectors for %r" % args.export_shard,
211
+ hint="build them first with `boost reindex --dense`")
212
+ print(json.dumps(shard))
213
+ return 0
214
+ path = Path(args.import_shard)
215
+ try:
216
+ shard = json.loads(path.read_text(encoding="utf-8"))
217
+ except (OSError, json.JSONDecodeError) as exc:
218
+ raise BoostError("cannot read shard %s: %s" % (path, exc)) from exc
219
+ tap = str(shard.get("tap") or "")
220
+ commit = rag._tap_commits().get(tap.replace("/", "__"), "")
221
+ ok, reason = dense.import_shard(shard, commit=commit)
222
+ if not ok:
223
+ raise BoostError("shard refused — %s" % reason,
224
+ hint="`boost reindex --dense` embeds it locally instead")
225
+ out.ok("%s: %s" % (tap, reason))
226
+ return 0
227
+
228
+
197
229
  def cmd_reindex(argv):
198
230
  """Build/refresh the full-content (RAG) search index over tapped items."""
199
231
  p = cliparse.parser(
@@ -203,10 +235,19 @@ def cmd_reindex(argv):
203
235
  help="reindex every tap, ignoring cached commits")
204
236
  p.add_argument("--dense", action="store_true",
205
237
  help="also embed chunks into the opt-in dense vector store "
206
- "(needs the `rag` extra and an embeddings API key)")
238
+ "(needs the `rag` extra; no API key required)")
239
+ p.add_argument("--export-shard", metavar="TAP",
240
+ help="write TAP's prebuilt vectors to stdout as JSON, for "
241
+ "publishing so others need not re-embed them")
242
+ p.add_argument("--import-shard", metavar="FILE",
243
+ help="merge a prebuilt vector shard, skipping the embed "
244
+ "cost; refused unless it matches this store's backend "
245
+ "and the tap's current commit")
207
246
  p.add_argument("--json", action="store_true", dest="as_json",
208
247
  help="machine-readable output")
209
248
  args = p.parse_args(argv)
249
+ if args.export_shard or args.import_shard:
250
+ return _shard_io(args)
210
251
  if not registry.list_taps():
211
252
  raise BoostError("no taps configured — nothing to index",
212
253
  hint="add the recommended registries with `boost tap --defaults`")
boost_cli/core/dense.py CHANGED
@@ -12,6 +12,7 @@ are the expensive step, so an unchanged tap is never re-embedded.
12
12
  """
13
13
  from __future__ import annotations
14
14
 
15
+ import base64
15
16
  import json
16
17
  import sqlite3
17
18
  from pathlib import Path
@@ -345,6 +346,116 @@ def build(entries: Optional[List[dict]] = None,
345
346
  con.close()
346
347
 
347
348
 
349
+ def export_shard(tap: str) -> dict:
350
+ """One tap's vectors plus the provenance needed to validate them later.
351
+
352
+ Embedding is the expensive half of the keyless tier — measured at ~1.2 s per
353
+ chunk on CPU, so 74 minutes for 743 entries — while querying is milliseconds.
354
+ A shard lets that cost be paid once in CI and downloaded by everyone else.
355
+
356
+ The provenance fields are not decoration: vectors are only comparable inside
357
+ the embedding space that produced them, so provider/model/dim have to travel
358
+ with the rows, and the registry commit has to travel too or a stale shard
359
+ would be indistinguishable from a current one.
360
+ """
361
+ # Plain sqlite3, NOT _connect: exporting reads `chunks`, `meta` and the
362
+ # stored embedding blobs, all ordinary tables. Routing through _connect
363
+ # would make export impossible without the sqlite-vec extension — the same
364
+ # trap `_recorded_meta` documents — and a machine that built vectors and
365
+ # then dropped the extra is exactly the one whose shard is worth having.
366
+ if not db_path().exists():
367
+ return {"tap": tap, "chunks": []}
368
+ try:
369
+ con = sqlite3.connect(str(db_path()))
370
+ except sqlite3.Error:
371
+ return {"tap": tap, "chunks": []}
372
+ try:
373
+ meta = _read_meta(con)
374
+ commits = meta.get("commits")
375
+ commit = ""
376
+ if isinstance(commits, dict):
377
+ commit = str(commits.get(tap.replace("/", "__")) or "")
378
+ chunks = []
379
+ for row in con.execute(
380
+ "SELECT c.name, c.tap, c.path, c.kind, c.cix, c.snip, v.embedding "
381
+ "FROM chunks c JOIN vec_chunks v ON v.rowid = c.id "
382
+ "WHERE c.tap = ? ORDER BY c.id", (tap,)):
383
+ name, ctap, path, kind, cix, snip, emb = row
384
+ chunks.append({
385
+ "name": name, "tap": ctap, "path": path, "kind": kind,
386
+ "cix": cix, "snip": snip,
387
+ # base64 so the shard is plain JSON and can be published as a
388
+ # release artifact without a binary format of its own.
389
+ "embedding": base64.b64encode(bytes(emb)).decode("ascii"),
390
+ })
391
+ return {"tap": tap, "commit": commit,
392
+ "provider": meta.get("provider"), "model": meta.get("model"),
393
+ "dim": meta.get("dim"), "version": INDEX_VERSION,
394
+ "chunks": chunks}
395
+ except sqlite3.Error:
396
+ return {"tap": tap, "chunks": []}
397
+ finally:
398
+ con.close()
399
+
400
+
401
+ def import_shard(shard: dict, commit: str) -> Tuple[bool, str]:
402
+ """Merge a prebuilt shard into this machine's store. ``(ok, reason)``.
403
+
404
+ Refuses rather than degrades. A vector is only meaningful against others
405
+ from the same embedding space, so a shard from a different provider, model
406
+ or dimension cannot be mixed in: doing so would not raise, it would quietly
407
+ return nonsense rankings, which is the worse failure. A shard whose commit
408
+ does not match the tap as it stands now is refused for a different reason —
409
+ accepting it would let `build()` mark that tap "reused" and never re-embed
410
+ it, pinning the user to stale vectors indefinitely.
411
+ """
412
+ for field in ("provider", "model", "dim"):
413
+ if shard.get(field) in (None, ""):
414
+ return False, "shard is missing %s" % field
415
+ if str(shard.get("commit") or "") != commit:
416
+ return False, ("commit mismatch: shard %r, tap %r"
417
+ % (shard.get("commit"), commit))
418
+ con = _connect()
419
+ if con is None:
420
+ return False, "no vector backend available"
421
+ try:
422
+ meta = _read_meta(con)
423
+ # An empty store has no opinion yet, so it adopts the shard's backend.
424
+ if meta.get("provider"):
425
+ for field in ("provider", "model", "dim"):
426
+ if meta.get(field) != shard.get(field):
427
+ return False, ("%s mismatch: store %r, shard %r"
428
+ % (field, meta.get(field), shard.get(field)))
429
+ dim = int(shard["dim"])
430
+ _ensure_schema(con, dim)
431
+ # Replace, never append: re-importing must not double a tap's rows.
432
+ _delete_taps(con, [str(shard.get("tap") or "")])
433
+ mod = _load()
434
+ for c in shard.get("chunks") or []:
435
+ cur = con.execute(
436
+ "INSERT INTO chunks (name, tap, path, kind, cix, snip) "
437
+ "VALUES (?, ?, ?, ?, ?, ?)",
438
+ (c.get("name"), c.get("tap"), c.get("path"), c.get("kind"),
439
+ c.get("cix"), c.get("snip")))
440
+ blob = base64.b64decode(c["embedding"])
441
+ con.execute("INSERT INTO vec_chunks (rowid, embedding) VALUES (?, ?)",
442
+ (cur.lastrowid, blob))
443
+ commits = meta.get("commits")
444
+ commits = dict(commits) if isinstance(commits, dict) else {}
445
+ commits[str(shard.get("tap") or "").replace("/", "__")] = commit
446
+ _write_meta(con, {"version": INDEX_VERSION,
447
+ "provider": shard.get("provider"),
448
+ "model": shard.get("model"), "dim": dim,
449
+ "commits": commits})
450
+ con.commit()
451
+ _ = mod # extension loaded by _connect; kept for symmetry
452
+ return True, "imported %d chunks" % len(shard.get("chunks") or [])
453
+ except sqlite3.Error as exc:
454
+ return False, "store error: %s" % exc
455
+ finally:
456
+ con.close()
457
+
458
+
348
459
  def _indexed_taps(con: sqlite3.Connection) -> set:
349
460
  """Every tap name the vector index currently holds chunks for."""
350
461
  try:
boost_cli/core/rag.py CHANGED
@@ -219,17 +219,33 @@ def _make_docs(entries: List[dict], tap_paths: Dict[str, Path]) -> List[dict]:
219
219
 
220
220
 
221
221
  def _postings_to_doc_tf(raw: dict) -> Dict[int, Dict[str, int]]:
222
- """Invert persisted postings back to per-doc term frequencies."""
222
+ """Invert persisted postings back to per-doc term frequencies.
223
+
224
+ Pure: it inverts whatever ``raw`` carries and reads nothing. An earlier
225
+ version fell back to the SQLite store with ``or``, which made a caller
226
+ passing ``{}`` silently receive tens of thousands of postings from the live
227
+ index — invisible in CI, where no store exists, and wrong everywhere else.
228
+ Callers that need the store now say so; see :func:`_kept_docs`.
229
+ """
223
230
  doc_tf: Dict[int, Dict[str, int]] = defaultdict(dict)
224
- for term, plist in (raw.get("postings") or _all_postings()).items():
231
+ for term, plist in (raw.get("postings") or {}).items():
225
232
  for doc_id, tf in plist:
226
233
  doc_tf[doc_id][term] = tf
227
234
  return doc_tf
228
235
 
229
236
 
230
237
  def _kept_docs(raw: dict, keep_safe: set) -> List[dict]:
231
- """Recover cached docs (with term freqs) for taps that did not change."""
232
- doc_tf = _postings_to_doc_tf(raw)
238
+ """Recover cached docs (with term freqs) for taps that did not change.
239
+
240
+ Term frequencies live in the SQLite postings store since they left the JSON
241
+ index, so an index without an inline ``postings`` key is normal rather than
242
+ empty — this is the caller that reaches for the store, explicitly.
243
+ """
244
+ # Key ABSENCE, not falsiness: an index carrying an explicit empty
245
+ # `postings` is stating it has none, and must not be answered from the
246
+ # store. Only an index with no such key predates the SQLite move.
247
+ doc_tf = _postings_to_doc_tf(
248
+ raw if "postings" in raw else {"postings": _all_postings()})
233
249
  return [{**meta, "tf": doc_tf.get(doc_id, {})}
234
250
  for doc_id, meta in enumerate(raw.get("docs", []))
235
251
  if meta["t"].replace("/", "__") in keep_safe]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boost-skill-cli
3
- Version: 1.0.332
3
+ Version: 1.0.334
4
4
  Summary: boost — Homebrew for AI coding skills
5
5
  Author: Jonathan Reyes
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -1,6 +1,6 @@
1
1
  boost_cli/__init__.py,sha256=e93dOJMl_tHyLowLz55AKh4ts4mzBiLdAezQcKIU3XU,1196
2
2
  boost_cli/__main__.py,sha256=40c3JGxDOKeEEdfKw5o6A8E_rhK9fFv8AYlmd-ngtcI,119
3
- boost_cli/_version.py,sha256=wO0IQXH4-Dp8_deWL43Rd61Ow1CrbbyoiHwdv8568Qc,524
3
+ boost_cli/_version.py,sha256=HAkpyNl7P13QMLknzYBKsKEpdzqP5XJchWEGYPR5o7s,524
4
4
  boost_cli/cli.py,sha256=qjDgTkvrTbaqgc9LApvDtHENkewrzN3XT8TU3UloKh0,14071
5
5
  boost_cli/cliparse.py,sha256=LTm5giUv9EMpCHkP7pkQZrJufurJjxH51ZAPlL62rm4,1156
6
6
  boost_cli/errors.py,sha256=hrrbdy5MMlPo_N_BztyjKKRdOY3CyoNR7NoZbpeDoyI,319
@@ -9,7 +9,7 @@ boost_cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuF
9
9
  boost_cli/commands/_common.py,sha256=15IIIC-UoX3kImYjmv_k7Xjgiq51MGcxlGEM_It-LZY,1053
10
10
  boost_cli/commands/bmad.py,sha256=0o5L_WyEMDvQfMBER719NnjYi9X5AFEvTH-ISFVRIeA,14617
11
11
  boost_cli/commands/configuration.py,sha256=jKw5gbXbYpke0J6SHuCpuwvlHDokVVIiQPRt5wsqBU8,48584
12
- boost_cli/commands/discovery.py,sha256=7pieTNpx05oKd-VxbL20HklK8e4Q0Ul6gGtYfzgPH90,46311
12
+ boost_cli/commands/discovery.py,sha256=huSkDUGOXTd3G0IMmCxtu2_KjGQtSv_G3ygF1YYJOxE,48249
13
13
  boost_cli/commands/hooks.py,sha256=3B-vBEQtPkZNNdFzTlhOXwYDwZ8oA4fi_uzOBhoKq3M,3709
14
14
  boost_cli/commands/info.py,sha256=ZUvum7AOSW6REuMl6znIVNj17uEkJIR3p8xACIZRSH0,33446
15
15
  boost_cli/commands/intelligence.py,sha256=ckiOTXt50JeNU5Iy4jFWq04hcgBWHnSA3jYD3nVJhKY,41361
@@ -27,7 +27,7 @@ boost_cli/core/capabilities.py,sha256=P37nPwWA9VLxHmydrNhLcC5Yql-goNBiPtPywpViJZ
27
27
  boost_cli/core/catalog.py,sha256=IdyqK__IjjlO4knb4DTAUzv02ySNb7MlQShga8DC3Zs,15362
28
28
  boost_cli/core/claude_settings.py,sha256=vyBEHzvBDdmAAjLGAl2HM3eyy2XgfjWEyRbdhI0yKuY,7203
29
29
  boost_cli/core/config.py,sha256=tq6cgwx1yP7N90S5DniBjCRCQ1mvRk3fImBrZXBLt04,7782
30
- boost_cli/core/dense.py,sha256=UbOzPlMhn8ZO8-FQt69QHxkYVCn_psgw01DTzTBrsHU,18911
30
+ boost_cli/core/dense.py,sha256=rxr7g-EW31vO4YZ3wN74Mvp4D26TgOGUk0hBXwSXK6c,24306
31
31
  boost_cli/core/ed25519.py,sha256=bpYtV3Nah54JPVHC7h2JWqlEi1P7-hbEvQBKfnuxmgI,4356
32
32
  boost_cli/core/embed.py,sha256=e0ARIJHKkT_bl40ZqCjpgq2puIoJ1qBW0DEqltLusuQ,10127
33
33
  boost_cli/core/faithfulness.py,sha256=hVFyyvfdbrFmTLzQw1k95r8XkVZ04y2hU9QsEBMmuCk,5025
@@ -51,7 +51,7 @@ boost_cli/core/paths.py,sha256=2mxatbdoAjhYfqZG93AsiQpqA2bkWYejIqlblLt-txQ,5056
51
51
  boost_cli/core/policy.py,sha256=yhcdJMB_oiViJPzj3rbI-AO1RFJusflT_oDqv9s-wgY,4902
52
52
  boost_cli/core/projectlock.py,sha256=ca2F6BGQ86WQtbpnwIgVFGC_eX6yr7aI47Ha9-VkR2g,4145
53
53
  boost_cli/core/provenance.py,sha256=19yRALM-Qw8YrA-Ih_ifXw1BFFAPDpn_I2OdT1okmQc,6406
54
- boost_cli/core/rag.py,sha256=SNNvFmlMeGN2ZS7aqTndQa6S4sZMFL_dJwjE9AVQWtg,34158
54
+ boost_cli/core/rag.py,sha256=mBGo6LAnX2_rfAE4HtKnyLuWaEABGhsCojvBnV6vA0M,35050
55
55
  boost_cli/core/registry.py,sha256=Y7ACBYfzhL4u4F3XfR9_kaGE1NvI-BidoEpgqhNzh0c,6236
56
56
  boost_cli/core/resolve.py,sha256=CgID8mLp7ND6r3wDPW5C_9vF8d-MiMJDfs5yrezjiGo,5949
57
57
  boost_cli/core/rules.py,sha256=x1QT1Egtbqwdb0yKvBnsSku4T6OfUDLiC_7-lKzI2RI,6541
@@ -68,9 +68,9 @@ boost_cli/core/updatediff.py,sha256=l75sqZOl3XjZUMaDxdVexZMix5Scpc7KXfhHZbtWNig,
68
68
  boost_cli/core/util.py,sha256=4zKmrfwzbn7hM08u1uxXi7bIOFJUPWMxTtAATtMMgYA,11636
69
69
  boost_cli/core/workflows.py,sha256=Ri1ieUWzHxPaSqFHHzLLTPqlfMlWxDg_ML3Qxz5XFeU,6761
70
70
  boost_cli/data/registries.json,sha256=PZaYT1ptBmn8U3eaO-neU9g0Idmu2BlqSSiMegYpfbE,167072
71
- boost_skill_cli-1.0.332.dist-info/licenses/LICENSE,sha256=U7k4Yv7aA3f8h2SW-CEvTi_kaucMxK5hXrUJjPXKjhs,35102
72
- boost_skill_cli-1.0.332.dist-info/METADATA,sha256=tiV70gYf25Uc5FXcicXabqBk_5bBDaJb9uXELi5sBpM,64715
73
- boost_skill_cli-1.0.332.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
74
- boost_skill_cli-1.0.332.dist-info/entry_points.txt,sha256=PqQTs8HnTuDi8oeUyaAryqwhBWukqBNwYnEkgR6YzJw,45
75
- boost_skill_cli-1.0.332.dist-info/top_level.txt,sha256=RMTlws4vuWbyR6Xg8qEgPsS8xz9-ZYEK3RZ2_pOUKww,10
76
- boost_skill_cli-1.0.332.dist-info/RECORD,,
71
+ boost_skill_cli-1.0.334.dist-info/licenses/LICENSE,sha256=U7k4Yv7aA3f8h2SW-CEvTi_kaucMxK5hXrUJjPXKjhs,35102
72
+ boost_skill_cli-1.0.334.dist-info/METADATA,sha256=oESmfneD6tTuQ8Foaa2KhGC2vjdV70OcS2SFIRpydkY,64715
73
+ boost_skill_cli-1.0.334.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
74
+ boost_skill_cli-1.0.334.dist-info/entry_points.txt,sha256=PqQTs8HnTuDi8oeUyaAryqwhBWukqBNwYnEkgR6YzJw,45
75
+ boost_skill_cli-1.0.334.dist-info/top_level.txt,sha256=RMTlws4vuWbyR6Xg8qEgPsS8xz9-ZYEK3RZ2_pOUKww,10
76
+ boost_skill_cli-1.0.334.dist-info/RECORD,,