chorus-discourse 0.3.1__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 (39) hide show
  1. chorus_discourse-0.3.1/CHANGELOG.md +46 -0
  2. chorus_discourse-0.3.1/LICENSE +110 -0
  3. chorus_discourse-0.3.1/MANIFEST.in +2 -0
  4. chorus_discourse-0.3.1/PKG-INFO +135 -0
  5. chorus_discourse-0.3.1/README.md +125 -0
  6. chorus_discourse-0.3.1/docs/releases/0.3.0.md +64 -0
  7. chorus_discourse-0.3.1/pyproject.toml +29 -0
  8. chorus_discourse-0.3.1/setup.cfg +4 -0
  9. chorus_discourse-0.3.1/src/chorus/__init__.py +10 -0
  10. chorus_discourse-0.3.1/src/chorus/__main__.py +5 -0
  11. chorus_discourse-0.3.1/src/chorus/cli.py +223 -0
  12. chorus_discourse-0.3.1/src/chorus/corpora.py +71 -0
  13. chorus_discourse-0.3.1/src/chorus/daemon.py +146 -0
  14. chorus_discourse-0.3.1/src/chorus/decision.py +648 -0
  15. chorus_discourse-0.3.1/src/chorus/flagship.py +76 -0
  16. chorus_discourse-0.3.1/src/chorus/item.py +65 -0
  17. chorus_discourse-0.3.1/src/chorus/mcp.py +168 -0
  18. chorus_discourse-0.3.1/src/chorus/model.py +32 -0
  19. chorus_discourse-0.3.1/src/chorus/receipt.py +178 -0
  20. chorus_discourse-0.3.1/src/chorus/sentiment.py +135 -0
  21. chorus_discourse-0.3.1/src/chorus/synthesize.py +311 -0
  22. chorus_discourse-0.3.1/src/chorus_discourse.egg-info/PKG-INFO +135 -0
  23. chorus_discourse-0.3.1/src/chorus_discourse.egg-info/SOURCES.txt +37 -0
  24. chorus_discourse-0.3.1/src/chorus_discourse.egg-info/dependency_links.txt +1 -0
  25. chorus_discourse-0.3.1/src/chorus_discourse.egg-info/entry_points.txt +2 -0
  26. chorus_discourse-0.3.1/src/chorus_discourse.egg-info/top_level.txt +1 -0
  27. chorus_discourse-0.3.1/tests/test_cli.py +150 -0
  28. chorus_discourse-0.3.1/tests/test_cluster.py +33 -0
  29. chorus_discourse-0.3.1/tests/test_corpora.py +47 -0
  30. chorus_discourse-0.3.1/tests/test_daemon.py +119 -0
  31. chorus_discourse-0.3.1/tests/test_decision.py +431 -0
  32. chorus_discourse-0.3.1/tests/test_item.py +24 -0
  33. chorus_discourse-0.3.1/tests/test_mcp.py +138 -0
  34. chorus_discourse-0.3.1/tests/test_model_pass.py +93 -0
  35. chorus_discourse-0.3.1/tests/test_receipt.py +215 -0
  36. chorus_discourse-0.3.1/tests/test_repo_art.py +272 -0
  37. chorus_discourse-0.3.1/tests/test_sentiment.py +39 -0
  38. chorus_discourse-0.3.1/tests/test_synthesize.py +152 -0
  39. chorus_discourse-0.3.1/tests/test_version_alignment.py +50 -0
