Skip to content

Commit 1394829

Browse files
committed
Release v3.5.0: serve the panel's conclusion, not only its concordance rate
The 2026-08-25 upload that added Qwen also added four columns nothing served: consensus_polarite, consensus_centralite, consensus_subjectivite_score and sentiment_disagreement. v3.4.0 left them alone because they are a new tool surface rather than part of serving a fifth model. This adds them. The reason they belong next to what v3.4.0 built is that they disagree with it about abstentions. agreement.scored_by_all counts articles EVERY model scored, so the 200 Qwen never annotated leave it entirely. The consensus threshold instead follows the votes actually cast, over half with a minimum of two, so those articles are still decided by the four models that did answer. The two are counted on different sets and their totals are not meant to reconcile; both now say so in the payload. get_sentiment_distribution takes model="consensus", and model="all" carries a consensus block alongside agreement so a comparison call answers both what the models said and what the panel concluded. consensus is NOT routed through resolveSentimentModel and is not a member of SENTIMENT_MODELS: no annotator produced these values, so admitting them to the registry would put a derived aggregate into a `model` field that everywhere else names the exact model that judged. A unit test pins that, along with the fact that "consensus" is not a retired handle either, having never named an annotator at all. The three fields do not behave alike, and treating them alike is the trap the implementation is shaped around. Polarity and centrality are majority labels whose empty value means NO MAJORITY FORMED, never "not computed": 429 and 465 articles respectively, each flagged in the dispute column. Those two populations partition the empties exactly, the rest being the 51 articles nobody scored. Subjectivity is a float median rank rather than a label, so it resolves whenever anyone voted and therefore covers MORE articles than either majority field, 12,195 against 11,869 and 11,833. An even number of voters puts it between two labels: 117 articles carry a half-rank of 1.5, 2.5, 3.5 or 4.5, which correspond to no label at all. It is reported as a numeric summary with its own note and never joined to SUBJECTIVITY_VALUES. search_by_sentiment gains a `disputed` filter over the pipe-joined dispute column, taking the stored French field names so that the English spellings fail loudly with valid_values rather than matching nothing. It turns the contested articles from a count into something readable, which matters most where a single model's label looks settled and is not. The comparison chart gets the consensus in its own panel row rather than among the model rings, where it would read as a sixth annotator, and a note stating its own base and that no model produced it. The fixture encodes each of these rather than restating them. Row 103 splits polarity two-two-one so no majority forms while centrality on the same row is unanimous, which is why the fields are stored apart. Row 104 is the row Qwen skipped: its polarity is still decided by the four remaining votes, so it sits in the consensus while being absent from agreement.scored_by_all, and row 103 is the reverse. Subjectivity resolves on all six fixture rows where the label fields reach five, so coverage.subjectivity > coverage.polarity is a shape a test protects. Half-ranks need an even split and cannot occur on a six-row fixture voting five-at-a-time without making the votes contradict each model's documented abstention policy, so the live smoke test covers them against the 117 real articles that have one, and also checks that the disputed filter returns exactly the count the aggregate reported. Token footprint 15,071 to 15,210 of a 16,000 ceiling, re-baselined.
1 parent d131b82 commit 1394829

16 files changed

Lines changed: 513 additions & 38 deletions

.agents/skills/iwac-mcp/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ Thirteen tools carry their own interactive chart on hosts that support MCP Apps
180180
**Actions:**
181181
1. Cross-reference MCP findings across subsets (articles vs. publications vs. references vs. documents vs. index)
182182
2. Use `get_sentiment_distribution` with `subject` filter to compare topic-specific sentiment against the collection baseline (e.g., `subject="Laïcité", country="Burkina Faso"` vs. the whole BF corpus)
183-
3. Before any sentiment claim carries weight, re-run it with `model="all"`. All five models agree unanimously on polarity for only **32%** of the corpus (3,929 of the 12,098 articles they all scored); where your slice diverges further, say so rather than quoting one model
183+
3. Before any sentiment claim carries weight, re-run it with `model="all"`. All five models agree unanimously on polarity for only **32%** of the corpus (3,929 of the 12,098 articles they all scored); where your slice diverges further, say so rather than quoting one model. That call now also returns the panel's `consensus`, which is what to quote when the claim is about the corpus rather than about a model — and `search_by_sentiment(disputed="polarite")` reads the 429 articles where no majority formed at all
184184
4. Use `search_articles` results (which include sentiment inline) to build topic-specific sentiment tables without extra calls
185185
5. Use `get_temporal_distribution` (optionally `group_by=country|newspaper`) to verify a claimed trend over time and compare trajectories across countries or outlets without paging. For an observance claim, `granularity="lunar_month"` is the test: it pools every year into the twelve lunar months, so a Ramadan or Dhu al-Hijja effect either stands above the even split or it does not
186186
6. Test whether a theme you have named is really distinct: `get_cooccurrence(field="subject")` shows what it is always discussed alongside, and `get_similar_items` on a key article shows whether your "finding" is one story reprinted across several outlets (scores ≥0.85)
@@ -215,7 +215,7 @@ Thirteen tools carry their own interactive chart on hosts that support MCP Apps
215215

