timbro 0.6.0__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 (175) hide show
  1. timbro-0.6.0/.claude-plugin/marketplace.json +17 -0
  2. timbro-0.6.0/.claude-plugin/plugin.json +19 -0
  3. timbro-0.6.0/.github/workflows/ci.yml +36 -0
  4. timbro-0.6.0/.github/workflows/publish.yml +66 -0
  5. timbro-0.6.0/.gitignore +20 -0
  6. timbro-0.6.0/.python-version +1 -0
  7. timbro-0.6.0/CLAUDE.md +37 -0
  8. timbro-0.6.0/CONTRIBUTING.md +26 -0
  9. timbro-0.6.0/LICENSE +21 -0
  10. timbro-0.6.0/PKG-INFO +309 -0
  11. timbro-0.6.0/README.md +275 -0
  12. timbro-0.6.0/assets/distance.svg +10 -0
  13. timbro-0.6.0/assets/logo-dark.svg +20 -0
  14. timbro-0.6.0/assets/logo.svg +20 -0
  15. timbro-0.6.0/eval/harness.py +94 -0
  16. timbro-0.6.0/eval/rubric_dashboard.py +154 -0
  17. timbro-0.6.0/eval/slop_bench/hc3/SOURCE.md +5 -0
  18. timbro-0.6.0/eval/slop_bench/hc3/human/finance-00.md +1 -0
  19. timbro-0.6.0/eval/slop_bench/hc3/human/finance-01.md +1 -0
  20. timbro-0.6.0/eval/slop_bench/hc3/human/finance-02.md +1 -0
  21. timbro-0.6.0/eval/slop_bench/hc3/human/finance-03.md +1 -0
  22. timbro-0.6.0/eval/slop_bench/hc3/human/finance-04.md +1 -0
  23. timbro-0.6.0/eval/slop_bench/hc3/human/finance-05.md +1 -0
  24. timbro-0.6.0/eval/slop_bench/hc3/human/finance-06.md +1 -0
  25. timbro-0.6.0/eval/slop_bench/hc3/human/finance-07.md +1 -0
  26. timbro-0.6.0/eval/slop_bench/hc3/human/medicine-00.md +1 -0
  27. timbro-0.6.0/eval/slop_bench/hc3/human/medicine-01.md +1 -0
  28. timbro-0.6.0/eval/slop_bench/hc3/human/medicine-02.md +1 -0
  29. timbro-0.6.0/eval/slop_bench/hc3/human/medicine-03.md +1 -0
  30. timbro-0.6.0/eval/slop_bench/hc3/human/medicine-04.md +1 -0
  31. timbro-0.6.0/eval/slop_bench/hc3/human/medicine-05.md +1 -0
  32. timbro-0.6.0/eval/slop_bench/hc3/human/medicine-06.md +1 -0
  33. timbro-0.6.0/eval/slop_bench/hc3/human/medicine-07.md +1 -0
  34. timbro-0.6.0/eval/slop_bench/hc3/human/open_qa-00.md +1 -0
  35. timbro-0.6.0/eval/slop_bench/hc3/human/open_qa-01.md +1 -0
  36. timbro-0.6.0/eval/slop_bench/hc3/human/open_qa-02.md +1 -0
  37. timbro-0.6.0/eval/slop_bench/hc3/human/open_qa-03.md +1 -0
  38. timbro-0.6.0/eval/slop_bench/hc3/human/open_qa-04.md +1 -0
  39. timbro-0.6.0/eval/slop_bench/hc3/human/open_qa-05.md +1 -0
  40. timbro-0.6.0/eval/slop_bench/hc3/human/open_qa-06.md +1 -0
  41. timbro-0.6.0/eval/slop_bench/hc3/human/open_qa-07.md +1 -0
  42. timbro-0.6.0/eval/slop_bench/hc3/human/reddit_eli5-00.md +1 -0
  43. timbro-0.6.0/eval/slop_bench/hc3/human/reddit_eli5-01.md +1 -0
  44. timbro-0.6.0/eval/slop_bench/hc3/human/reddit_eli5-02.md +1 -0
  45. timbro-0.6.0/eval/slop_bench/hc3/human/reddit_eli5-03.md +1 -0
  46. timbro-0.6.0/eval/slop_bench/hc3/human/reddit_eli5-04.md +1 -0
  47. timbro-0.6.0/eval/slop_bench/hc3/human/reddit_eli5-05.md +1 -0
  48. timbro-0.6.0/eval/slop_bench/hc3/human/reddit_eli5-06.md +1 -0
  49. timbro-0.6.0/eval/slop_bench/hc3/human/reddit_eli5-07.md +1 -0
  50. timbro-0.6.0/eval/slop_bench/hc3/human/wiki_csai-00.md +4 -0
  51. timbro-0.6.0/eval/slop_bench/hc3/human/wiki_csai-01.md +5 -0
  52. timbro-0.6.0/eval/slop_bench/hc3/human/wiki_csai-02.md +2 -0
  53. timbro-0.6.0/eval/slop_bench/hc3/human/wiki_csai-03.md +1 -0
  54. timbro-0.6.0/eval/slop_bench/hc3/human/wiki_csai-04.md +3 -0
  55. timbro-0.6.0/eval/slop_bench/hc3/human/wiki_csai-05.md +2 -0
  56. timbro-0.6.0/eval/slop_bench/hc3/human/wiki_csai-06.md +2 -0
  57. timbro-0.6.0/eval/slop_bench/hc3/human/wiki_csai-07.md +6 -0
  58. timbro-0.6.0/eval/slop_bench/hc3/llm/finance-00.md +1 -0
  59. timbro-0.6.0/eval/slop_bench/hc3/llm/finance-01.md +1 -0
  60. timbro-0.6.0/eval/slop_bench/hc3/llm/finance-02.md +1 -0
  61. timbro-0.6.0/eval/slop_bench/hc3/llm/finance-03.md +1 -0
  62. timbro-0.6.0/eval/slop_bench/hc3/llm/finance-04.md +1 -0
  63. timbro-0.6.0/eval/slop_bench/hc3/llm/finance-05.md +1 -0
  64. timbro-0.6.0/eval/slop_bench/hc3/llm/finance-06.md +1 -0
  65. timbro-0.6.0/eval/slop_bench/hc3/llm/finance-07.md +1 -0
  66. timbro-0.6.0/eval/slop_bench/hc3/llm/medicine-00.md +3 -0
  67. timbro-0.6.0/eval/slop_bench/hc3/llm/medicine-01.md +11 -0
  68. timbro-0.6.0/eval/slop_bench/hc3/llm/medicine-02.md +5 -0
  69. timbro-0.6.0/eval/slop_bench/hc3/llm/medicine-03.md +1 -0
  70. timbro-0.6.0/eval/slop_bench/hc3/llm/medicine-04.md +9 -0
  71. timbro-0.6.0/eval/slop_bench/hc3/llm/medicine-05.md +5 -0
  72. timbro-0.6.0/eval/slop_bench/hc3/llm/medicine-06.md +11 -0
  73. timbro-0.6.0/eval/slop_bench/hc3/llm/medicine-07.md +5 -0
  74. timbro-0.6.0/eval/slop_bench/hc3/llm/open_qa-00.md +1 -0
  75. timbro-0.6.0/eval/slop_bench/hc3/llm/open_qa-01.md +1 -0
  76. timbro-0.6.0/eval/slop_bench/hc3/llm/open_qa-02.md +1 -0
  77. timbro-0.6.0/eval/slop_bench/hc3/llm/open_qa-03.md +1 -0
  78. timbro-0.6.0/eval/slop_bench/hc3/llm/open_qa-04.md +1 -0
  79. timbro-0.6.0/eval/slop_bench/hc3/llm/open_qa-05.md +1 -0
  80. timbro-0.6.0/eval/slop_bench/hc3/llm/open_qa-06.md +1 -0
  81. timbro-0.6.0/eval/slop_bench/hc3/llm/open_qa-07.md +1 -0
  82. timbro-0.6.0/eval/slop_bench/hc3/llm/reddit_eli5-00.md +4 -0
  83. timbro-0.6.0/eval/slop_bench/hc3/llm/reddit_eli5-01.md +6 -0
  84. timbro-0.6.0/eval/slop_bench/hc3/llm/reddit_eli5-02.md +4 -0
  85. timbro-0.6.0/eval/slop_bench/hc3/llm/reddit_eli5-03.md +4 -0
  86. timbro-0.6.0/eval/slop_bench/hc3/llm/reddit_eli5-04.md +5 -0
  87. timbro-0.6.0/eval/slop_bench/hc3/llm/reddit_eli5-05.md +3 -0
  88. timbro-0.6.0/eval/slop_bench/hc3/llm/reddit_eli5-06.md +4 -0
  89. timbro-0.6.0/eval/slop_bench/hc3/llm/reddit_eli5-07.md +3 -0
  90. timbro-0.6.0/eval/slop_bench/hc3/llm/wiki_csai-00.md +13 -0
  91. timbro-0.6.0/eval/slop_bench/hc3/llm/wiki_csai-01.md +13 -0
  92. timbro-0.6.0/eval/slop_bench/hc3/llm/wiki_csai-02.md +17 -0
  93. timbro-0.6.0/eval/slop_bench/hc3/llm/wiki_csai-03.md +13 -0
  94. timbro-0.6.0/eval/slop_bench/hc3/llm/wiki_csai-04.md +9 -0
  95. timbro-0.6.0/eval/slop_bench/hc3/llm/wiki_csai-05.md +1 -0
  96. timbro-0.6.0/eval/slop_bench/hc3/llm/wiki_csai-06.md +13 -0
  97. timbro-0.6.0/eval/slop_bench/hc3/llm/wiki_csai-07.md +9 -0
  98. timbro-0.6.0/eval/slop_benchmark.py +90 -0
  99. timbro-0.6.0/pyproject.toml +67 -0
  100. timbro-0.6.0/scripts/derive_concreteness_prior.py +169 -0
  101. timbro-0.6.0/scripts/derive_fw_reference.py +103 -0
  102. timbro-0.6.0/scripts/hc3_subsample.py +99 -0
  103. timbro-0.6.0/scripts/release.sh +64 -0
  104. timbro-0.6.0/skills/timbro/SKILL.md +94 -0
  105. timbro-0.6.0/src/timbro/__init__.py +11 -0
  106. timbro-0.6.0/src/timbro/analyze.py +449 -0
  107. timbro-0.6.0/src/timbro/cleanup/__init__.py +15 -0
  108. timbro-0.6.0/src/timbro/cleanup/latex.py +118 -0
  109. timbro-0.6.0/src/timbro/cleanup/papers.py +261 -0
  110. timbro-0.6.0/src/timbro/cli.py +251 -0
  111. timbro-0.6.0/src/timbro/concreteness.py +94 -0
  112. timbro-0.6.0/src/timbro/config.py +142 -0
  113. timbro-0.6.0/src/timbro/flow.py +128 -0
  114. timbro-0.6.0/src/timbro/fw.py +88 -0
  115. timbro-0.6.0/src/timbro/hedge.py +104 -0
  116. timbro-0.6.0/src/timbro/lexicons/boosters.txt +66 -0
  117. timbro-0.6.0/src/timbro/lexicons/connectives_conditional.txt +17 -0
  118. timbro-0.6.0/src/timbro/lexicons/hedges.txt +90 -0
  119. timbro-0.6.0/src/timbro/lexicons/hype.txt +61 -0
  120. timbro-0.6.0/src/timbro/lexicons/negations.txt +22 -0
  121. timbro-0.6.0/src/timbro/lexicons/plain_wording.txt +223 -0
  122. timbro-0.6.0/src/timbro/mcp_server.py +80 -0
  123. timbro-0.6.0/src/timbro/metric.py +120 -0
  124. timbro-0.6.0/src/timbro/model.py +510 -0
  125. timbro-0.6.0/src/timbro/norms/NOTICE.md +9 -0
  126. timbro-0.6.0/src/timbro/norms/concreteness_brysbaert2014.csv.gz +0 -0
  127. timbro-0.6.0/src/timbro/profiles.py +253 -0
  128. timbro-0.6.0/src/timbro/report.py +224 -0
  129. timbro-0.6.0/src/timbro/rewrite.py +63 -0
  130. timbro-0.6.0/src/timbro/rubrics/__init__.py +23 -0
  131. timbro-0.6.0/src/timbro/rubrics/base.py +50 -0
  132. timbro-0.6.0/src/timbro/rubrics/density/__init__.py +5 -0
  133. timbro-0.6.0/src/timbro/rubrics/density/checks.py +127 -0
  134. timbro-0.6.0/src/timbro/rubrics/density/rubric.py +24 -0
  135. timbro-0.6.0/src/timbro/rubrics/features.py +804 -0
  136. timbro-0.6.0/src/timbro/rubrics/registry.py +15 -0
  137. timbro-0.6.0/src/timbro/rubrics/report.py +51 -0
  138. timbro-0.6.0/src/timbro/rubrics/rules.py +496 -0
  139. timbro-0.6.0/src/timbro/rubrics/schimel/__init__.py +5 -0
  140. timbro-0.6.0/src/timbro/rubrics/schimel/rubric.py +21 -0
  141. timbro-0.6.0/src/timbro/rubrics/sections.py +32 -0
  142. timbro-0.6.0/src/timbro/rubrics/slop/__init__.py +5 -0
  143. timbro-0.6.0/src/timbro/rubrics/slop/checks.py +91 -0
  144. timbro-0.6.0/src/timbro/rubrics/slop/rubric.py +26 -0
  145. timbro-0.6.0/src/timbro/sample/contrast/01-synergy.md +9 -0
  146. timbro-0.6.0/src/timbro/sample/contrast/02-revolutionize.md +11 -0
  147. timbro-0.6.0/src/timbro/sample/contrast/03-paradigm.md +9 -0
  148. timbro-0.6.0/src/timbro/sample/exemplars/01-shipping.md +9 -0
  149. timbro-0.6.0/src/timbro/sample/exemplars/02-debugging.md +9 -0
  150. timbro-0.6.0/src/timbro/sample/exemplars/03-reviews.md +9 -0
  151. timbro-0.6.0/src/timbro/sample/exemplars/04-estimates.md +9 -0
  152. timbro-0.6.0/src/timbro/spacy_model.py +30 -0
  153. timbro-0.6.0/src/timbro/tells.py +324 -0
  154. timbro-0.6.0/src/timbro/text.py +169 -0
  155. timbro-0.6.0/tests/test_analyze.py +273 -0
  156. timbro-0.6.0/tests/test_analyze_folk_advice.py +166 -0
  157. timbro-0.6.0/tests/test_concreteness.py +113 -0
  158. timbro-0.6.0/tests/test_derive_concreteness_prior.py +130 -0
  159. timbro-0.6.0/tests/test_derive_fw_reference.py +59 -0
  160. timbro-0.6.0/tests/test_fw.py +123 -0
  161. timbro-0.6.0/tests/test_hedge.py +120 -0
  162. timbro-0.6.0/tests/test_leading_words.py +96 -0
  163. timbro-0.6.0/tests/test_markdown_scoring.py +75 -0
  164. timbro-0.6.0/tests/test_preprocess.py +217 -0
  165. timbro-0.6.0/tests/test_profiles.py +100 -0
  166. timbro-0.6.0/tests/test_report_attribution.py +66 -0
  167. timbro-0.6.0/tests/test_report_severity.py +46 -0
  168. timbro-0.6.0/tests/test_rubric_density.py +131 -0
  169. timbro-0.6.0/tests/test_rubric_schimel.py +585 -0
  170. timbro-0.6.0/tests/test_rubric_slop.py +84 -0
  171. timbro-0.6.0/tests/test_runtime_latex.py +121 -0
  172. timbro-0.6.0/tests/test_scoring_policy.py +52 -0
  173. timbro-0.6.0/tests/test_slop_benchmark.py +77 -0
  174. timbro-0.6.0/tests/test_tells.py +107 -0
  175. timbro-0.6.0/uv.lock +2739 -0
