boost-skill-cli 1.0.38__tar.gz → 1.0.40__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 (109) hide show
  1. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/PKG-INFO +1 -1
  2. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/_version.py +2 -2
  3. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/commands/discovery.py +3 -2
  4. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/commands/info.py +7 -3
  5. boost_skill_cli-1.0.40/boost_cli/spin.py +56 -0
  6. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_skill_cli.egg-info/PKG-INFO +1 -1
  7. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_skill_cli.egg-info/SOURCES.txt +2 -0
  8. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/docs/design-roadmap.html +1 -1
  9. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/docs/roadmap.html +141 -1
  10. boost_skill_cli-1.0.40/tests/unit/test_spin.py +71 -0
  11. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/.gitignore +0 -0
  12. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/CLAUDE.md +0 -0
  13. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/CONTRIBUTING.md +0 -0
  14. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/LICENSE +0 -0
  15. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/MANIFEST.in +0 -0
  16. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/Makefile +0 -0
  17. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/README.md +0 -0
  18. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/SECURITY.md +0 -0
  19. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost +0 -0
  20. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/__init__.py +0 -0
  21. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/__main__.py +0 -0
  22. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/cli.py +0 -0
  23. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/cliparse.py +0 -0
  24. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/commands/__init__.py +0 -0
  25. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/commands/bmad.py +0 -0
  26. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/commands/configuration.py +0 -0
  27. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/commands/hooks.py +0 -0
  28. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/commands/intelligence.py +0 -0
  29. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/commands/pkg.py +0 -0
  30. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/commands/quality.py +0 -0
  31. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/commands/taps.py +0 -0
  32. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/commands/team.py +0 -0
  33. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/core/__init__.py +0 -0
  34. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/core/agents.py +0 -0
  35. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/core/ai.py +0 -0
  36. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/core/catalog.py +0 -0
  37. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/core/claude_settings.py +0 -0
  38. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/core/config.py +0 -0
  39. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/core/dense.py +0 -0
  40. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/core/embed.py +0 -0
  41. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/core/frontmatter.py +0 -0
  42. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/core/gitutil.py +0 -0
  43. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/core/journal.py +0 -0
  44. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/core/lockfile.py +0 -0
  45. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/core/logs.py +0 -0
  46. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/core/output.py +0 -0
  47. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/core/paths.py +0 -0
  48. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/core/policy.py +0 -0
  49. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/core/rag.py +0 -0
  50. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/core/registry.py +0 -0
  51. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/core/store.py +0 -0
  52. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/core/util.py +0 -0
  53. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/data/registries.json +0 -0
  54. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_cli/errors.py +0 -0
  55. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_skill_cli.egg-info/dependency_links.txt +0 -0
  56. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_skill_cli.egg-info/entry_points.txt +0 -0
  57. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_skill_cli.egg-info/requires.txt +0 -0
  58. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/boost_skill_cli.egg-info/top_level.txt +0 -0
  59. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/docs/.claude/settings.local.json +0 -0
  60. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/docs/DEBUGGING.md +0 -0
  61. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/docs/demo.gif +0 -0
  62. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/docs/demo.tape +0 -0
  63. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/docs/index.html +0 -0
  64. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/docs/rag-architecture.md +0 -0
  65. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/pyproject.toml +0 -0
  66. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/scripts/build_registries.py +0 -0
  67. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/scripts/mutation_gate.py +0 -0
  68. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/setup.cfg +0 -0
  69. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/style/README.md +0 -0
  70. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/style/boost.css +0 -0
  71. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/style/boost.js +0 -0
  72. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/style/demo.html +0 -0
  73. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/conftest.py +0 -0
  74. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/functional/test_cli_bmad.py +0 -0
  75. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/functional/test_cli_configuration.py +0 -0
  76. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/functional/test_cli_discovery.py +0 -0
  77. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/functional/test_cli_hooks.py +0 -0
  78. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/functional/test_cli_info.py +0 -0
  79. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/functional/test_cli_intelligence.py +0 -0
  80. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/functional/test_cli_pkg.py +0 -0
  81. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/functional/test_cli_quality.py +0 -0
  82. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/functional/test_cli_taps.py +0 -0
  83. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/functional/test_cli_team.py +0 -0
  84. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/functional/test_everyday_loop.py +0 -0
  85. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/make_fixture.py +0 -0
  86. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/smoke.sh +0 -0
  87. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/unit/test_agents.py +0 -0
  88. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/unit/test_ai.py +0 -0
  89. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/unit/test_catalog.py +0 -0
  90. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/unit/test_claude_settings.py +0 -0
  91. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/unit/test_cliparse.py +0 -0
  92. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/unit/test_config.py +0 -0
  93. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/unit/test_dense.py +0 -0
  94. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/unit/test_embed.py +0 -0
  95. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/unit/test_errors_and_cli_table.py +0 -0
  96. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/unit/test_frontmatter.py +0 -0
  97. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/unit/test_gitutil.py +0 -0
  98. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/unit/test_journal.py +0 -0
  99. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/unit/test_lockfile.py +0 -0
  100. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/unit/test_logs.py +0 -0
  101. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/unit/test_mutation_hardening.py +0 -0
  102. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/unit/test_output.py +0 -0
  103. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/unit/test_paths.py +0 -0
  104. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/unit/test_policy.py +0 -0
  105. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/unit/test_rag.py +0 -0
  106. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/unit/test_registry.py +0 -0
  107. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/unit/test_store.py +0 -0
  108. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/unit/test_util.py +0 -0
  109. {boost_skill_cli-1.0.38 → boost_skill_cli-1.0.40}/tests/unit/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boost-skill-cli