216216
**For null results:** `Search for [term] in [tool] with [parameters] returned 0 results.`
217217

218-
**For AI sentiment findings:** Five models scored the corpus independently — `gpt-5-6-luna` (the default), `mistral-small-2603`, `deepseek-v4-flash-0731`, `gemma-4-31b-it` and `qwen3-8-27b`; the result keys are `polarity`, `centrality`, `subjectivity`. They do **not** all cover the same articles — `qwen3-8-27b` scores 12,098 where the rest score 12,298, a deliberate gap concentrated on articles peripheral to Islam — so read each model's `coverage` before comparing counts, and never present Qwen's shortfall as a defect. **Name the exact model in any reported figure** — the tools echo the model id precisely so a number can be attributed, and the generation-1 models these replaced are refused rather than substituted (`gemini` included: Google's generation-2 member is Gemma, a different model line, so the handle errors instead of resolving). Treat `subjectivity` as the ordinal **label** it is (Très objectif → Très subjectif), never as a percentage, and prefer not to report it at all: it is much less reliable than the other two scales. When comparing topics or countries, use `get_sentiment_distribution` with a `subject` filter for aggregate data, or tabulate the sentiment columns from `search_articles` results. See `references/biases-and-limitations.md` §4 before any sentiment claim carries an argument.
218+
**For AI sentiment findings:** Five models scored the corpus independently — `gpt-5-6-luna` (the default), `mistral-small-2603`, `deepseek-v4-flash-0731`, `gemma-4-31b-it` and `qwen3-8-27b`; the result keys are `polarity`, `centrality`, `subjectivity`. They do **not** all cover the same articles — `qwen3-8-27b` scores 12,098 where the rest score 12,298, a deliberate gap concentrated on articles peripheral to Islam — so read each model's `coverage` before comparing counts, and never present Qwen's shortfall as a defect. `model="consensus"` gives the panel's precomputed majority, which is **not a sixth model** and must be attributed to the panel rather than to any annotator; its empty values mean *no majority formed*, and its subjectivité is a float median rank rather than a label. **Name the exact model in any reported figure** — the tools echo the model id precisely so a number can be attributed, and the generation-1 models these replaced are refused rather than substituted (`gemini` included: Google's generation-2 member is Gemma, a different model line, so the handle errors instead of resolving). Treat `subjectivity` as the ordinal **label** it is (Très objectif → Très subjectif), never as a percentage, and prefer not to report it at all: it is much less reliable than the other two scales. When comparing topics or countries, use `get_sentiment_distribution` with a `subject` filter for aggregate data, or tabulate the sentiment columns from `search_articles` results. See `references/biases-and-limitations.md` §4 before any sentiment claim carries an argument.
219219

220220
**For the evidence-base ledger (open every synthesis with one):** Report three tiers separately — items **read in full**, items **triaged on an AI abstract/snippet only**, and matches **surveyed by count only**. Example: `Evidence base: 4 articles + 1 document read in full; 18 articles triaged on description_ai; 1,909 keyword matches surveyed by count.` This keeps full-text evidence visibly distinct from AI-derived (abstract/snippet) evidence, which carries weaker evidential status.
221221