@@ -0,0 +1,17 @@
1
+ {
2
+ "$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
3
+ "name": "timbro",
4
+ "description": "Keep your writing sounding like you — even when an LLM is writing.",
5
+ "owner": {
6
+ "name": "Nicolo' Brandizzi",
7
+ "url": "https://github.com/nicofirst1"
8
+ },
9
+ "plugins": [
10
+ {
11
+ "name": "timbro",
12
+ "description": "Score a draft against your voice and get named, content-preserving edits to align it. Bundles the skill + the score_voice / accept_rewrite / check_voice MCP tools.",
13
+ "source": "./",
14
+ "category": "productivity"
15
+ }
16
+ ]
17
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "timbro",
3
+ "version": "0.6.0",
4
+ "description": "Keep your writing sounding like you — even when an LLM is writing. Scores a draft's distance from your voice (score_voice) and runs a deterministic Schimel writing rubric (check_voice). Returns named, content-preserving edits. Local, white-box, MCP-ready.",
5
+ "author": {
6
+ "name": "Nicolo' Brandizzi",
7
+ "url": "https://nicolobrandizzi.com"
8
+ },
9
+ "homepage": "https://github.com/nicofirst1/timbro",
10
+ "keywords": ["writing", "style", "voice", "consistency", "mcp", "ai-agents"],
11
+ "mcpServers": {
12
+ "timbro": {
13
+ "command": "uv",
14
+ "args": ["run", "--directory", "${CLAUDE_PLUGIN_ROOT}", "timbro-mcp"],
15
+ "comment": "Runs on the packaged sample voice out of the box. To use your real voice, add TIMBRO_EXEMPLARS / TIMBRO_CONTRAST (absolute paths to your own corpora) to the env below.",
16
+ "env": {}
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,36 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main, dev]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ strategy:
11
+ matrix:
12
+ os: [ubuntu-latest, macos-latest]
13
+ runs-on: ${{ matrix.os }}
14
+ timeout-minutes: 20
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - name: Install uv
19
+ uses: astral-sh/setup-uv@v5
20
+ with:
21
+ enable-cache: true
22
+
23
+ - name: Cache HF models
24
+ uses: actions/cache@v4
25
+ with:
26
+ path: ~/.cache/huggingface
27
+ key: ${{ runner.os }}-hf-${{ hashFiles('uv.lock') }}
28
+
29
+ - name: Install dependencies
30
+ run: uv sync
31
+
32
+ - name: Lint
33
+ run: uvx ruff@0.15.20 check src/
34
+
35
+ - name: Test
36
+ run: uv run pytest
@@ -0,0 +1,66 @@
1
+ name: Publish to PyPI
2
+
3
+ # Runs on version tags (vX.Y.Z). Publishing uses PyPI's trusted-publisher OIDC flow
4
+ # (pypa/gh-action-pypi-publish) -- no API token secret to manage or leak. The human
5
+ # step this workflow depends on: register this repo/workflow as a trusted publisher
6
+ # for the `timbro` project at https://pypi.org/manage/project/timbro/settings/publishing/
7
+ # (one-time setup, done after the first manual `uv publish`).
8
+
9
+ on:
10
+ push:
11
+ tags:
12
+ - "v*.*.*"
13
+
14
+ jobs:
15
+ verify-and-build:
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - name: Install uv
21
+ uses: astral-sh/setup-uv@v5
22
+ with:
23
+ python-version: "3.11"
24
+
25
+ - name: Assert tag == pyproject.toml == plugin.json version
26
+ run: |
27
+ TAG_VERSION="${GITHUB_REF_NAME#v}"
28
+ PYPROJECT_VERSION=$(grep -m1 '^version = ' pyproject.toml | sed -E 's/version = "(.*)"/\1/')
29
+ PLUGIN_VERSION=$(python3 -c "import json; print(json.load(open('.claude-plugin/plugin.json'))['version'])")
30
+ echo "tag=$TAG_VERSION pyproject=$PYPROJECT_VERSION plugin=$PLUGIN_VERSION"
31
+ if [ "$TAG_VERSION" != "$PYPROJECT_VERSION" ] || [ "$TAG_VERSION" != "$PLUGIN_VERSION" ]; then
32
+ echo "::error::version mismatch: tag=$TAG_VERSION pyproject=$PYPROJECT_VERSION plugin=$PLUGIN_VERSION"
33
+ exit 1
34
+ fi
35
+
36
+ - name: Build
37
+ run: uv build
38
+
39
+ - name: twine check
40
+ run: uvx twine check dist/*
41
+
42
+ - name: Cold-start smoke test (clean venv, no repo deps)
43
+ run: |
44
+ python3 -m venv /tmp/cold-venv
45
+ /tmp/cold-venv/bin/pip install --quiet dist/*.whl
46
+ /tmp/cold-venv/bin/timbro check README.md
47
+
48
+ - uses: actions/upload-artifact@v4
49
+ with:
50
+ name: dist
51
+ path: dist/
52
+
53
+ publish:
54
+ needs: verify-and-build
55
+ runs-on: ubuntu-latest
56
+ environment: pypi
57
+ permissions:
58
+ id-token: write # OIDC trusted publishing, no token secret
59
+ steps:
60
+ - uses: actions/download-artifact@v4
61
+ with:
62
+ name: dist
63
+ path: dist/
64
+
65
+ - name: Publish to PyPI
66
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,20 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+ .worktrees/
12
+
13
+ # Local corpora (private / third-party content) -- bring your own
14
+ data/
15
+
16
+ # Local agent config
17
+ .claude/
18
+
19
+ # Internal build spec / thinking record (kept local, not published)
20
+ PLAN.md
@@ -0,0 +1 @@
1
+ 3.11
timbro-0.6.0/CLAUDE.md ADDED
@@ -0,0 +1,37 @@
1
+ # Timbro — agent notes
2
+
3
+ Measures a draft's distance from a target voice and returns a named revision direction. Does not rewrite — the agent does. Local, CPU-only.
4
+
5
+ ## Active plan — follow the GitHub milestones
6
+
7
+ All planned work lives in GitHub issues (`gh issue list`), grouped into milestones. **Before starting anything substantive, read the relevant issue and work within it** — don't invent parallel work; if something is missing, add an issue to the right milestone instead.
8
+
9
+ ### Implementer guardrails
10
+
11
+ Issues are labeled by required capability: `agent:mechanical` = fully specified, follow the "Implementer spec" section literally; `agent:judgment` = has open taste/decision surface — ask the user before deviating or deciding, don't guess.
12
+
13
+ - One issue per branch/PR. Don't fold in drive-by refactors.
14
+ - The "Implementer spec" sections are decisions, not suggestions. If a number or approach in one looks wrong, comment on the issue and stop — do not silently substitute your own.
15
+ - Before declaring done, run `uv run pytest` and `uv run ruff check src/` and quote the output in the PR.
16
+ - Never touch `_PENALTY` / `_WEIGHTS` values, the verdict thresholds in `report.py`, or add a dependency, unless the issue explicitly says so.
17
+ - Respect issue dependencies. If your issue is blocked, say so instead of working around it.
18
+
19
+ ## Commands
20
+
21
+ - `uv run timbro score draft.md` — score a file (runs on the packaged sample voice if no corpus env vars set)
22
+ - `uv run timbro-mcp` — MCP server (stdio)
23
+ - `uv run python -m timbro.model` — core smoke test
24
+ - `uv run ruff check src/` — lint
25
+ - Corpus: `TIMBRO_EXEMPLARS` (toward) / `TIMBRO_CONTRAST` (away). Named profiles live under `$XDG_DATA_HOME/timbro/profiles/<name>/{exemplars,contrast}/` by default (`$XDG_DATA_HOME` falls back to `~/.local/share`); an existing `~/.timbro/profiles/` is used instead if present (legacy installs); `TIMBRO_PROFILE_ROOT` overrides both.
26
+
27
+ ## Releasing an update
28
+
29
+ The plugin updater compares by **version string**, so without a bump it will not pick up code changes (`already at latest version`).
30
+
31
+ Run `scripts/release.sh <new-version>` — it bumps both `.claude-plugin/plugin.json` and `pyproject.toml` (and fails loud if they end up mismatched), `uv lock`s, commits, confirms before pushing to `main`, then refreshes the marketplace clone, updates the plugin, and syncs the new cache venv.
32
+
33
+ ## Gotchas
34
+
35
+ - `en_core_web_sm` is pinned as a direct-URL wheel dep (needs `tool.hatch.metadata.allow-direct-references`). No manual `spacy download`.
36
+ - Defaults resolve relative to the package dir (`src/timbro/sample/`), not CWD — so the plugin works inside its cache sandbox.
37
+ - `data/` is gitignored (private corpora); the shipped `src/timbro/sample/` is the only corpus that publishes.
@@ -0,0 +1,26 @@
1
+ # Contributing
2
+
3
+ ## Setup
4
+
5
+ ```
6
+ uv sync
7
+ ```
8
+
9
+ That's it — `uv` installs everything, including the pinned spaCy model wheel (no manual `spacy download`).
10
+
11
+ ## Before you start
12
+
13
+ Read `CLAUDE.md`'s "Active plan" section and the [open issues](https://github.com/nicofirst1/timbro/issues) first. All planned work lives in GitHub issues grouped into milestones — work within an existing issue rather than inventing parallel work. One issue per branch/PR; don't fold in drive-by refactors.
14
+
15
+ ## Test and lint
16
+
17
+ ```
18
+ uv run pytest
19
+ uv run ruff check src/
20
+ ```
21
+
22
+ Both must pass before you open a PR — CI runs the same two commands on push/PR (ubuntu + macos).
23
+
24
+ ## Releasing
25
+
26
+ See the "Releasing an update" section in `CLAUDE.md` (`scripts/release.sh <new-version>`). Only maintainers cut releases.
timbro-0.6.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nicolo' Brandizzi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
timbro-0.6.0/PKG-INFO ADDED
@@ -0,0 +1,309 @@
1
+ Metadata-Version: 2.4
2
+ Name: timbro
3
+ Version: 0.6.0
4
+ Summary: Measure the timbre of your writing as a metric space and get an interpretable, content-preserving direction to revise a draft toward your voice.
5
+ Project-URL: Homepage, https://github.com/nicofirst1/timbro
6
+ Project-URL: Repository, https://github.com/nicofirst1/timbro
7
+ Project-URL: Issues, https://github.com/nicofirst1/timbro/issues
8
+ Author: Nicolo' Brandizzi
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: editing,linting,nlp,spacy,style,voice,writing
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: Quality Assurance
22
+ Classifier: Topic :: Text Processing :: Linguistic
23
+ Requires-Python: >=3.11
24
+ Requires-Dist: lexical-diversity>=0.1.1
25
+ Requires-Dist: mcp>=1.27.2
26
+ Requires-Dist: numpy>=2.4.6
27
+ Requires-Dist: scikit-learn>=1.9.0
28
+ Requires-Dist: sentence-transformers>=5.5.1
29
+ Requires-Dist: setuptools<81
30
+ Requires-Dist: spacy>=3.8.14
31
+ Requires-Dist: textdescriptives>=2.8.2
32
+ Requires-Dist: wordfreq>=3.1.1
33
+ Description-Content-Type: text/markdown
34
+
35
+ <p align="center">
36
+ <picture>
37
+ <source media="(prefers-color-scheme: dark)" srcset="assets/logo-dark.svg">
38
+ <img src="assets/logo.svg" width="180" alt="Timbro">
39
+ </picture>
40
+ </p>
41
+
42
+ <h1 align="center">Timbro</h1>
43
+
44
+ <p align="center">
45
+ <em>Catch AI slop with deterministic, offline checks that never call an LLM. Then keep what's left sounding like you.</em>
46
+ </p>
47
+
48
+ <p align="center">
49
+ <a href="https://github.com/nicofirst1/timbro/actions/workflows/ci.yml"><img src="https://github.com/nicofirst1/timbro/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
50
+ <img src="https://img.shields.io/badge/python-3.11%2B-111111?style=flat-square" alt="Python 3.11+">
51
+ <img src="https://img.shields.io/badge/inference-local%20·%20CPU--only-111111?style=flat-square" alt="Local CPU-only inference">
52
+ <img src="https://img.shields.io/badge/MCP-ready-111111?style=flat-square" alt="MCP ready">
53
+ <img src="https://img.shields.io/badge/license-MIT-111111?style=flat-square" alt="MIT license">
54
+ </p>
55
+
56
+ ---
57
+
58
+ **LLM prose has a tell.** Em/en dashes everywhere, "it's not X, it's Y", the _delve / tapestry / seamless_ vocabulary, a tidy wrap-up about the future. A reader feels it, but "sounds AI-written" is not something you can put in CI.
59
+
60
+ Timbro makes it one. `timbro slop` runs ~19 deterministic detectors (regex + part-of-speech, no model, no network) and returns a verdict, four dimension scores, and the exact markers it found:
61
+
62
+ ```
63
+ $ timbro slop draft.md
64
+ slop: WARN (0.69)
65
+
66
+ diction 0.70
67
+ construction 0.70
68
+ rhythm 0.80
69
+ formatting 0.55
70
+
71
+ Top findings
72
+ - formatting: 2× em/en dashes
73
+ - diction: 12× AI-tell diction (delve, tapestry, seamless, robust, …)
74
+ - construction: signposting phrases, wrap-up phrases
75
+ ```
76
+
77
+ Delete the flagged markers, re-run, and it reads `slop: PASS (1.00)`. Same meaning, no tells.
78
+
79
+ **Sanity check, not a headline number:** `eval/slop_benchmark.py` scores a small set of genuinely LLM-generated paragraphs against the packaged known-good human prose and reports how often `slop` fires on each side. It's a repo-local smoke test, not independent validation — the human side is the same corpus the tell rules were tuned against (see the script's own caveat), so its false-positive rate isn't a claim about unseen writing. Run it yourself:
80
+
81
+ ```bash
82
+ uv run python eval/slop_benchmark.py
83
+ ```
84
+
85
+ **Why not just ask an LLM "does this read AI-generated?"** Because that is an LLM grading an LLM: nondeterministic, an API call every time, and it can't show you _which_ words tripped it. Timbro is white-box. Every flag is a named marker you can see, cite, and remove; it runs local and CPU-only, gives the same answer every time, and is fast enough for a git hook.
86
+
87
+ ## And a positive target, not just a blocklist
88
+
89
+ Any regex list can tell you what to strip. Timbro's second act tells you what your writing should sound _like_. Seed it with posts you've accepted as your voice, and it scores any draft for **how far** it sits from that voice and **which way** to revise it, in named features, without changing what it says. That positive target is what separates it from every slop-lister.
90
+
91
+ - **You, consistently.** A personal blog or newsletter should sound like one person across years of posts — not like whichever model wrote each one.
92
+ - **A company on-brand.** Marketing, docs, and posts drift across authors and tools. Seed Timbro with your on-brand corpus and every draft gets measured against it.
93
+ - **An agent that self-corrects.** LLMs are fluent but stylistically inconsistent. Timbro gives an agent a _measurable target_ and a _named direction_, so it can revise toward a voice instead of guessing.
94
+
95
+ ## Numbers
96
+
97
+ This README was written by Claude (Opus 4.8). With Timbro you can see exactly how it scores against [my actual blog voice](https://nicolobrandizzi.com/blog/) — the same number your agent watches as it revises:
98
+
99
+ <p align="center">
100
+ <img src="assets/distance.svg" width="780" alt="A 0-to-far axis: my blog voice sits in a 9–35 band; this README lands just outside it at 47; marketing hype is far out at 86">
101
+ </p>
102
+
103
+ It lands at 47 — outside my blog range (9–35): recognizably _not_ my essay voice (it's code-heavy docs), but a world away from sales-speak at 86. And Timbro hands back the _direction_ to close the gap: **more conjunctions, fewer abstract nouns, less code-block punctuation**. Scored against: [Horizon AI Fragmentation](https://nicolobrandizzi.com/blog/horizon-analysis/), [Teaching Machines to Think](https://nicolobrandizzi.com/blog/rl-reasoning-llm/), [The Digital Poisoners](https://nicolobrandizzi.com/blog/pravda-grooming/), [The SOTA Trap](https://nicolobrandizzi.com/blog/sota-trap/), [AI Gigafactories](https://nicolobrandizzi.com/blog/ai-gigafactories-tool/).
104
+
105
+ ## How it works
106
+
107
+ Your agent runs one loop, and Timbro scores every turn of it:
108
+
109
+ ```
110
+ score → how far from your voice, and which way to move
111
+ edit → revise toward the named direction
112
+ re-score → distance dropped AND meaning held?
113
+ repeat → until the distance stops falling
114
+ ```
115
+
116
+ Each score is three legible layers plus a guard:
117
+
118
+ - **Scalar — "how far"** — a pre-trained [StyleDistance](https://huggingface.co/StyleDistance/styledistance) embedding, scored by multi-modal **kNN**.
119
+ - **Direction — "which way"** — **POS-unigram** rates, z-scored against your corpus and weighted by each feature's R². Every move is a named habit.
120
+ - **Flow** — paragraph-embedding trajectory (speed, volume, circuitousness) + the Schimel "circle-back" (`cos(first, last)`).
121
+ - **Content guard** — semantic cosine via a _general_ model (all-MiniLM): changes _how_ it reads, never _what_ it says.
122
+
123
+ ## The writing rubric (`check`)
124
+
125
+ Voice alignment answers _"does this sound like me?"_. The rubric answers a separate question — _"is this good prose?"_ — and needs **no voice corpus**. `timbro check` (and the `check_voice` MCP tool) runs ~30 deterministic checks distilled from Joshua Schimel's _Writing Science_, all linguistic/structural (spaCy dependency parse + POS + counting), **no LLM-as-judge**: buried subject–verb core, passive voice, comma splices, expletive openings, preposition chains, nominalizations, long Latinate words, word-echo repetition, inconsistent terminology, metadiscourse and citation-as-subject frames, caveat/defensive closings, unearned claim words, significance-without-magnitude, and more. It returns a per-dimension score and a ranked findings list — recall-first, so a model consumer filters the occasional false positive. Rubrics are pluggable via a registry (`--rubric <name>`); `schimel` ships today. `uv run python eval/rubric_dashboard.py` prints each rule's findings-per-1000-words on known-good prose, so noisy rules can be spotted and demoted rather than deleted.
126
+
127
+ ```bash
128
+ uv run timbro check draft.md # human-readable
129
+ uv run timbro check draft.md --json # {verdict, overall, dimensions, findings}
130
+ ```
131
+
132
+ ## Install
133
+
134
+ ### As a one-shot CLI, no clone (fastest)
135
+
136
+ ```bash
137
+ uvx timbro check draft.md # first run downloads the spaCy POS model, then scores
138
+ ```
139
+
140
+ ### As a Claude Code plugin (one command)
141
+
142
+ ```
143
+ /plugin marketplace add nicofirst1/timbro
144
+ /plugin install timbro@timbro
145
+ ```
146
+
147
+ This installs the **skill** _and_ wires up the **MCP tools** (`score_voice`, `accept_rewrite`, `check_voice`) in one shot. It works immediately on a small **packaged sample voice** — ask Claude _"score this against the Timbro sample voice"_ to see it run.
148
+
149
+ To use **your** voice, point the MCP server at your own corpus. Edit the `timbro` entry in your MCP config (or the plugin's `plugin.json`) to set absolute paths:
150
+
151
+ ```json
152
+ "env": {
153
+ "TIMBRO_EXEMPLARS": "/abs/path/to/your/exemplars",
154
+ "TIMBRO_CONTRAST": "/abs/path/to/your/contrast"
155
+ }
156
+ ```
157
+
158
+ The POS model and the sample corpus both ship with the plugin — no manual download step.
159
+
160
+ ### As a skill
161
+
162
+ Copy just the skill so the agent knows when and how to use Timbro:
163
+
164
+ ```bash
165
+ cp -r skills/timbro ~/.claude/skills/ # personal, or .claude/skills/ per-project
166
+ ```
167
+
168
+ Now ask Claude the same way — it runs Timbro, reads the direction, and proposes content-preserving edits.
169
+
170
+ ### As an MCP server (Claude Code, Cursor, Windsurf, Claude Desktop, …)
171
+
172
+ ```bash
173
+ # Claude Code
174
+ claude mcp add timbro \
175
+ -e TIMBRO_EXEMPLARS=$PWD/data/exemplars \
176
+ -e TIMBRO_CONTRAST=$PWD/data/contrast \
177
+ -- uv run --directory $PWD timbro-mcp
178
+ ```
179
+
180
+ Or drop this into any agent's `.mcp.json` / MCP settings:
181
+
182
+ ```json
183
+ {
184
+ "mcpServers": {
185
+ "timbro": {
186
+ "command": "uv",
187
+ "args": ["run", "--directory", "/abs/path/to/timbro", "timbro-mcp"],
188
+ "env": {
189
+ "TIMBRO_EXEMPLARS": "/abs/path/to/timbro/data/exemplars",
190
+ "TIMBRO_CONTRAST": "/abs/path/to/timbro/data/contrast"
191
+ }
192
+ }
193
+ }
194
+ }
195
+ ```
196
+
197
+ The agent gets three tools:
198
+
199
+ | Tool | Returns |
200
+ | ----------------------------------- | ------------------------------------------------------------------------------------- |
201
+ | `score_voice(text)` | `{distance, direction, flow}` |
202
+ | `accept_rewrite(original, revised)` | `{accepted, content_ok, similarity, distance_before, distance_after, improved}` |
203
+ | `check_voice(text)` | `{verdict, overall, dimensions, findings}` — the deterministic writing rubric (below) |
204
+
205
+ ### As a one-shot CLI
206
+
207
+ No server, no agent — just score a file:
208
+
209
+ ```bash
210
+ uv run timbro slop draft.md # deterministic AI-slop / tells report
211
+ uv run timbro score draft.md # distance from your voice + revision direction
212
+ cat draft.md | uv run timbro score - # stdin
213
+ uv run timbro score draft.md --json # raw payload
214
+ uv run timbro check draft.md # Schimel prose-quality rubric (below)
215
+ ```
216
+
217
+ ### From source (required for the MCP and CLI options above)
218
+
219
+ Requires Python ≥ 3.11 and [`uv`](https://docs.astral.sh/uv/).
220
+
221
+ ```bash
222
+ git clone git@github.com:nicofirst1/timbro.git && cd timbro
223
+ uv sync # pulls deps + the en_core_web_sm POS model (no manual spacy download)
224
+
225
+ uv run timbro score draft.md # runs immediately on the packaged sample voice
226
+
227
+ # to use your own voice, bring a corpus (both dirs are gitignored — your writing stays private)
228
+ mkdir -p data/exemplars data/contrast
229
+ # data/exemplars/ → posts that define your (or your company's) voice — 6+ pieces
230
+ # data/contrast/ → other authors' posts (the "not-our-voice" set), optional but sharpens it
231
+
232
+ TIMBRO_EXEMPLARS=data/exemplars TIMBRO_CONTRAST=data/contrast uv run timbro score draft.md
233
+ uv run python eval/harness.py data/exemplars data/contrast # confirm it separates your voice
234
+ ```
235
+
236
+ The two sentence-transformer models download from Hugging Face on first use. Everything runs **local and CPU-only** at inference — no API calls.
237
+
238
+ ## FAQ
239
+
240
+ **My voice legitimately uses em-dashes — won't `slop` nag me?** By default it flags against zero, so yes. Add `timbro slop draft.md --profile <name>` to baseline the tells against your own corpus instead: a tell is flagged only where the draft _overuses_ it relative to how you normally write. Absolute mode answers "is this AI-generated?"; `--profile` answers "is this driftier than my own writing?".
241
+
242
+ **Do I need the contrast set?** No, but it sharpens the direction — without it, every feature looks equally informative.
243
+
244
+ **Will it work on one author / a whole company?** Both. The "voice" is whatever you put in `data/exemplars/`. Mixed registers (blogs + papers) are fine — the scorer is multi-modal.
245
+
246
+ **Can I keep several directions (academic vs. slop, clear vs. jargon)?** Yes — one folder pair per dimension, selected by env var. Profiles live under `~/.timbro/profiles/<name>/{exemplars,contrast}/` by default (override with `TIMBRO_PROFILE_ROOT`). Point the env vars at the one you want for a given task:
247
+
248
+ ```bash
249
+ P=~/.timbro/profiles/academic
250
+ TIMBRO_EXEMPLARS=$P/exemplars TIMBRO_CONTRAST=$P/contrast uv run timbro score draft.md
251
+ ```
252
+
253
+ No code, no flags — collect good/bad examples per dimension and swap the two paths. If you want Timbro to scaffold and manage the local profile layout for you, use the `timbro profiles ...` commands below.
254
+
255
+ **Can Timbro create and manage profiles for me?** Yes. Use the built-in profile helpers to scaffold a profile, describe it, add files, and print the right env vars:
256
+
257
+ ```bash
258
+ uv run timbro profiles init science-clarity --about "Plain-language scientific explanation."
259
+ uv run timbro profiles add-file science-clarity notes/pvalue.md --to exemplars
260
+ uv run timbro profiles add-file science-clarity sloppy-example.md --to contrast
261
+ uv run timbro profiles add-file science-clarity paper.tex --to exemplars
262
+ uv run timbro profiles env science-clarity
263
+ ```
264
+
265
+ Programmatically:
266
+
267
+ ```python
268
+ from timbro.profiles import init_profile, add_file
269
+
270
+ profile = init_profile("science-clarity", about="Plain-language scientific explanation.")
271
+ add_file("science-clarity", "notes/pvalue.md", bucket="exemplars")
272
+ add_file("science-clarity", "paper.tex", bucket="exemplars")
273
+ print(profile.env)
274
+ ```
275
+
276
+ If `detex` is installed, `.tex` files are converted on ingest and raw LaTeX is normalized automatically during scoring.
277
+
278
+ For scoring, prefer profile-native selection over manual env vars:
279
+
280
+ ```bash
281
+ uv run timbro score draft.md --profile science-clarity
282
+ uv run timbro score draft.md --profile science-clarity,academic
283
+ ```
284
+
285
+ **Does it rewrite for me?** No, and that's deliberate. Timbro _measures_; your agent rewrites and Timbro judges the result (closer to voice **and** same meaning). Keeps the scoring honest and local.
286
+
287
+ ## Layout
288
+
289
+ ```
290
+ src/timbro/
291
+ ├── model.py # corpus → POS features + StyleDistance embedding → VoiceModel
292
+ ├── text.py # shared substrate: split_paragraphs/_sentences, strip_markup, MiniLM embedder
293
+ ├── flow.py # paragraph trajectory, circle-back, order gates
294
+ ├── rewrite.py # content-preservation guard + accept-rewrite loop
295
+ ├── report.py # the shared {distance, direction, flow} payload
296
+ ├── tells.py # AI-tell detectors (regex + POS); feed the `slop` rubric and the score direction
297
+ ├── rubrics/ # `check` (schimel/density) + `slop` (tells) rubrics: features + rules + registry
298
+ ├── cleanup/ # ingest-time corpus prep (LaTeX/paper extraction — not markdown)
299
+ ├── cli.py # `timbro score` + `timbro check` + `timbro slop`
300
+ └── mcp_server.py # MCP wrapper: score_voice, accept_rewrite, check_voice
301
+ skills/timbro/ # Claude Code skill
302
+ eval/harness.py # LOO-AUC, permutation baseline, direction sign test
303
+ eval/rubric_dashboard.py # per-rule findings-per-1000-words on known-good prose
304
+ eval/slop_benchmark.py # slop hit rate / false-positive rate on a small LLM/human corpus
305
+ ```
306
+
307
+ ## Contributing
308
+
309
+ See [CONTRIBUTING.md](CONTRIBUTING.md).