boost-skill-cli 1.0.58__tar.gz → 1.0.60__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 (117) hide show
  1. boost_skill_cli-1.0.60/.gitattributes +4 -0
  2. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/CONTRIBUTING.md +14 -0
  3. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/PKG-INFO +1 -1
  4. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/_version.py +2 -2
  5. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/core/catalog.py +23 -1
  6. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_skill_cli.egg-info/PKG-INFO +1 -1
  7. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_skill_cli.egg-info/SOURCES.txt +2 -0
  8. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/scripts/build_registries.py +54 -7
  9. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/unit/test_catalog.py +26 -0
  10. boost_skill_cli-1.0.60/tests/unit/test_registries_fresh.py +49 -0
  11. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/.gitignore +0 -0
  12. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/CLAUDE.md +0 -0
  13. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/LICENSE +0 -0
  14. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/MANIFEST.in +0 -0
  15. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/Makefile +0 -0
  16. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/README.md +0 -0
  17. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/SECURITY.md +0 -0
  18. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost +0 -0
  19. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/__init__.py +0 -0
  20. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/__main__.py +0 -0
  21. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/cli.py +0 -0
  22. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/cliparse.py +0 -0
  23. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/commands/__init__.py +0 -0
  24. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/commands/bmad.py +0 -0
  25. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/commands/configuration.py +0 -0
  26. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/commands/discovery.py +0 -0
  27. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/commands/hooks.py +0 -0
  28. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/commands/info.py +0 -0
  29. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/commands/intelligence.py +0 -0
  30. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/commands/pkg.py +0 -0
  31. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/commands/quality.py +0 -0
  32. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/commands/taps.py +0 -0
  33. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/commands/team.py +0 -0
  34. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/core/__init__.py +0 -0
  35. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/core/agents.py +0 -0
  36. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/core/ai.py +0 -0
  37. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/core/claude_settings.py +0 -0
  38. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/core/config.py +0 -0
  39. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/core/dense.py +0 -0
  40. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/core/embed.py +0 -0
  41. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/core/frontmatter.py +0 -0
  42. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/core/gitutil.py +0 -0
  43. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/core/journal.py +0 -0
  44. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/core/lockfile.py +0 -0
  45. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/core/logs.py +0 -0
  46. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/core/mcp.py +0 -0
  47. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/core/output.py +0 -0
  48. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/core/paths.py +0 -0
  49. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/core/policy.py +0 -0
  50. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/core/rag.py +0 -0
  51. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/core/registry.py +0 -0
  52. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/core/store.py +0 -0
  53. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/core/util.py +0 -0
  54. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/data/registries.json +0 -0
  55. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/errors.py +0 -0
  56. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_cli/spin.py +0 -0
  57. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_skill_cli.egg-info/dependency_links.txt +0 -0
  58. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_skill_cli.egg-info/entry_points.txt +0 -0
  59. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_skill_cli.egg-info/requires.txt +0 -0
  60. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/boost_skill_cli.egg-info/top_level.txt +0 -0
  61. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/docs/.claude/settings.local.json +0 -0
  62. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/docs/DEBUGGING.md +0 -0
  63. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/docs/demo.gif +0 -0
  64. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/docs/demo.tape +0 -0
  65. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/docs/design-roadmap.html +0 -0
  66. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/docs/index.html +0 -0
  67. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/docs/mcp-hub.html +0 -0
  68. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/docs/rag-architecture.md +0 -0
  69. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/docs/roadmap.html +0 -0
  70. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/index.html +0 -0
  71. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/pyproject.toml +0 -0
  72. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/scripts/mutation_gate.py +0 -0
  73. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/setup.cfg +0 -0
  74. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/style/README.md +0 -0
  75. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/style/boost.css +0 -0
  76. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/style/boost.js +0 -0
  77. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/style/demo.html +0 -0
  78. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/conftest.py +0 -0
  79. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/functional/test_cli_bmad.py +0 -0
  80. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/functional/test_cli_configuration.py +0 -0
  81. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/functional/test_cli_discovery.py +0 -0
  82. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/functional/test_cli_hooks.py +0 -0
  83. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/functional/test_cli_info.py +0 -0
  84. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/functional/test_cli_intelligence.py +0 -0
  85. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/functional/test_cli_pkg.py +0 -0
  86. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/functional/test_cli_quality.py +0 -0
  87. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/functional/test_cli_taps.py +0 -0
  88. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/functional/test_cli_team.py +0 -0
  89. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/functional/test_everyday_loop.py +0 -0
  90. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/make_fixture.py +0 -0
  91. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/smoke.sh +0 -0
  92. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/unit/test_agents.py +0 -0
  93. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/unit/test_ai.py +0 -0
  94. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/unit/test_claude_settings.py +0 -0
  95. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/unit/test_cliparse.py +0 -0
  96. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/unit/test_config.py +0 -0
  97. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/unit/test_dense.py +0 -0
  98. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/unit/test_dense_fallback.py +0 -0
  99. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/unit/test_embed.py +0 -0
  100. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/unit/test_errors_and_cli_table.py +0 -0
  101. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/unit/test_frontmatter.py +0 -0
  102. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/unit/test_gitutil.py +0 -0
  103. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/unit/test_journal.py +0 -0
  104. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/unit/test_lockfile.py +0 -0
  105. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/unit/test_logs.py +0 -0
  106. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/unit/test_mcp.py +0 -0
  107. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/unit/test_mutation_hardening.py +0 -0
  108. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/unit/test_output.py +0 -0
  109. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/unit/test_paths.py +0 -0
  110. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/unit/test_policy.py +0 -0
  111. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/unit/test_rag.py +0 -0
  112. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/unit/test_registry.py +0 -0
  113. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/unit/test_spin.py +0 -0
  114. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/unit/test_store.py +0 -0
  115. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/unit/test_token_parity.py +0 -0
  116. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/unit/test_util.py +0 -0
  117. {boost_skill_cli-1.0.58 → boost_skill_cli-1.0.60}/tests/unit/test_version.py +0 -0