3
- Version: 1.0.38
3
+ Version: 1.0.40
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.38'
22
- __version_tuple__ = version_tuple = (1, 0, 38)
21
+ __version__ = version = '1.0.40'
22
+ __version_tuple__ = version_tuple = (1, 0, 40)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -15,7 +15,7 @@ import sys
15
15
  import time
16
16
  from pathlib import Path
17
17
 
18
- from .. import cliparse
18
+ from .. import cliparse, spin
19
19
  from ..core import agents, ai, catalog, dense, embed, gitutil, journal, lockfile, paths, rag, registry, store, util
20
20
  from ..core import output as out
21
21
  from ..errors import BoostError
@@ -204,7 +204,8 @@ def cmd_search(argv):
204
204
  ranker = "full-content BM25" if use_rag else "heuristic relevance"
205
205
  if args.smart:
206
206
  if ai.available():
207
- reranked = _ai_rank(query, scored)
207
+ with spin.Spinner("ranking %d matches with Claude" % len(scored)):
208
+ reranked = _ai_rank(query, scored)
208
209
  if reranked:
209
210
  scored, ranker = reranked, "Claude Haiku relevance"
210
211
  else:
@@ -123,9 +123,13 @@ def cmd_list(argv):
123
123
  rows = []
124
124
  for name in sorted(skills):
125
125
  e = skills[name]
126
- flags = (["pinned"] if e.get("pinned") else []) + \
127
- (["quarantined"] if e.get("quarantined") else []) + \
128
- ["#" + t for t in e.get("tags") or []]
126
+ # Aurora-tinted flags — now that table() aligns by visible width,
127
+ # colored cells stay in their column: pinned amber, quarantined pink,
128
+ # tags dim.
129
+ flags = ([out.aurora("pinned", "yellow")] if e.get("pinned") else []) + \
130
+ ([out.aurora("quarantined", "pink")] if e.get("quarantined")
131
+ else []) + \
132
+ [out.c("#" + t, out.DIM) for t in e.get("tags") or []]
129
133
  rows.append((name, e.get("version", "?"), e.get("tap", "?"),
130
134
  "·".join(a.split("-")[0] for a in e.get("agents") or []),
131
135
  " ".join(flags)))
