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 +2 -2
- boost_cli/commands/discovery.py +42 -1
- boost_cli/core/dense.py +111 -0
- boost_cli/core/rag.py +20 -4
- {boost_skill_cli-1.0.332.dist-info → boost_skill_cli-1.0.334.dist-info}/METADATA +1 -1
- {boost_skill_cli-1.0.332.dist-info → boost_skill_cli-1.0.334.dist-info}/RECORD +10 -10
- {boost_skill_cli-1.0.332.dist-info → boost_skill_cli-1.0.334.dist-info}/WHEEL +0 -0
- {boost_skill_cli-1.0.332.dist-info → boost_skill_cli-1.0.334.dist-info}/entry_points.txt +0 -0
- {boost_skill_cli-1.0.332.dist-info → boost_skill_cli-1.0.334.dist-info}/licenses/LICENSE +0 -0
- {boost_skill_cli-1.0.332.dist-info → boost_skill_cli-1.0.334.dist-info}/top_level.txt +0 -0
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.
|
|
22
|
-
__version_tuple__ = version_tuple = (1, 0,
|
|
21
|
+
__version__ = version = '1.0.334'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 0, 334)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
boost_cli/commands/discovery.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
72
|
-
boost_skill_cli-1.0.
|
|
73
|
-
boost_skill_cli-1.0.
|
|
74
|
-
boost_skill_cli-1.0.
|
|
75
|
-
boost_skill_cli-1.0.
|
|
76
|
-
boost_skill_cli-1.0.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|