@@ -0,0 +1,4 @@
1
+ # Generated build outputs — never hand-edit; regenerate from source (see
2
+ # CONTRIBUTING.md "Generated files"). Marked linguist-generated so GitHub
3
+ # collapses their diffs and excludes them from language stats.
4
+ boost_cli/data/registries.json linguist-generated=true
@@ -33,6 +33,20 @@ agent configs.
33
33
  - Anything under `boost_cli/core/` is mutation-tested; expect to add unit
34
34
  tests that actually kill your mutants.
35
35
 
36
+ ## Generated files — never hand-edit
37
+
38
+ Some checked-in files are build outputs. Edit the source, regenerate, and commit
39
+ the result **as the final step before opening the PR** so the artifact never
40
+ drifts from its source (CI fails the build otherwise):
41
+
42
+ | Generated file | Source of truth | Regenerate with |
43
+ |---|---|---|
44
+ | `boost_cli/data/registries.json` | `SKILLS`/`RULES`/`WORKFLOWS` tuples in `scripts/build_registries.py` | `python3 scripts/build_registries.py` |
45
+
46
+ CI runs `python3 scripts/build_registries.py --check` and fails on drift; the
47
+ same guard lives in `tests/unit/test_registries_fresh.py`. Regenerating last also
48
+ keeps line-adjacent JSON conflicts between parallel branches rare.
49
+
36
50
  ## Pull requests
37
51
 
38
52
  Branch from `main`, keep PRs focused, and write the PR description in the
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boost-skill-cli
3
- Version: 1.0.58
3
+ Version: 1.0.60
4
4
  Summary: boost — Homebrew for AI coding skills
5
5
  Author: Jonathan Reyes
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -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.58'
22
- __version_tuple__ = version_tuple = (1, 0, 58)
21
+ __version__ = version = '1.0.60'
22
+ __version_tuple__ = version_tuple = (1, 0, 60)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -210,6 +210,28 @@ def resolve_one(name: str) -> dict:
210
210
  return matches[0]
211
211
 
212
212
 
213
+ def _meta_text(meta) -> str:
214
+ """Lowercased, space-joined flatten of a frontmatter dict for substring
215
+ search. Cheaper than ``json.dumps(meta).lower()`` — which built and threw
216
+ away a JSON string on every entry of every query just to substring-match —
217
+ while matching the same words (tags, keys, scalar values)."""
218
+ parts: List[str] = []
219
+
220
+ def walk(v) -> None:
221
+ if isinstance(v, dict):
222
+ for k, val in v.items():
223
+ parts.append(str(k))
224
+ walk(val)
225
+ elif isinstance(v, (list, tuple)):
226
+ for val in v:
227
+ walk(val)
228
+ elif v is not None:
229
+ parts.append(str(v))
230
+
231
+ walk(meta or {})
232
+ return " ".join(parts).lower()
233
+
234
+
213
235
  def search(query: str, entries: Optional[List[dict]] = None):
214
236
  """Rank entries against a query -> [(entry, score)] best-first."""
215
237
  entries = all_entries() if entries is None else entries
@@ -219,7 +241,7 @@ def search(query: str, entries: Optional[List[dict]] = None):
219
241
  for e in entries:
220
242
  name = e["name"].lower()
221
243
  desc = (e["description"] or "").lower()
222
- blob = " ".join([name, desc, json.dumps(e.get("meta", {})).lower()])
244
+ blob = " ".join([name, desc, _meta_text(e.get("meta", {}))])
223
245
  score = 0