@@ -0,0 +1,56 @@
1
+ """A braille spinner for indeterminate waits (network calls, AI reranking…).
2
+
3
+ Deliberately silent — no thread, no output — whenever the stream isn't an
4
+ interactive TTY or color is off, so piped output, CI logs and captured test
5
+ output stay clean. Usage:
6
+
7
+ with spin.Spinner("ranking with Claude"):
8
+ result = slow_network_call()
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import itertools
13
+ import sys
14
+ import threading
15
+ from typing import Optional
16
+
17
+ from .core import output as out
18
+
19
+ _FRAMES = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
20
+ _INTERVAL = 0.08
21
+
22
+
23
+ class Spinner:
24
+ def __init__(self, label: str = "", stream=None):
25
+ self.label = label
26
+ self.stream = stream if stream is not None else sys.stderr
27
+ self._stop = threading.Event()
28
+ self._thread: Optional[threading.Thread] = None
29
+
30
+ def active(self) -> bool:
31
+ """Animate only on an interactive, color-capable TTY."""
32
+ s = self.stream
33
+ return (out.color_level(s) > 0
34
+ and hasattr(s, "isatty") and s.isatty())
35
+
36
+ def __enter__(self) -> "Spinner":
37
+ if self.active():
38
+ self._thread = threading.Thread(target=self._spin, daemon=True)
39
+ self._thread.start()
40
+ return self
41
+
42
+ def _spin(self) -> None:
43
+ for frame in itertools.cycle(_FRAMES):
44
+ if self._stop.is_set():
45
+ break
46
+ self.stream.write("\r" + out.aurora(frame, "cyan") + " " + self.label)
47
+ self.stream.flush()
48
+ self._stop.wait(_INTERVAL)
49
+
50
+ def __exit__(self, *exc) -> None:
51
+ self._stop.set()
52
+ if self._thread is not None:
53
+ self._thread.join(timeout=1)
54
+ if self.active():
55
+ self.stream.write("\r" + " " * (out.visible_len(self.label) + 2) + "\r")
56
+ self.stream.flush()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boost-skill-cli
3
- Version: 1.0.38
3
+ Version: 1.0.40
4
4
  Summary: boost — Homebrew for AI coding skills
5
5
  Author: Jonathan Reyes
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -15,6 +15,7 @@ boost_cli/_version.py
15
15
  boost_cli/cli.py
16
16
  boost_cli/cliparse.py
17
17
  boost_cli/errors.py
18
+ boost_cli/spin.py
18
19
  boost_cli/commands/__init__.py
19
20
  boost_cli/commands/bmad.py
20
21
  boost_cli/commands/configuration.py
@@ -101,6 +102,7 @@ tests/unit/test_paths.py
101
102
  tests/unit/test_policy.py
102
103
  tests/unit/test_rag.py
103
104
  tests/unit/test_registry.py
105
+ tests/unit/test_spin.py
104
106
  tests/unit/test_store.py
105
107
  tests/unit/test_util.py
106
108
  tests/unit/test_version.py
@@ -349,7 +349,7 @@
349
349
  <div class="rcards">
350
350
 
351
351
  <article class="ritem" data-track="motion" data-impact="high">
352
- <div class="ritem-top"><span class="rid">BOOST-D10</span><span class="rstatus next">Next</span></div>
352
+ <div class="ritem-top"><span class="rid">BOOST-D10</span><span class="rstatus done">Shipped</span></div>
353
353
  <h4>Braille spinner for network waits</h4>
354
354
  <p>Network-bound commands — <code>search</code> (AI rank), <code>tap</code> clone, <code>discover</code>, <code>index</code> — currently freeze silently while they work. Add a lightweight braille-dot spinner (<code>⠋⠙⠹⠸…</code>) tinted with the Aurora accent, auto-suppressed on non-TTY and under <code>NO_COLOR</code>. The single biggest "feels fast" upgrade.</p>
355
355
  <div class="rmeta"><span class="m-cat">motion</span><span>Complexity S</span><span class="m-imp high">Impact High</span><span class="m-wow">★★★★☆</span></div>
@@ -116,7 +116,7 @@
116
116
  <div class="stat"><b>4</b><span>Shipped</span></div>
117
117
  <div class="stat"><b>3</b><span>Next up</span></div>
118
118
  <div class="stat"><b>4</b><span>Planned</span></div>
119
- <div class="stat"><b>24</b><span>Loop finds</span></div>
119
+ <div class="stat"><b>32</b><span>Loop finds</span></div>
120
120
  </div>
121
121
  </section>
122
122
 
@@ -732,6 +732,146 @@
732
732
  </div>
733
733
  </section>
734
734
 
735
+ <!-- ============ DOCS-SITE & CONTENT QUALITY ============ -->
736
+ <section class="block reveal" id="docsite">
737
+ <div class="block-head">
738
+ <h2>Docs-site &amp; content quality</h2>
739
+ <span class="count">// planned · free tooling for the Pages site, README &amp; prose</span>
740
+ <span class="rule"></span>
741
+ </div>
742
+ <div class="rgrid">
743
+
744
+ <article class="cap rcard">
745
+ <div class="head"><span class="pill planned">Planned</span><span class="cat">Docs · Perf</span></div>
746
+ <h3>Lighthouse CI on the Pages site</h3>
747
+ <p><code>treosh/lighthouse-ci-action</code> scores <code>index.html</code>
748
+ and <code>roadmap.html</code> on performance, accessibility,
749
+ best-practices and SEO on every deploy, and fails the build against
750
+ budgets. Turns the marketing surface's quality into a number that can't
751
+ silently regress as the Aurora theme evolves.</p>
752
+ <div class="meta">
753
+ <span class="m">Complexity <b>M</b></span>
754
+ <span class="m hi">Impact <b>High</b></span>
755
+ <span class="m">Wow <b class="wow">★★★★★</b></span>
756
+ <span class="m">4 score budgets</span>
757
+ </div>
758
+ </article>
759
+
760
+ <article class="cap rcard">
761
+ <div class="head"><span class="pill planned">Planned</span><span class="cat">Quality · A11y</span></div>
762
+ <h3>Accessibility audit — <code>pa11y-ci</code> / axe-core</h3>
763
+ <p>Run WCAG checks over the rendered pages: colour contrast, focus order,
764
+ alt text and ARIA. The Aurora palette leans on muted
765
+ <code>--text-2/--text-3</code> tokens against dark glass — exactly where
766
+ contrast ratios slip below AA. Catches the a11y regressions a purely
767
+ visual review misses.</p>
768
+ <div class="meta">
769
+ <span class="m">Complexity <b>M</b></span>
770
+ <span class="m">Impact <b>Med</b></span>
771
+ <span class="m">Wow <b class="wow">★★★★</b></span>
772
+ <span class="m">WCAG AA</span>
773
+ </div>
774
+ </article>
775
+
776
+ <article class="cap rcard">
777
+ <div class="head"><span class="pill planned">Planned</span><span class="cat">Quality · Docs</span></div>
778
+ <h3>HTML validation — <code>html-validate</code></h3>
779
+ <p>The guide and roadmap are hand-authored HTML whose tags are balanced by
780
+ eye today. A CI <code>html-validate</code> pass catches unclosed
781
+ elements, duplicate <code>id</code>s and invalid nesting before they
782
+ ship a subtly broken layout — the automated version of the manual
783
+ tag-count sanity check.</p>
784
+ <div class="meta">
785
+ <span class="m">Complexity <b>S</b></span>
786
+ <span class="m">Impact <b>Med</b></span>
787
+ <span class="m">Wow <b class="wow">★★★</b></span>
788
+ <span class="m">hand-authored HTML</span>
789
+ </div>
790
+ </article>
791
+
792
+ <article class="cap rcard">
793
+ <div class="head"><span class="pill planned">Planned</span><span class="cat">Quality · Docs</span></div>
794
+ <h3>Broken-link &amp; anchor checking — <code>lychee</code></h3>
795
+ <p>Fast, free link checker over the README and <code>docs/*.html</code>:
796
+ verifies external URLs (GitHub, PyPI), asset paths
797
+ (<code>../style/boost.css</code>) and in-page anchors
798
+ (<code>#next</code>, <code>#health</code>) still resolve. A dead link in
799
+ the landing page is the first broken promise a visitor sees.</p>
800
+ <div class="meta">
801
+ <span class="m">Complexity <b>S</b></span>
802
+ <span class="m">Impact <b>Med</b></span>
803
+ <span class="m">Wow <b class="wow">★★★</b></span>
804
+ <span class="m">URLs + anchors</span>
805
+ </div>
806
+ </article>
807
+
808
+ <article class="cap rcard">
809
+ <div class="head"><span class="pill planned">Planned</span><span class="cat">Quality · Content</span></div>
810
+ <h3>Prose &amp; terminology linting — <code>vale</code></h3>
811
+ <p><code>vale</code> enforces a consistent voice across the README and docs
812
+ with a small boost vocabulary — canonical spelling of <em>skill</em>,
813
+ <em>tap</em>, <em>registry</em>; no <code>Boost</code>/<code>boost</code>
814
+ drift; flagged weasel words. Keeps a docs-heavy project reading like one
815
+ author wrote it.</p>
816
+ <div class="meta">
817
+ <span class="m">Complexity <b>S</b></span>
818
+ <span class="m">Impact <b>Low</b></span>
819
+ <span class="m">Wow <b class="wow">★★</b></span>
820
+ <span class="m">house style</span>
821
+ </div>
822
+ </article>
823
+
824
+ <article class="cap rcard">
825
+ <div class="head"><span class="pill planned">Planned</span><span class="cat">Quality · Docs</span></div>
826
+ <h3>Markdown consistency — <code>markdownlint-cli2</code></h3>
827
+ <p>Lint every Markdown surface — README, <code>CLAUDE.md</code>,
828
+ <code>DEBUGGING.md</code>, <code>rag-architecture.md</code> — for
829
+ heading order, list style and fenced-code hygiene. Cheap, deterministic,
830
+ and it keeps the contributor-facing docs as tidy as the code the gates
831
+ already enforce.</p>
832
+ <div class="meta">
833
+ <span class="m">Complexity <b>S</b></span>
834
+ <span class="m">Impact <b>Low</b></span>
835
+ <span class="m">Wow <b class="wow">★★</b></span>
836
+ <span class="m">every .md</span>
837
+ </div>
838
+ </article>
839
+
840
+ <article class="cap rcard">
841
+ <div class="head"><span class="pill planned">Planned</span><span class="cat">Quality · Style</span></div>
842
+ <h3>Theme-asset linting — <code>stylelint</code> + <code>eslint</code></h3>
843
+ <p>The shared <code>style/boost.css</code> and <code>boost.js</code> theme
844
+ the guide, roadmap and demo together, so a bug there recolours or breaks
845
+ everything at once. <code>stylelint</code> guards the CSS tokens and
846
+ <code>eslint</code> the reveal/interaction JS — the one place a small
847
+ mistake has system-wide blast radius.</p>
848
+ <div class="meta">
849
+ <span class="m">Complexity <b>S</b></span>
850
+ <span class="m">Impact <b>Med</b></span>
851
+ <span class="m">Wow <b class="wow">★★</b></span>
852
+ <span class="m">shared blast radius</span>
853
+ </div>
854
+ </article>
855
+
856
+ <article class="cap rcard">
857
+ <div class="head"><span class="pill planned">Planned</span><span class="cat">Testing · Docs</span></div>
858
+ <h3>Post-deploy smoke — headless load check</h3>
859
+ <p>After each Pages deploy, headless-load the guide and roadmap and assert
860
+ the essentials: HTTP 200, every asset resolves, and the console is free
861
+ of errors. Distinct from the planned visual-regression pass — this is a
862
+ cheap always-on health gate that would have caught the current broken
863
+ Pages deploy automatically.</p>
864
+ <div class="meta">
865
+ <span class="m">Complexity <b>M</b></span>
866
+ <span class="m">Impact <b>Med</b></span>
867
+ <span class="m">Wow <b class="wow">★★★</b></span>
868
+ <span class="m">catches red deploys</span>
869
+ </div>
870
+ </article>
871
+
872
+ </div>
873
+ </section>
874
+
735
875
  <footer>
736
876
  <p>Maintained by the boost quality loop · themed with
737
877
  <a href="../style/boost.css">style/boost.css</a> ·
@@ -0,0 +1,71 @@
1
+ """Unit tests: boost_cli/spin.Spinner — the TTY-guarded braille spinner."""
2
+ from __future__ import annotations
3
+
4
+ import io
5
+
6
+ import pytest
7
+
8
+ from boost_cli import spin
9
+
10
+
11
+ class FakeStream(io.StringIO):
12
+ def __init__(self, tty):
13
+ super().__init__()
14
+ self._tty = tty
15
+
16
+ def isatty(self):
17
+ return self._tty
18
+
19
+
20
+ @pytest.fixture(autouse=True)
21
+ def force_color(monkeypatch):
22
+ # color on, so `active()` hinges purely on the TTY check
23
+ monkeypatch.delenv("NO_COLOR", raising=False)
24
+ monkeypatch.setenv("CLICOLOR_FORCE", "1")
25
+
26
+
27
+ def test_silent_on_non_tty():
28
+ s = FakeStream(tty=False)
29
+ with spin.Spinner("working", stream=s) as sp:
30
+ pass
31
+ assert sp.active() is False
32
+ assert s.getvalue() == "" # nothing written, no thread started
33
+ assert sp._thread is None
34
+
35
+
36
+ def test_no_color_is_silent(monkeypatch):
37
+ monkeypatch.delenv("CLICOLOR_FORCE", raising=False)
38
+ monkeypatch.setenv("NO_COLOR", "1")
39
+ s = FakeStream(tty=True) # a TTY, but color is off
40
+ with spin.Spinner("working", stream=s):
41
+ pass
42
+ assert s.getvalue() == ""
43
+
44
+
45
+ def test_active_on_color_tty():
46
+ assert spin.Spinner("x", stream=FakeStream(tty=True)).active() is True
47
+
48
+
49
+ def test_frames_are_braille():
50
+ assert spin._FRAMES == "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
51
+
52
+
53
+ def test_context_manager_returns_self():
54
+ s = FakeStream(tty=False)
55
+ sp = spin.Spinner("x", stream=s)
56
+ assert sp.__enter__() is sp
57
+ assert sp.__exit__(None, None, None) is None # doesn't suppress exceptions
58
+
59
+
60
+ def test_tty_run_writes_and_clears():
61
+ # exercise the animated path: wait (bounded) for the thread's first frame
62
+ import time
63
+ s = FakeStream(tty=True)
64
+ with spin.Spinner("go", stream=s):
65
+ for _ in range(50):
66
+ if s.getvalue():
67
+ break
68
+ time.sleep(0.02)
69
+ out = s.getvalue()
70
+ assert "\r" in out # in-place redraw
71
+ assert "go" in out # the label was shown
File without changes