|
| 1 | +# AMS contribution-signal inventory — what real repos actually expose as eligibility signals |
| 2 | + |
| 3 | +Research spike for **#6794**, grounding the `ContributionProfile` schema design (#6795) in an audit of real |
| 4 | +repos rather than an assumption about what repos "usually" do. Every number below was read from the live |
| 5 | +GitHub REST API (`GET /repos/{owner}/{repo}/labels`, `GET /repos/{owner}/{repo}/contents/{path}`) against the |
| 6 | +sample on 2026-07-17. **Research and writeup only — no code, no schema, no `discover` wiring here.** |
| 7 | + |
| 8 | +## Summary |
| 9 | + |
| 10 | +**Three findings should shape the schema, and all three argue for treating extraction as best-effort and |
| 11 | +per-signal rather than as a reliable parse:** |
| 12 | + |
| 13 | +1. **There is no universal eligibility label.** `good first issue` / `help wanted` exist in only **6 of 10** |
| 14 | + sampled repos. The three largest (rust, deno, kubernetes) use their own taxonomies entirely — rust's is |
| 15 | + `E-easy` / `E-medium` / `E-hard` / `E-help-wanted`. A profile that name-matches a fixed list silently |
| 16 | + returns "no eligible issues" on exactly the repos with the most contributor activity. |
| 17 | +2. **Label _descriptions_ are unreliable, and unreliable unevenly.** Coverage ranges from **3/17** (tailwind) |
| 18 | + and **8/76** (react) to **99/100** (rust). So a schema cannot depend on the description carrying the |
| 19 | + semantics — but it also must not ignore it, because on some repos the description is the _only_ place the |
| 20 | + semantics live (rust's `E-easy` description literally reads "…Good first issue.", which name-matching alone |
| 21 | + would miss). |
| 22 | +3. **`CONTRIBUTING.md` is frequently a signpost, not the rules.** react's is **208 bytes** and kubernetes' is |
| 23 | + **525 bytes** — both mostly a link to an external contributor guide. "Has CONTRIBUTING.md" is therefore a |
| 24 | + near-worthless presence check; size and content matter, and the real rules often live off-repo where AMS |
| 25 | + cannot read them. |
| 26 | + |
| 27 | +**A fourth finding is worth stating because it cuts against our own intuition:** the _linked-issue requirement_ |
| 28 | +— the rule loopover's own gate enforces hardest — is **not a general convention**. Grepping the real |
| 29 | +`CONTRIBUTING.md` of each repo for linked-issue language: loopover **8** mentions, react **0**, rust **0**, |
| 30 | +kubernetes **0**. Building the profile around "does a PR need a closing-keyword issue reference" would encode |
| 31 | +loopover's local norm as if it were an ecosystem norm. |
| 32 | + |
| 33 | +## The sample |
| 34 | + |
| 35 | +Ten repos: two of JSONbored's own gate-enabled repos, six well-known OSS projects with deliberately different |
| 36 | +contribution norms, one repo with AI-agent docs but no human contribution docs, and one with no contribution |
| 37 | +docs at all (the negative case #6794 asks for). |
| 38 | + |
| 39 | +| Repo | Labels | Described | `CONTRIBUTING` | PR template | AI-agent docs | |
| 40 | +| -------------------------- | -----: | --------: | ------------------ | ----------- | ------------------------ | |
| 41 | +| `JSONbored/loopover` | 27 | 25 | ✅ 19,926 B | — | `AGENTS.md`, `CLAUDE.md` | |
| 42 | +| `JSONbored/metagraphed` | 27 | 21 | ✅ | — | `AGENTS.md`, `CLAUDE.md` | |
| 43 | +| `facebook/react` | 76 | 8 | ⚠️ 208 B (pointer) | ✅ | `CLAUDE.md` | |
| 44 | +| `rust-lang/rust` | 100+ | 99 | ✅ 2,712 B | — | — | |
| 45 | +| `sveltejs/svelte` | 36 | 12 | ✅ 10,278 B | ✅ | `AGENTS.md` | |
| 46 | +| `denoland/deno` | 100+ | 67 | ✅ `.github/` | ✅ | `CLAUDE.md` | |
| 47 | +| `tailwindlabs/tailwindcss` | 17 | 3 | ✅ `.github/` | ✅ | — | |
| 48 | +| `kubernetes/kubernetes` | 100+ | 36 | ⚠️ 525 B (pointer) | ✅ | `AGENTS.md` | |
| 49 | +| `JSONbored/sure-aio` | 16 | 13 | — | — | `AGENTS.md` | |
| 50 | +| `sindresorhus/slugify` | 8 | 8 | — | — | — | |
| 51 | + |
| 52 | +`CONTRIBUTING` lives at the repo root in 6/10 and under `.github/` in 2/10, so extraction must probe both. |
| 53 | + |
| 54 | +## Signal type 1 — eligibility labels |
| 55 | + |
| 56 | +| Repo | Conventional eligibility label present? | |
| 57 | +| -------------------------- | --------------------------------------- | |
| 58 | +| loopover, metagraphed | `help wanted` only | |
| 59 | +| react, tailwindcss | `good first issue` only | |
| 60 | +| svelte, slugify, sure-aio | both | |
| 61 | +| **rust, deno, kubernetes** | **neither** | |
| 62 | + |
| 63 | +rust's actual taxonomy, with the semantics in the description rather than the name: |
| 64 | + |
| 65 | +``` |
| 66 | +E-easy :: Call for participation: Easy difficulty. Experience needed to fix: Not much. Good first issue. |
| 67 | +E-help-wanted :: Call for participation: Help is requested to fix this issue. |
| 68 | +E-medium :: Call for participation: Medium difficulty. Experience needed to fix: Intermediate. |
| 69 | +E-hard :: Call for participation: Hard difficulty. Experience needed to fix: A lot. |
| 70 | +``` |
| 71 | + |
| 72 | +**Implication for the schema:** eligibility-label rules need to be a _list of matchers over name **and** |
| 73 | +description_, not a fixed name list — and the profile must be able to say "this repo exposes no eligibility |
| 74 | +label at all", which is the honest answer for 3/10 of the sample and is different from "we failed to look". |
| 75 | + |
| 76 | +loopover's own `gittensor:*` labels are the far end of the spectrum: every one carries an explicit, |
| 77 | +machine-readable eligibility statement in its description. That is a **local convention**, not a shape to |
| 78 | +generalize from — it exists because loopover authored its own labels for its own gate. |
| 79 | + |
| 80 | +## Signal type 2 — exclusion / maintainer-only labels |
| 81 | + |
| 82 | +Nothing in the sample marks issues maintainer-only via a label whose _name_ says so. The closest generic |
| 83 | +signals are status labels (`blocked`, `needs-triage`, `on-hold`) whose exclusion meaning is conventional, not |
| 84 | +stated. react's `good first issue (taken)` is the one explicit "no longer available" marker found — and it |
| 85 | +encodes availability in the **name suffix**, which no other repo in the sample does. |
| 86 | + |
| 87 | +**Implication:** exclusion rules will be weaker and more inferential than eligibility rules; the confidence |
| 88 | +indicator #6795 calls for matters most here. |
| 89 | + |
| 90 | +## Signal type 3 — assignee rules |
| 91 | + |
| 92 | +Only loopover's `CONTRIBUTING.md` mentions assignment at all (1 mention); rust and kubernetes: 0. kubernetes |
| 93 | +does use `/assign` heavily, but that lives in its **external** community guide, not in-repo. |
| 94 | + |
| 95 | +**Implication:** "not assigned to the repo owner" cannot be sourced from docs for most repos. It is derivable |
| 96 | +from the issue's own `assignees` field at query time, which is a _runtime_ check, not a profile rule — worth |
| 97 | +separating in the schema. |
| 98 | + |
| 99 | +## Signal type 4 — PR templates and linked-issue requirements |
| 100 | + |
| 101 | +PR templates exist in 6/10, but presence says little: they are largely checklists of prose ("I have read the |
| 102 | +contributing guide"), not machine-checkable eligibility gates. As noted in the Summary, the closing-keyword |
| 103 | +linked-issue requirement is loopover-specific (8 vs 0 vs 0 vs 0). |
| 104 | + |
| 105 | +## Signal type 5 — AI-agent-facing docs (the surprise) |
| 106 | + |
| 107 | +**6 of 10 carry `AGENTS.md` and/or `CLAUDE.md`** — including react, deno, kubernetes and svelte. This is the |
| 108 | +most _consistently present_ signal in the sample after labels themselves, and it did not exist as a convention |
| 109 | +two years ago. |
| 110 | + |
| 111 | +`JSONbored/sure-aio` is the sharpest data point: **`AGENTS.md` and no `CONTRIBUTING.md`** — a repo that |
| 112 | +documents its rules for an AI contributor and not for a human one. Any extraction that treats `CONTRIBUTING.md` |
| 113 | +as the primary source and agent docs as a fallback has the priority backwards for that repo. |
| 114 | + |
| 115 | +## The negative case |
| 116 | + |
| 117 | +`sindresorhus/slugify`: no `CONTRIBUTING`, no PR template, no agent docs, 8 labels — all described, but only |
| 118 | +`good first issue` / `help wanted` carry eligibility meaning. `sindresorhus/p-map` (7 labels) and `chalk/chalk` |
| 119 | +(10 labels) match. This is a real and common shape: **labels are the only signal**, and the profile must |
| 120 | +degrade to "eligibility from labels alone, low completeness" rather than fail. |
| 121 | + |
| 122 | +## What this means for #6795 |
| 123 | + |
| 124 | +- **Per-signal provenance and confidence are load-bearing, not nice-to-have.** The sample's signal quality |
| 125 | + varies so widely between repos that a single repo-level confidence score would be useless — `rust` has |
| 126 | + excellent label descriptions and no PR template; `react` has a PR template and almost no label descriptions. |
| 127 | +- **Every rule must be independently absent.** 3/10 have no eligibility label; 2/10 have no docs at all; 4/10 |
| 128 | + have no agent docs. "Absent" needs to be a first-class value, distinct from "not yet extracted". |
| 129 | +- **Do not model the linked-issue rule as a core field.** It is loopover's norm, absent from the rest of the |
| 130 | + sample. |
| 131 | +- **Probe both `CONTRIBUTING.md` and `.github/CONTRIBUTING.md`,** and treat a very small file as a pointer |
| 132 | + rather than as rules. |
| 133 | +- **Rank agent docs at least as highly as `CONTRIBUTING.md`** as a rule source. |
| 134 | + |
| 135 | +## Method / reproducibility |
| 136 | + |
| 137 | +Every figure came from the live API; nothing was inferred from memory. Label counts are `length` over |
| 138 | +`GET /repos/{owner}/{repo}/labels?per_page=100` (paginated for the 100+ repos); "Described" counts labels whose |
| 139 | +`description` is non-null and non-empty; doc presence is a `GET .../contents/{path}` probe (404 ⇒ absent); file |
| 140 | +sizes are the contents API's own `size`; the linked-issue and assignment figures are case-insensitive greps of |
| 141 | +each decoded `CONTRIBUTING.md`. Counts are a point-in-time snapshot and will drift as these repos evolve — the |
| 142 | +_shape_ of the finding (wide, uneven variance) is the durable part, not the exact integers. |
0 commit comments