@@ -0,0 +1,46 @@
1
+ # Changelog
2
+
3
+ ## 0.3.1 - 2026-09-22
4
+
5
+ - Fixes the gather-to-chorus wiring gap. `normalize()` mapped only `feed_item`,
6
+ so feed entries from a real gather corpus were dropped without a diagnostic and
7
+ only YouTube comments reached the discourse surface. `feed-entry` now maps to
8
+ the `feed` source, and `feed_item` is kept for back-compat.
9
+ - Adds an OIDC trusted-publishing release workflow with tag/version, artifact
10
+ digest, clean-venv entry-point resolution, and sdist-rebuild gates.
11
+ - Publishes to PyPI as `chorus-discourse`. The console script stays `chorus`.
12
+
13
+ ## 0.3.0 - 2026-09-07
14
+
15
+ - Adds `chorus decision` and MCP `chorus.decision`, a source-change review gate
16
+ that compares current and reference gather-style source packs, reports
17
+ added/removed/changed/unchanged item ids, verifies both deterministic Chorus
18
+ digests, and returns typed `UNVERIFIABLE` failure states for missing, malformed,
19
+ empty, or ambiguous source inputs.
20
+ - Adds a safe public projection for the review-gate result. It keeps counts,
21
+ hashes, receipts, and limitations by default; human-readable public ids, source
22
+ names, refs, and URLs require an operator-authored projection policy rather
23
+ than source-row metadata. It excludes raw source text, author names, local paths,
24
+ private sessions, and bulk comments.
25
+ - Hardens false-success controls for source packs: missing or malformed text,
26
+ invalid, missing, mismatched, or non-UTF-8 Gather objects, duplicate ids, and
27
+ absent, null, blank, or non-string ids return typed `UNVERIFIABLE` instead of
28
+ being compared as unchanged.
29
+
30
+ This release does not decide whether a source claim is true, complete, or ready
31
+ for publication. `MATCH` means the compared source observations and fingerprints
32
+ are unchanged; `DRIFT` means a reviewer should inspect the changed source rows;
33
+ `UNVERIFIABLE` means source capture or identity must be repaired before reuse.
34
+
35
+ ## 0.2.0 - 2026-09-07
36
+
37
+ - Adds deterministic corpus-salience theme labels with `label_quality` support metadata so weak and singleton labels are visible in the digest.
38
+ - Writes new receipts as `chorus-lens/3`, binding label terms and `label_quality` into the re-checkable digest body.
39
+ - Preserves historical `chorus-lens/2` verification through an explicit legacy verifier. v2 receipts verify against the v2 body shape and do not bind the current v3-only `terms` and `label_quality` fields.
40
+ - Fixes receipt verification so the submitted digest body must match its receipt before the verifier accepts a re-derived body.
41
+
42
+ This release does not claim better semantic insight from comment corpora. It reduces generic-label false success on the included controls and labels weak support, while lexical clustering remains a known limitation.
43
+
44
+ ## 0.1.0
45
+
46
+ - Initial discourse synthesis package with CLI, MCP stdio surface, daemon, deterministic sentiment, lexical clustering, contested aspects, and re-checkable receipts.
@@ -0,0 +1,110 @@
1
+ # Functional Source License, Version 1.1, MIT Future License
2
+
3
+ ## Abbreviation
4
+
5
+ FSL-1.1-MIT
6
+
7
+ ## Notice
8
+
9
+ Copyright 2026 Zain Dana Harper
10
+
11
+ ## Terms and Conditions
12
+
13
+ ### Licensor ("We")
14
+
15
+ The party offering the Software under these Terms and Conditions.
16
+
17
+ ### The Software
18
+
19
+ The "Software" is each version of the software that we make available under
20
+ these Terms and Conditions, as indicated by our inclusion of these Terms and
21
+ Conditions with the Software.
22
+
23
+ ### License Grant
24
+
25
+ Subject to your compliance with this License Grant and the Patents,
26
+ Redistribution and Trademark clauses below, we hereby grant you the right to
27
+ use, copy, modify, create derivative works, publicly perform, publicly display
28
+ and redistribute the Software for any Permitted Purpose identified below.
29
+
30
+ ### Permitted Purpose
31
+
32
+ A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
33
+ means making the Software available to others in a commercial product or
34
+ service that:
35
+
36
+ 1. substitutes for the Software;
37
+
38
+ 2. substitutes for any other product or service we offer using the Software
39
+ that exists as of the date we make the Software available; or
40
+
41
+ 3. offers the same or substantially similar functionality as the Software.
42
+
43
+ Permitted Purposes specifically include using the Software:
44
+
45
+ 1. for your internal use and access;
46
+
47
+ 2. for non-commercial education;
48
+
49
+ 3. for non-commercial research; and
50
+
51
+ 4. in connection with professional services that you provide to a licensee
52
+ using the Software in accordance with these Terms and Conditions.
53
+
54
+ ### Patents
55
+
56
+ To the extent your use for a Permitted Purpose would necessarily infringe our
57
+ patents, the license grant above includes a license under our patents. If you
58
+ make a claim against any party that the Software infringes or contributes to
59
+ the infringement of any patent, then your patent license to the Software ends
60
+ immediately.
61
+
62
+ ### Redistribution
63
+
64
+ The Terms and Conditions apply to all copies, modifications and derivatives of
65
+ the Software.
66
+
67
+ If you redistribute any copies, modifications or derivatives of the Software,
68
+ you must include a copy of or a link to these Terms and Conditions and not
69
+ remove any copyright notices provided in or with the Software.
70
+
71
+ ### Disclaimer
72
+
73
+ THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
74
+ IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
75
+ PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
76
+
77
+ IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
78
+ SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
79
+ EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
80
+
81
+ ### Trademarks
82
+
83
+ Except for displaying the License Details and identifying us as the origin of
84
+ the Software, you have no right under these Terms and Conditions to use our
85
+ trademarks, trade names, service marks or product names.
86
+
87
+ ## Grant of Future License
88
+
89
+ We hereby irrevocably grant you an additional license to use the Software under
90
+ the MIT license that is effective on the second anniversary of the date we make
91
+ the Software available. On or after that date, you may use the Software under
92
+ the MIT license, in which case the following will apply:
93
+
94
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
95
+ this software and associated documentation files (the "Software"), to deal in
96
+ the Software without restriction, including without limitation the rights to
97
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
98
+ of the Software, and to permit persons to whom the Software is furnished to do
99
+ so, subject to the following conditions:
100
+
101
+ The above copyright notice and this permission notice shall be included in all
102
+ copies or substantial portions of the Software.
103
+
104
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
105
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
106
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
107
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
108
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
109
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
110
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ include CHANGELOG.md
2
+ recursive-include docs/releases *.md
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.4
2
+ Name: chorus-discourse
3
+ Version: 0.3.1
4
+ Summary: A discourse-synthesis satellite for gather: weighted, clustered, re-checkable readings of comment corpora.
5
+ License-Expression: LicenseRef-FSL-1.1-MIT
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Dynamic: license-file
10
+
11
+ # chorus
12
+
13
+ ![chorus](docs/art/chorus-header.svg)
14
+
15
+ Read a comment section the way you wish you could: not scrolled, but **synthesized**.
16
+
17
+ chorus takes a corpus of comments or threads and returns a weighted, clustered,
18
+ re-checkable reading of the discourse. It tells you the themes people are actually
19
+ voicing, ranks them by how much the crowd engaged and how strongly they felt,
20
+ surfaces the sharpest dissent instead of hiding it behind an average, and names the
21
+ topics the crowd is genuinely **split** on. Every digest carries a receipt a
22
+ stranger can re-run to get the same answer. Zero third-party runtime dependencies.
23
+
24
+ It orbits [gather](https://github.com/HarperZ9/gather): gather captures the corpus
25
+ with provenance, chorus synthesizes the discourse on top of it.
26
+
27
+ ![Eight stages of turning a corpus of comments into a discourse digest: normalize, engagement, score, weight, cluster, themes, contested, and receipt. Gathered rows become discourse items, and rows that are not discourse are skipped. Engagement is read from the source when it is present; when a source genuinely has no signal, engagement is zero and the absence is recorded, so a missing vote is never counted as a real zero-weight one. Sentiment comes from a thirty word lexicon with negation, intensifier, capitalization and punctuation rules, and the same text always scores the same. Weight is the natural log of one plus engagement, multiplied by one plus half the sentiment intensity, so a loud comment nobody engaged with stays small. Clustering is a hashed TF-IDF cosine against the nearest leader across five hundred and twelve dimensions, seeded most-engaged first. Each theme carries its size, its sentiment split, its controversy, and the single highest-weight voice that disagrees with the majority. Contested aspects are measured separately across every comment that mentions a term, so a topic the corpus is split on survives the clustering that would file praise and complaint about it under different themes. The receipt hashes the inputs, the parameters and the digest body. Three outcomes: re-derived, rejected, and advisory only.](docs/art/synthesis-lane.svg)
28
+
29
+ ## What you get
30
+
31
+ - **Themes, ranked.** Comments cluster into themes by what they say; each theme
32
+ carries a corpus-salience label, label-support metadata, its size, an
33
+ engagement-and-sentiment weight, a sentiment split, a *controversy* score (how
34
+ divided and how strongly felt), and the single highest-weight voice that
35
+ disagrees with the majority. Singleton and weak-support labels are named as
36
+ such in the digest instead of being presented as broad crowd themes. Theme item
37
+ IDs stay in the output so callers can resolve labels back to the source links
38
+ and provenance in their corpus.
39
+ - **The contested topics, named.** A separate lens reports the aspects the corpus
40
+ is genuinely split on, measured across *every* comment that mentions a topic. It
41
+ is immune to the lexical clustering that would otherwise file "the battery is
42
+ amazing" and "the battery is terrible" under different themes and hide the fight.
43
+ One-sided praise and neutral chatter are excluded; only real two-sided
44
+ disagreement is surfaced.
45
+ - **A receipt, not a vibe.** `--verify` re-derives the whole digest from the inputs
46
+ and confirms it. A tampered digest fails even if its own hash was recomputed to
47
+ match. Sentiment is a *weight*, never a verdict.
48
+ - **Honest nulls.** A missing engagement signal is recorded as absent, never
49
+ counted as a zero. A too-thin corpus says so. The lexicon's limits (English-only,
50
+ literal, no sarcasm) are stated in the digest itself.
51
+ - **A daemon.** Point it at a watchlist and it re-synthesizes only when a corpus
52
+ actually changes, storing each receipted digest by its own hash.
53
+ - **A source-change review gate.** Compare a current gather corpus against a
54
+ reference corpus before reusing a prior synthesis or release note. The result
55
+ reports added, removed, changed, unchanged, digest verification, a local digest
56
+ outline, and typed source failure states. The optional public projection is
57
+ safe by default as hashes and counts. Human-readable public ids, source names,
58
+ refs, and URLs require an operator-authored `--public-policy` sidecar; raw
59
+ source row metadata cannot authorize public output. The projection omits raw
60
+ source text, author names, local paths, private session content, and bulk
61
+ comments. Source URLs appear there only through that public-policy sidecar.
62
+ - **An MCP surface.** Drive it from any MCP host: `chorus.run`, `chorus.corpora`,
63
+ `chorus.digests`, `chorus.decision`, `chorus.status`, `chorus.doctor`.
64
+
65
+ ## Release notes
66
+
67
+ See [CHANGELOG.md](CHANGELOG.md). Version 0.3.0 adds `chorus decision` and MCP `chorus.decision` as a source-change review gate for deciding whether a prior synthesis or release note can reuse the same source observations, needs source review, or must hold for source repair. It does not decide whether a source claim is true, complete, or ready for publication. Version 0.2.0 writes `chorus-lens/3` receipts with label-support metadata while preserving historical `chorus-lens/2` verification through an explicit legacy path. v2 receipts do not bind the current v3-only label terms or `label_quality` fields.
68
+
69
+ ## Run it
70
+
71
+ ```bash
72
+ pip install -e .
73
+
74
+ chorus run examples/discourse-sample.json --verify # try it on the bundled sample
75
+ chorus run <corpus> --verify # a corpus -> a verified discourse digest
76
+ chorus decision <current> --reference <reference> --task "Check whether sources changed"
77
+ chorus decision <current> --reference <reference> --public --public-policy public-policy.json
78
+ chorus corpora <root> # discover gather corpora as discourse sources
79
+ chorus watch add <corpus> # add a corpus to the daemon watchlist
80
+ chorus daemon --interval 300 # poll the watchlist, synthesize on change
81
+ chorus digests <store> # what the daemon has synthesized
82
+ chorus mcp # the MCP stdio server
83
+ ```
84
+
85
+ `<corpus>` is a gather corpus directory (a folder holding `catalog.jsonl`) or a
86
+ JSON list of rows. Add `--model "<command>"` to `run` to overlay a model's read on
87
+ the comments the lexicon is least sure about; the overlay is provenance-tagged and
88
+ never enters the re-checkable core.
89
+
90
+ No service key or provider account is needed for the deterministic path. The
91
+ bundled sample is the quickest local check:
92
+
93
+ ```bash
94
+ chorus run examples/discourse-sample.json --verify
95
+ python -m pytest
96
+ ```
97
+
98
+ If `chorus` is not on `PATH`, reinstall from the checkout with
99
+ `python -m pip install -e .` and open a new shell. `chorus.status` and
100
+ `chorus.doctor` are MCP tools exposed through `chorus mcp`; they are not separate
101
+ CLI subcommands. Public source-change projections need an operator-authored
102
+ `--public-policy` sidecar before source names, ids, refs, or URLs appear.
103
+
104
+ ![Eight stages of verifying a discourse digest: receipt, version, vocabulary, inputs, rescore, recluster, rehash, and verdict. The receipt supplies the parameters and hashes the original run recorded. The method version selects the verifier for that pipeline; unsupported versions fail instead of being guessed. The lexicon is hashed into the receipt, so editing the word list invalidates every digest that was built with the old one. The corpus hash is checked before any work is done. Then the stored sentiment is thrown away and every comment is re-scored from its own text, which is why fabricated sentiment cannot verify. Clustering and weighting re-run from the parameters the receipt recorded, not from the live defaults, so raising a default cannot silently break an already-versioned receipt. The digest body is rebuilt from that re-derivation and hashed. The verdict is one boolean with nothing taken on trust: a digest whose themes, weights or sentiment distribution do not follow from the inputs fails, even when its own stored hash was recomputed to match its tampered body. Three outcomes: verified, tampered, and no receipt.](docs/art/verify-lane.svg)
105
+
106
+ ## The receipt
107
+
108
+ The digest's `receipt` binds the inputs, the method, and the result. `verify`
109
+ first checks that the submitted digest body still matches its own receipt, then
110
+ re-runs the deterministic pipeline (score, cluster, weight) from the same corpus
111
+ through the method-version verifier recorded in the receipt. Current runs write
112
+ `chorus-lens/3`; historical `chorus-lens/2` receipts remain checkable through
113
+ their legacy label body. Unsupported method versions fail closed. Any model
114
+ overlay is listed separately with its own provenance and is excluded from that
115
+ check: the parts a stranger can re-derive and the parts that are model opinion
116
+ are kept distinct, on the record.
117
+
118
+ ![Twelve rows covering every number a digest reports and where it comes from. The lexicon is thirty words, fifteen positive and fifteen negative, each carrying a valence between minus three and plus three, and the whole list is hashed into every receipt. Ten intensifiers are looked for up to three tokens back, widening or narrowing a valence. Nine negators flip a valence and keep about three quarters of its size, so a negation weakens a claim rather than erasing it. The compound score runs from minus one to one, the summed valence divided by the root of itself squared plus fifteen, so no single loud comment runs away with a theme. Two emphases apply: an all-caps valence word of more than one letter counts a quarter more, and up to four exclamation marks add five percent each. Weight is the log of one plus engagement, times one plus half the sentiment intensity. Clustering runs a hashed TF-IDF cosine over five hundred and twelve dimensions against the nearest leader, joining above eighteen hundredths, seeded most-engaged first. Controversy is the population standard deviation of a theme's sentiment, zero at consensus and near one at a hard split. Twelve contested aspects are surfaced, each needing three mentioning voices and real disagreement on both sides. The receipt carries seven fields. The accented row is the model overlay, which is advisory opinion on the items the lexicon is least sure of and is deliberately kept outside the digest hash. The last row is engagement coverage, which reports how many items actually carried a signal, because an absent signal is recorded absent rather than counted as a zero.](docs/art/method-table.svg)
119
+
120
+ ## Design
121
+
122
+ The design and its two exposed defects-caught-in-review live in
123
+ [docs/superpowers/specs](docs/superpowers/specs). Sentiment is coarse by
124
+ construction and the digest says so; clustering is lexical, not semantic. chorus
125
+ tells you what it did and hands you the means to check it.
126
+
127
+ ## License
128
+
129
+ Source-available under the Functional Source License (FSL-1.1-MIT) (see [LICENSE](LICENSE)):
130
+ read it, run it, build on it; commercial use that competes with the project is
131
+ reserved.
132
+
133
+ ---
134
+
135
+ **[Zentropy Labs](https://github.com/ZentropyLabs-ai)** · order out of entropy. An independent lab building evidence-first tools that leave a re-checkable artifact behind. Built by Zain Dana Harper in Seattle. The full workbench is at [Project Telos](https://harperz9.github.io).
@@ -0,0 +1,125 @@
1
+ # chorus
2
+
3
+ ![chorus](docs/art/chorus-header.svg)
4
+
5
+ Read a comment section the way you wish you could: not scrolled, but **synthesized**.
6
+
7
+ chorus takes a corpus of comments or threads and returns a weighted, clustered,
8
+ re-checkable reading of the discourse. It tells you the themes people are actually
9
+ voicing, ranks them by how much the crowd engaged and how strongly they felt,
10
+ surfaces the sharpest dissent instead of hiding it behind an average, and names the
11
+ topics the crowd is genuinely **split** on. Every digest carries a receipt a
12
+ stranger can re-run to get the same answer. Zero third-party runtime dependencies.
13
+
14
+ It orbits [gather](https://github.com/HarperZ9/gather): gather captures the corpus
15
+ with provenance, chorus synthesizes the discourse on top of it.
16
+
17
+ ![Eight stages of turning a corpus of comments into a discourse digest: normalize, engagement, score, weight, cluster, themes, contested, and receipt. Gathered rows become discourse items, and rows that are not discourse are skipped. Engagement is read from the source when it is present; when a source genuinely has no signal, engagement is zero and the absence is recorded, so a missing vote is never counted as a real zero-weight one. Sentiment comes from a thirty word lexicon with negation, intensifier, capitalization and punctuation rules, and the same text always scores the same. Weight is the natural log of one plus engagement, multiplied by one plus half the sentiment intensity, so a loud comment nobody engaged with stays small. Clustering is a hashed TF-IDF cosine against the nearest leader across five hundred and twelve dimensions, seeded most-engaged first. Each theme carries its size, its sentiment split, its controversy, and the single highest-weight voice that disagrees with the majority. Contested aspects are measured separately across every comment that mentions a term, so a topic the corpus is split on survives the clustering that would file praise and complaint about it under different themes. The receipt hashes the inputs, the parameters and the digest body. Three outcomes: re-derived, rejected, and advisory only.](docs/art/synthesis-lane.svg)
18
+
19
+ ## What you get
20
+
21
+ - **Themes, ranked.** Comments cluster into themes by what they say; each theme
22
+ carries a corpus-salience label, label-support metadata, its size, an
23
+ engagement-and-sentiment weight, a sentiment split, a *controversy* score (how
24
+ divided and how strongly felt), and the single highest-weight voice that
25
+ disagrees with the majority. Singleton and weak-support labels are named as
26
+ such in the digest instead of being presented as broad crowd themes. Theme item
27
+ IDs stay in the output so callers can resolve labels back to the source links
28
+ and provenance in their corpus.
29
+ - **The contested topics, named.** A separate lens reports the aspects the corpus
30
+ is genuinely split on, measured across *every* comment that mentions a topic. It
31
+ is immune to the lexical clustering that would otherwise file "the battery is
32
+ amazing" and "the battery is terrible" under different themes and hide the fight.
33
+ One-sided praise and neutral chatter are excluded; only real two-sided
34
+ disagreement is surfaced.
35
+ - **A receipt, not a vibe.** `--verify` re-derives the whole digest from the inputs
36
+ and confirms it. A tampered digest fails even if its own hash was recomputed to
37
+ match. Sentiment is a *weight*, never a verdict.
38
+ - **Honest nulls.** A missing engagement signal is recorded as absent, never
39
+ counted as a zero. A too-thin corpus says so. The lexicon's limits (English-only,
40
+ literal, no sarcasm) are stated in the digest itself.
41
+ - **A daemon.** Point it at a watchlist and it re-synthesizes only when a corpus
42
+ actually changes, storing each receipted digest by its own hash.
43
+ - **A source-change review gate.** Compare a current gather corpus against a
44
+ reference corpus before reusing a prior synthesis or release note. The result
45
+ reports added, removed, changed, unchanged, digest verification, a local digest
46
+ outline, and typed source failure states. The optional public projection is
47
+ safe by default as hashes and counts. Human-readable public ids, source names,
48
+ refs, and URLs require an operator-authored `--public-policy` sidecar; raw
49
+ source row metadata cannot authorize public output. The projection omits raw
50
+ source text, author names, local paths, private session content, and bulk
51
+ comments. Source URLs appear there only through that public-policy sidecar.
52
+ - **An MCP surface.** Drive it from any MCP host: `chorus.run`, `chorus.corpora`,
53
+ `chorus.digests`, `chorus.decision`, `chorus.status`, `chorus.doctor`.
54
+
55
+ ## Release notes
56
+
57
+ See [CHANGELOG.md](CHANGELOG.md). Version 0.3.0 adds `chorus decision` and MCP `chorus.decision` as a source-change review gate for deciding whether a prior synthesis or release note can reuse the same source observations, needs source review, or must hold for source repair. It does not decide whether a source claim is true, complete, or ready for publication. Version 0.2.0 writes `chorus-lens/3` receipts with label-support metadata while preserving historical `chorus-lens/2` verification through an explicit legacy path. v2 receipts do not bind the current v3-only label terms or `label_quality` fields.
58
+
59
+ ## Run it
60
+
61
+ ```bash
62
+ pip install -e .
63
+
64
+ chorus run examples/discourse-sample.json --verify # try it on the bundled sample
65
+ chorus run <corpus> --verify # a corpus -> a verified discourse digest
66
+ chorus decision <current> --reference <reference> --task "Check whether sources changed"
67
+ chorus decision <current> --reference <reference> --public --public-policy public-policy.json
68
+ chorus corpora <root> # discover gather corpora as discourse sources
69
+ chorus watch add <corpus> # add a corpus to the daemon watchlist
70
+ chorus daemon --interval 300 # poll the watchlist, synthesize on change
71
+ chorus digests <store> # what the daemon has synthesized
72
+ chorus mcp # the MCP stdio server
73
+ ```
74
+
75
+ `<corpus>` is a gather corpus directory (a folder holding `catalog.jsonl`) or a
76
+ JSON list of rows. Add `--model "<command>"` to `run` to overlay a model's read on
77
+ the comments the lexicon is least sure about; the overlay is provenance-tagged and
78
+ never enters the re-checkable core.
79
+
80
+ No service key or provider account is needed for the deterministic path. The
81
+ bundled sample is the quickest local check:
82
+
83
+ ```bash
84
+ chorus run examples/discourse-sample.json --verify
85
+ python -m pytest
86
+ ```
87
+
88
+ If `chorus` is not on `PATH`, reinstall from the checkout with
89
+ `python -m pip install -e .` and open a new shell. `chorus.status` and
90
+ `chorus.doctor` are MCP tools exposed through `chorus mcp`; they are not separate
91
+ CLI subcommands. Public source-change projections need an operator-authored
92
+ `--public-policy` sidecar before source names, ids, refs, or URLs appear.
93
+
94
+ ![Eight stages of verifying a discourse digest: receipt, version, vocabulary, inputs, rescore, recluster, rehash, and verdict. The receipt supplies the parameters and hashes the original run recorded. The method version selects the verifier for that pipeline; unsupported versions fail instead of being guessed. The lexicon is hashed into the receipt, so editing the word list invalidates every digest that was built with the old one. The corpus hash is checked before any work is done. Then the stored sentiment is thrown away and every comment is re-scored from its own text, which is why fabricated sentiment cannot verify. Clustering and weighting re-run from the parameters the receipt recorded, not from the live defaults, so raising a default cannot silently break an already-versioned receipt. The digest body is rebuilt from that re-derivation and hashed. The verdict is one boolean with nothing taken on trust: a digest whose themes, weights or sentiment distribution do not follow from the inputs fails, even when its own stored hash was recomputed to match its tampered body. Three outcomes: verified, tampered, and no receipt.](docs/art/verify-lane.svg)
95
+
96
+ ## The receipt
97
+
98
+ The digest's `receipt` binds the inputs, the method, and the result. `verify`
99
+ first checks that the submitted digest body still matches its own receipt, then
100
+ re-runs the deterministic pipeline (score, cluster, weight) from the same corpus
101
+ through the method-version verifier recorded in the receipt. Current runs write
102
+ `chorus-lens/3`; historical `chorus-lens/2` receipts remain checkable through
103
+ their legacy label body. Unsupported method versions fail closed. Any model
104
+ overlay is listed separately with its own provenance and is excluded from that
105
+ check: the parts a stranger can re-derive and the parts that are model opinion
106
+ are kept distinct, on the record.
107
+
108
+ ![Twelve rows covering every number a digest reports and where it comes from. The lexicon is thirty words, fifteen positive and fifteen negative, each carrying a valence between minus three and plus three, and the whole list is hashed into every receipt. Ten intensifiers are looked for up to three tokens back, widening or narrowing a valence. Nine negators flip a valence and keep about three quarters of its size, so a negation weakens a claim rather than erasing it. The compound score runs from minus one to one, the summed valence divided by the root of itself squared plus fifteen, so no single loud comment runs away with a theme. Two emphases apply: an all-caps valence word of more than one letter counts a quarter more, and up to four exclamation marks add five percent each. Weight is the log of one plus engagement, times one plus half the sentiment intensity. Clustering runs a hashed TF-IDF cosine over five hundred and twelve dimensions against the nearest leader, joining above eighteen hundredths, seeded most-engaged first. Controversy is the population standard deviation of a theme's sentiment, zero at consensus and near one at a hard split. Twelve contested aspects are surfaced, each needing three mentioning voices and real disagreement on both sides. The receipt carries seven fields. The accented row is the model overlay, which is advisory opinion on the items the lexicon is least sure of and is deliberately kept outside the digest hash. The last row is engagement coverage, which reports how many items actually carried a signal, because an absent signal is recorded absent rather than counted as a zero.](docs/art/method-table.svg)
109
+
110
+ ## Design
111
+
112
+ The design and its two exposed defects-caught-in-review live in
113
+ [docs/superpowers/specs](docs/superpowers/specs). Sentiment is coarse by
114
+ construction and the digest says so; clustering is lexical, not semantic. chorus
115
+ tells you what it did and hands you the means to check it.
116
+
117
+ ## License
118
+
119
+ Source-available under the Functional Source License (FSL-1.1-MIT) (see [LICENSE](LICENSE)):
120
+ read it, run it, build on it; commercial use that competes with the project is
121
+ reserved.
122
+
123
+ ---
124
+
125
+ **[Zentropy Labs](https://github.com/ZentropyLabs-ai)** · order out of entropy. An independent lab building evidence-first tools that leave a re-checkable artifact behind. Built by Zain Dana Harper in Seattle. The full workbench is at [Project Telos](https://harperz9.github.io).
@@ -0,0 +1,64 @@
1
+ # Chorus 0.3.0
2
+
3
+ Date: 2026-09-07
4
+
5
+ ## Useful benefit
6
+
7
+ Chorus 0.3.0 adds a source-change review gate through the CLI command
8
+ `chorus decision` and the MCP tool `chorus.decision`. A reviewer can compare a
9
+ current Gather-style source pack against the reference pack used for a prior
10
+ synthesis or release note and get a machine-readable result:
11
+
12
+ - `MATCH`: the compared source observations and fingerprints are unchanged.
13
+ - `DRIFT`: one or more source rows were added, removed, or changed, so the prior
14
+ synthesis or release note needs source review before reuse.
15
+ - `UNVERIFIABLE`: the source pack is missing, malformed, internally inconsistent,
16
+ or has invalid identities, so reuse should hold until source capture is
17
+ repaired.
18
+
19
+ The public projection is safe by default as hashes, counts, checks, limitations,
20
+ and policy provenance. Human-readable public ids, source names, refs, and URLs
21
+ require an operator-authored public projection policy. Raw source row metadata
22
+ cannot publish those fields.
23
+
24
+ ## Semantic limits
25
+
26
+ This release does not decide whether a source claim is true, complete, or ready
27
+ for publication. A `MATCH` result means the compared rows and fingerprints did not
28
+ change. It does not prove that the source set is representative, that no relevant
29
+ source exists elsewhere, that the implementation using the source is correct, or
30
+ that a publication should proceed. `DRIFT` is a review trigger. `UNVERIFIABLE` is
31
+ a source-repair trigger.
32
+
33
+ ## False-success controls
34
+
35
+ Regression checks cover the observed failure modes:
36
+
37
+ - missing paths and malformed JSON return typed `UNVERIFIABLE` results;
38
+ - missing, blank, non-string, or null source text returns typed `UNVERIFIABLE`;
39
+ - invalid, missing, mismatched, or non-UTF-8 Gather content objects return typed
40
+ `UNVERIFIABLE`;
41
+ - duplicate ids and absent, null, blank, bool, numeric, list, or dict ids return
42
+ typed `UNVERIFIABLE` before normalization can coerce them;
43
+ - source-controlled row metadata cannot authorize public ids, source names,
44
+ refs, or URLs;
45
+ - operator public projection policy can publish safe human-readable fields when
46
+ supplied deliberately by the caller.
47
+
48
+ ## Install and use
49
+
50
+ Download the wheel and checksums from the GitHub release. With Python 3.10 or
51
+ later, install the downloaded file and inspect the command's input options:
52
+
53
+ ```sh
54
+ python -m pip install ./chorus_discourse-0.3.0-py3-none-any.whl
55
+ chorus decision --help
56
+ ```
57
+
58
+ The package has no runtime dependencies. Existing MCP hosts can call
59
+ `chorus.decision` through `chorus mcp`. Keep input packs private unless they were
60
+ deliberately prepared for publication. A public policy authorizes only its
61
+ selected metadata; it does not authorize publishing raw source corpora.
62
+
63
+ GitHub downloads, host installation, and third-party marketplace approval are
64
+ separate distribution states. This release makes no marketplace approval claim.
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "chorus-discourse"
7
+ version = "0.3.1"
8
+ description = "A discourse-synthesis satellite for gather: weighted, clustered, re-checkable readings of comment corpora."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "LicenseRef-FSL-1.1-MIT"
12
+ license-files = ["LICENSE"]
13
+ dependencies = []
14
+
15
+ [project.scripts]
16
+ chorus = "chorus.cli:main"
17
+
18
+ [tool.setuptools.packages.find]
19
+ where = ["src"]
20
+
21
+ [tool.pytest.ini_options]
22
+ # Without this, pytest imports whatever copy of the package happens to be
23
+ # installed in the environment instead of this repo's src/. That is not a
24
+ # style preference: chorus was resolving to a stale editable install pointing
25
+ # at an old worktree, and plexus to a copy in site-packages, so both suites
26
+ # were green while testing source that was not this checkout.
27
+ pythonpath = ["src"]
28
+ testpaths = ["tests"]
29
+ addopts = "-q"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,10 @@
1
+ """chorus — a discourse-synthesis satellite for gather.
2
+
3
+ Turns a corpus of comments and threads into a weighted, clustered, re-checkable
4
+ reading of the discourse. Sentiment is a weight and a signal here, never an
5
+ accept gate. Stdlib only; deterministic; every digest carries a receipt.
6
+ """
7
+ from chorus.item import DiscourseItem, normalize
8
+
9
+ __version__ = "0.3.1"
10
+ __all__ = ["DiscourseItem", "normalize"]
@@ -0,0 +1,5 @@
1
+ import sys
2
+ from chorus.cli import main
3
+
4
+ if __name__ == "__main__":
5
+ sys.exit(main(sys.argv[1:]))