.agents/skills/iwac-mcp/references/biases-and-limitations.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ Five models scored the corpus independently: **`gpt-5-6-luna`**, **`mistral-smal
4545

4646
**Always name the model behind a reported figure.** The columns name the exact model that produced them, and so do the tools; vendor shorthand (`chatgpt`, `mistral`, `deepseek`, `gemma`/`google`, `qwen`/`alibaba`) is accepted as input and resolves to the model id, which is what the payload echoes back.
4747

48+
**The panel's majority is available as `model="consensus"`, and it is not a sixth model.** No annotator produced it, so a consensus figure is attributed to *the panel*, never to a model. Prefer it to any single model's number when the claim is about the corpus rather than about a model, and prefer it to `agreement` when you want a conclusion rather than a concordance rate. Three things to carry with it:
49+
50+
- The majority follows the votes **actually cast** (over half, minimum two), so it decides articles that `agreement` discards — including the 200 Qwen never annotated. The two are counted on different sets; do not reconcile their totals.
51+
- An empty polarité or centralité consensus means **no majority formed** (429 and 465 articles), never "not computed". Those are the genuinely contested articles, and `search_by_sentiment(disputed="polarite")` reads them. A single model's label on such an article looks settled and is not, which makes them worth quoting *as* disagreement rather than averaging away.
52+
- The subjectivité consensus is a **float median rank, not a label**, so it resolves for more articles (12,195) than either majority field, and an even number of voters yields a half-rank (1.5, 2.5 …) matching no label at all. Never map it onto the five labels, and never report it as a percentage. It inherits every reliability caveat above: a median of weak judgements is still weak.
53+
54+
The panel splits on subjectivité far more than on anything else — 3,184 articles against 429 for polarité and 465 for centralité — which is the same finding as the κ table below, counted a different way.
55+
4856
**Generation 1 is gone from these tools.** An earlier campaign (`gemini-3-flash-preview`, `gpt-5-mini`, `ministral-14b-2512`) scored the same corpus and its columns survive on Hugging Face, but the server no longer serves them: asking for one returns an error rather than substituting the same vendor's generation-2 model. The two generations differ in model, in prompt AND in how subjectivity is stored, so a figure from one is not comparable to a figure from the other — if a source quotes a generation-1 number, say which generation it came from rather than presenting it alongside these. **`gemini` is refused too**, and that is not an oversight: Google's generation-2 member is Gemma 4 31B, a different model line, so reading `gemini` as `gemma-4-31b-it` would attribute one model's judgements to another.
4957

5058
### Scale Definitions

.agents/skills/iwac-mcp/references/tools-by-phase.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ Search the 4,697 authority records by name.
8686

8787
### search_by_sentiment
8888
Filter articles by **`gpt-5-6-luna`** sentiment labels (exact match, accents optional). One model's reading, not a consensus — say so when reporting, and use `get_sentiment_distribution(model="all")` to see how far the other four agree.
89+
90+
- `disputed` (optional, validated) *(v3.5.0+)*: `polarite` | `centralite` | `subjectivite` — keep only the articles the panel **split on** for that field. French field names, as stored; the English spellings error with `valid_values`. This is how you read the contested cases rather than merely counting them: `disputed="polarite"` returns the 429 articles where no majority formed, which are exactly the ones a single model's label would misrepresent as settled. Combines with the country, subject and date filters.
8991
- `polarity` (optional): Très positif | Positif | Neutre | Négatif | Très négatif | Non applicable
9092
- `centrality` (optional): Très central | Central | Secondaire | Marginal | Non abordé
9193
- `subjectivity` (optional): Très objectif | Plutôt objectif | Mixte | Plutôt subjectif | Très subjectif. Filterable since v2.0.0, when generation 2 turned this field from a 1-5 rating into a closed label vocabulary — a numeric value now errors. Unscored where the model answered `Non abordé`, so this filter excludes those rows too. It is the weakest of the three scales: use it to *find* articles to read, not to count them
@@ -228,11 +230,23 @@ Counts of matching items per year (or month) — one call replaces paging throug
228230
### get_sentiment_distribution *(`model` added v0.13.0; model-exact ids since the 2026-07-31 dataset rename)*
229231
Aggregated AI sentiment counts.
230232
- `country` (optional, exact name), `newspaper` (optional), `subject` (optional)
231-
- `model` (optional, validated): `gpt-5-6-luna` (default) | `mistral-small-2603` | `deepseek-v4-flash-0731` | `gemma-4-31b-it` | `qwen3-8-27b` | **all**. Vendor shorthand (`chatgpt` / `mistral` / `deepseek` / `gemma` or `google` / `qwen` or `alibaba`) resolves to these ids, which is what the payload echoes back — quote the id, never the vendor. The generation-1 ids (`gemini-3-flash-preview`, `gpt-5-mini`, `ministral-14b-2512`) are **refused by name**, not substituted, and so is bare `gemini`: Google's generation-2 member is Gemma, a different model line (`qwen` resolves, by contrast, because the Qwen line did score generation 2)
233+
- `model` (optional, validated): `gpt-5-6-luna` (default) | `mistral-small-2603` | `deepseek-v4-flash-0731` | `gemma-4-31b-it` | `qwen3-8-27b` | **all** | **consensus**. Vendor shorthand (`chatgpt` / `mistral` / `deepseek` / `gemma` or `google` / `qwen` or `alibaba`) resolves to these ids, which is what the payload echoes back — quote the id, never the vendor. The generation-1 ids (`gemini-3-flash-preview`, `gpt-5-mini`, `ministral-14b-2512`) are **refused by name**, not substituted, and so is bare `gemini`: Google's generation-2 member is Gemma, a different model line (`qwen` resolves, by contrast, because the Qwen line did score generation 2)
232234
- Returns `polarity_distribution`, `centrality_distribution` and `subjectivity` — a distribution over the five French labels plus `scored` / `unscored` and a `mean_rank` / `median_rank` derived by ranking the labels 1-5. That rank is a position on a five-point scale, never a percentage. The block also carries a `caveat`: quote it whenever you quote the number
233235
- With `model="all"`: `by_model` (each model's distributions), `agreement` (how often they concur on polarity — ten pairwise counts and one unanimous count for five models) and `agreement_matrix` (where the first two part company)
234236
- Every model block carries a `coverage` object, because the five do **not** score the same articles: `qwen3-8-27b` reaches 12,098 where the others reach 12,298. `agreement` is measured on the articles *all* of them scored, so its `scored_by_all` base — and every pairwise count in it, including pairs Qwen is not part of — is the smaller one, and `base_caveats` names why. Compare proportions, not raw counts, across models
235237

238+
### `model="consensus"` — what the panel concluded *(v3.5.0+)*
239+
240+
A precomputed majority, served as stored rather than derived at query time, and **not a sixth model**: no annotator produced it, so never attribute a consensus figure to one. It also rides along inside `model="all"` as a `consensus` block, so a comparison call answers both "how far do they agree" and "what did they conclude".
241+
242+
The majority threshold follows the votes **actually cast** (over half, minimum two). That is the reason to prefer it over `agreement`: `agreement` counts only articles *every* model scored, so the 200 Qwen skipped drop out of it entirely, while the consensus still decides them on the four remaining votes. The two are counted on different sets and their totals should not be reconciled.
243+
244+
The three fields do not behave alike, and this is the part to get right:
245+
246+
- `polarity_distribution` / `centrality_distribution` are **majority labels**. An empty value means **no majority formed** (429 and 465 articles), never "not computed". Those articles are absent from the distribution, so read `coverage` for the denominator.
247+
- `subjectivity_median_rank` is a **float median on the 1-5 scale, not a label**. A median resolves whenever anyone voted, so it covers *more* articles (12,195) than either majority field. An even number of voters yields a **half-rank** (1.5, 2.5 …) matching no label at all — 117 articles have one. Never map it back onto the five labels, and never quote it as a percentage.
248+
- `disputed` counts the fields the panel split on: polarité 429, centralité 465, subjectivité 3,184, any 3,778 (30.6% of the corpus). On the two label fields a dispute is *why* the consensus is empty; on subjectivité the median still resolved, so a disputed article still carries a value.
249+
236250
**Tip:** `get_sentiment_distribution(subject="Laïcité", country="Burkina Faso")` gives the polarity distribution for laïcité articles in BF specifically; compare against the unfiltered country baseline.
237251

238252
**Use `model="all"` before quoting any sentiment figure that carries an argument.** Corpus-wide the five models agree unanimously on polarity for only 3,929 of the 12,098 articles they all scored (**32%**). That number is the confidence floor: in a slice where they diverge further, a single model's polarity is a weak claim, and the disagreement is itself reportable. Four models reached 36% and the first three alone 43%, so a figure copied from an older draft will overstate the agreement.

CITATION.cff

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ identifiers:
2626
value: 10.5281/zenodo.21805837
2727
description: "Concept DOI for all versions of the software."
2828
license: MIT
29-
version: 3.4.0
29+
version: 3.5.0
3030
date-released: "2026-08-25"
3131
keywords:
3232
- Model Context Protocol

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,9 @@ IWAC is a digital archive focused on Islam and Muslims in West Africa:
180180
`qwen3-8-27b`. All five agree on polarity for only ~32% of articles, so
181181
`get_sentiment_distribution(model="all")` is the honest way to quote a figure.
182182
They do not all cover the same articles either — `qwen3-8-27b` scores 12,098
183-
where the rest score 12,298 — so each model reports its own `coverage`
183+
where the rest score 12,298 — so each model reports its own `coverage`.
184+
`model="consensus"` returns the panel's precomputed majority (not a sixth
185+
model), and `search_by_sentiment(disputed=…)` reads the articles it split on
184186
- **4,700+ authority records** (persons, organisations, places, events, subjects)
185187
- **1,500+ Islamic publications** (periodical issues, books) with full OCR
186188
- **860+ academic references**, half with abstracts
@@ -240,15 +242,15 @@ Machine-readable metadata lives in [CITATION.cff](CITATION.cff) — GitHub's
240242
**Cite this repository** button (sidebar) renders it as APA or BibTeX with the
241243
current version filled in. In text:
242244

243-
> Madore, F. (2026). *IWAC MCP Server* (Version 3.4.0) [Computer software].
245+
> Madore, F. (2026). *IWAC MCP Server* (Version 3.5.0) [Computer software].
244246
> Zenodo. https://doi.org/10.5281/zenodo.21805837
245247
246248
```bibtex
247249
@software{madore_iwac_mcp_server,
248250
author = {Madore, Frédérick},
249251
title = {{IWAC MCP Server}},
250252
year = {2026},
251-
version = {3.4.0},
253+
version = {3.5.0},
252254
publisher = {Zenodo},
253255
doi = {10.5281/zenodo.21805837},
254256
url = {https://github.com/fmadore/iwac-mcp-server},

mcpb/manifest.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"manifest_version": "0.3",
33
"name": "iwac-mcp-server",
44
"display_name": "Islam West Africa Collection (IWAC)",
5-
"version": "3.4.0",
5+
"version": "3.5.0",
66
"description": "Read-only access to the Islam West Africa Collection via Hugging Face datasets.",
77
"long_description": "Search and analyse the Islam West Africa Collection (IWAC): ~12,000 newspaper articles, 1,500 Islamic publications (searchable by keyword, subject, series, and table of contents), 4,700 index entries, plus audiovisual recordings, fieldwork photographs, archival documents, and academic references. Includes AI sentiment analysis (polarity/centrality/subjectivity, scored independently by gpt-5-6-luna, mistral-small-2603, deepseek-v4-flash-0731, gemma-4-31b-it and qwen3-8-27b), per-article AI abstracts, and per-year coverage timelines. All matching is accent- and case-insensitive.\n\n**No API key is required for the core tools** (cross-subset search/fetch, keyword search, filtering, statistics, timelines, item details). Three optional semantic-search tools use Gemini embeddings — over all articles, the publication series with tables of contents, and the photographs (cross-modal: describe what an image shows) — and need a free Google/Gemini API key; these are disabled by default.\n\nOn first use the server downloads ~250 MB of parquet data from Hugging Face into a local cache.",
88
"author": {
@@ -71,11 +71,11 @@
7171
},
7272
{
7373
"name": "search_by_sentiment",
74-
"description": "Filter articles by gpt-5-6-luna polarity, centrality and subjectivity labels."
74+
"description": "Filter articles by gpt-5-6-luna polarity, centrality and subjectivity labels, or by the fields the model panel split on."
7575
},
7676
{
7777
"name": "get_sentiment_distribution",
78-
"description": "Aggregate AI sentiment stats across a filter set, for one of the five models that scored the corpus or for all five plus their agreement. Renders as interactive donut charts in hosts that support MCP Apps."
78+
"description": "Aggregate AI sentiment stats across a filter set, for one of the five models that scored the corpus for all five plus their agreement, or for the panel's precomputed majority. Renders as interactive donut charts in hosts that support MCP Apps."
7979
},
8080
{
8181
"name": "search_index",

mcpb/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "iwac-mcp-server-bundle",
3-
"version": "3.4.0",
3+
"version": "3.5.0",
44
"description": "Node MCP server for the Islam West Africa Collection, packaged as a Claude Desktop extension (.mcpb).",
55
"private": true,
66
"type": "module",

0 commit comments

Comments
 (0)