224
246
  if q == name:
225
247
  score += 100
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boost-skill-cli
3
- Version: 1.0.58
3
+ Version: 1.0.60
4
4
  Summary: boost — Homebrew for AI coding skills
5
5
  Author: Jonathan Reyes
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -1,3 +1,4 @@
1
+ .gitattributes
1
2
  .gitignore
2
3
  CLAUDE.md
3
4
  CONTRIBUTING.md
@@ -106,6 +107,7 @@ tests/unit/test_output.py
106
107
  tests/unit/test_paths.py
107
108
  tests/unit/test_policy.py
108
109
  tests/unit/test_rag.py
110
+ tests/unit/test_registries_fresh.py
109
111
  tests/unit/test_registry.py
110
112
  tests/unit/test_spin.py
111
113
  tests/unit/test_store.py
@@ -6,13 +6,18 @@ entry is deduped by owner/repo, classified by type + category, and tagged with
6
6
  `list_only` when the repo is an awesome-list that mostly links out (so its
7
7
  scannable item count is far below its advertised total).
8
8
 
9
- Run: python3 scripts/build_registries.py
9
+ Run: python3 scripts/build_registries.py # regenerate the JSON
10
+ Verify: python3 scripts/build_registries.py --check # fail if it has drifted
10
11
  """
11
12
  from __future__ import annotations
12
13
 
14
+ import argparse
13
15
  import json
16
+ import sys
14
17
  from pathlib import Path
15
18
 
19
+ DEST = Path(__file__).resolve().parent.parent / "boost_cli" / "data" / "registries.json"
20
+
16
21
  # Repos that are index/awesome READMEs — they *point at* many items but ship
17
22
  # few or no scannable item files themselves. Kept for discovery, flagged so
18
23
  # count math stays honest.
@@ -163,7 +168,12 @@ def rows(pairs, typ):
163
168
  }
164
169
 
165
170
 
166
- def main() -> int:
171
+ def build_payload() -> dict:
172
+ """Assemble the registries payload from the SKILLS/RULES/WORKFLOWS tuples.
173
+
174
+ Pure function of the source tuples — no I/O — so both the writer and the
175
+ ``--check`` verifier render byte-for-byte identical output.
176
+ """
167
177
  by_name: dict = {}
168
178
  for entry in list(rows(SKILLS, "skill")) + list(rows(RULES, "rule")) + \
169
179
  list(rows(WORKFLOWS, "workflow")):
@@ -181,7 +191,7 @@ def main() -> int:
181
191
 
182
192
  entries = sorted(by_name.values(), key=lambda e: (e["type"], e["name"].lower()))
183
193
  scannable = [e for e in entries if not e["list_only"]]
184
- payload = {
194
+ return {
185
195
  "generated_note": "Curated skill/rule/workflow registries for boost. "
186
196
  "est_items are research estimates; verify by tapping. "
187
197
  "list_only repos are index/awesome lists that link out.",
@@ -190,10 +200,16 @@ def main() -> int:
190
200
  "est_items_scannable": sum(e["est_items"] for e in scannable),
191
201
  "registries": entries,
192
202
  }
193
- dest = Path(__file__).resolve().parent.parent / "boost_cli" / "data" / "registries.json"
194
- dest.parent.mkdir(parents=True, exist_ok=True)
195
- dest.write_text(json.dumps(payload, indent=2) + "\n")
196
- print("wrote %s" % dest)
203
+
204
+
205
+ def render(payload: dict) -> str:
206
+ """Serialize a payload to the exact on-disk representation."""
207
+ return json.dumps(payload, indent=2) + "\n"
208
+
209
+
210
+ def _summary(payload: dict) -> None:
211
+ entries = payload["registries"]
212
+ scannable = [e for e in entries if not e["list_only"]]
197
213
  print("registries: %d (scannable %d, list-only %d)"
198
214
  % (len(entries), len(scannable), len(entries) - len(scannable)))
199
215
  print("est items total: %d scannable: %d"
@@ -205,6 +221,37 @@ def main() -> int:
205
221
  by_type[e["type"]][1] += e["est_items"]
206
222
  for typ, (n, est) in sorted(by_type.items()):
207
223
  print(" %-9s %3d repos ~%d items" % (typ, n, est))
224
+
225
+
226
+ def main(argv: list[str] | None = None) -> int:
227
+ parser = argparse.ArgumentParser(description=__doc__)
228
+ parser.add_argument(
229
+ "--check", action="store_true",
230
+ help="verify the committed JSON matches a fresh build; exit 1 on drift "
231
+ "(does not write). Used in CI to catch un-regenerated edits.",
232
+ )
233
+ args = parser.parse_args(argv)
234
+
235
+ payload = build_payload()
236
+ fresh = render(payload)
237
+
238
+ if args.check:
239
+ current = DEST.read_text() if DEST.exists() else ""
240
+ if current != fresh:
241
+ print(
242
+ "ERROR: %s is out of date — regenerate it with\n"
243
+ " python3 scripts/build_registries.py\n"
244
+ "and commit the result (see CONTRIBUTING.md)." % DEST,
245
+ file=sys.stderr,
246
+ )
247
+ return 1
248
+ print("%s is up to date." % DEST)
249
+ return 0
250
+
251
+ DEST.parent.mkdir(parents=True, exist_ok=True)
252
+ DEST.write_text(fresh)
253
+ print("wrote %s" % DEST)
254
+ _summary(payload)
208
255
  return 0
209
256
 
210
257
 
@@ -443,6 +443,32 @@ class TestSearchRanking:
443
443
  res = catalog.search("security", entries=[e])
444
444
  assert [(m["name"], s) for m, s in res] == [("x", 2)]
445
445
 
446
+ def test_meta_key_still_matches(self):
447
+ # keys are part of the search text, just as json.dumps(meta) included
448
+ # them — a query for a frontmatter key name still scores.
449
+ e = _entry("x", "t", meta={"framework": "react"})
450
+ assert [(m["name"], s) for m, s in catalog.search("framework", entries=[e])] \
451
+ == [("x", 2)]
452
+
453
+ def test_meta_nested_value_matches(self):
454
+ e = _entry("x", "t", meta={"requires": [{"skill": "planning"}]})
455
+ assert [(m["name"], s) for m, s in catalog.search("planning", entries=[e])] \
456
+ == [("x", 2)]
457
+
458
+
459
+ class TestMetaText:
460
+ def test_flattens_keys_and_values_lowercased(self):
461
+ assert catalog._meta_text({"Tags": ["AI", "ML"]}) == "tags ai ml"
462
+
463
+ def test_nested_dicts_and_lists(self):
464
+ out = catalog._meta_text({"a": {"b": [1, "Two"]}})
465
+ assert out == "a b 1 two"
466
+
467
+ def test_skips_none_and_empty(self):
468
+ assert catalog._meta_text({}) == ""
469
+ assert catalog._meta_text(None) == ""
470
+ assert catalog._meta_text({"k": None}) == "k"
471
+
446
472
  def test_description_only_match(self):
447
473
  e = _entry("styler", "t", desc="handles jira tickets")
448
474
  res = catalog.search("jira", entries=[e])
@@ -0,0 +1,49 @@
1
+ """Committed registries.json stays in lockstep with its generator.
2
+
3
+ ``boost_cli/data/registries.json`` is a generated artifact (source of truth is
4
+ the SKILLS/RULES/WORKFLOWS tuples in ``scripts/build_registries.py``). If a repo
5
+ row is edited without regenerating, the committed JSON drifts. This test — the
6
+ in-suite twin of the ``build_registries.py --check`` CI step — fails on drift so
7
+ a stale artifact can't merge.
8
+
9
+ Skips when the repo-root files aren't reachable (e.g. the mutation sandbox,
10
+ which only copies ``boost_cli/``).
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import importlib.util
15
+ from pathlib import Path
16
+
17
+ import pytest
18
+
19
+ _ROOT = Path(__file__).resolve().parents[2]
20
+ _SCRIPT = _ROOT / "scripts" / "build_registries.py"
21
+ _JSON = _ROOT / "boost_cli" / "data" / "registries.json"
22
+
23
+
24
+ def _load_builder():
25
+ spec = importlib.util.spec_from_file_location("build_registries", _SCRIPT)
26
+ assert spec and spec.loader
27
+ module = importlib.util.module_from_spec(spec)
28
+ spec.loader.exec_module(module)
29
+ return module
30
+
31
+
32
+ @pytest.mark.skipif(not (_SCRIPT.exists() and _JSON.exists()),
33
+ reason="repo-root files not reachable (e.g. mutation sandbox)")
34
+ def test_registries_json_is_regenerated():
35
+ builder = _load_builder()
36
+ fresh = builder.render(builder.build_payload())
37
+ committed = _JSON.read_text()
38
+ assert committed == fresh, (
39
+ "boost_cli/data/registries.json is out of date — regenerate with\n"
40
+ " python3 scripts/build_registries.py\n"
41
+ "and commit the result (see CONTRIBUTING.md)."
42
+ )
43
+
44
+
45
+ @pytest.mark.skipif(not _SCRIPT.exists(),
46
+ reason="repo-root files not reachable (e.g. mutation sandbox)")
47
+ def test_check_flag_passes_on_fresh_tree():
48
+ builder = _load_builder()
49
+ assert builder.main(["--check"]) == 0
File without changes