From 7d5620f8136f0310ae06f5bfa863a2e95c3e3f15 Mon Sep 17 00:00:00 2001 From: Michael O'Boyle Date: Mon, 13 Jul 2026 09:25:01 -0400 Subject: [PATCH] feat(committee): add Craftsman (Rishub C R) as non-voting contributor Adds a 'contributors' array to committee-config.json, separate from the voting 'members' roster. Craftsman (CraftsMan-Labs) is recorded as a committee contributor without vote/quorum weight. Non-voting is enforced structurally, not by label: - Quorum is Math.floor(members.length / 2) + 1 in the vote workflows, which read members.length and ignore role. Keeping contributors OUT of members[] leaves the quorum denominator (6 -> majority 4) unchanged. - Vote casting is gated by GitHub org/collaborator permission; CraftsMan-Labs is not an agenticsorg member/collaborator, so no counted vote is possible. - New test/committee-config.test.js asserts the invariant (no login in both arrays; members.length==6; quorum==4). Mutation-checked: moving Craftsman into members[] fails the test. Dashboard surfaces contributors (build-dashboard.mjs); committee_size stays members-only. dashboard.json is CI-regenerated by pages.yml, not hand-edited. Co-Authored-By: Claude Opus 4.8 --- data/committee-config.json | 3 +++ scripts/build-dashboard.mjs | 1 + test/committee-config.test.js | 26 ++++++++++++++++++++++++++ 3 files changed, 30 insertions(+) create mode 100644 test/committee-config.test.js diff --git a/data/committee-config.json b/data/committee-config.json index d411822..717ecd2 100644 --- a/data/committee-config.json +++ b/data/committee-config.json @@ -8,5 +8,8 @@ {"login": "shaal", "name": "Ofer Shaal", "role": "member"}, {"login": "inde5media", "name": "Mat Mathews", "role": "member"}, {"login": "rcraw", "name": "Robert Ranson", "role": "member"} + ], + "contributors": [ + {"login": "CraftsMan-Labs", "name": "Rishub C R (Craftsman)", "role": "contributor"} ] } diff --git a/scripts/build-dashboard.mjs b/scripts/build-dashboard.mjs index 1fbbb96..a6b0afd 100644 --- a/scripts/build-dashboard.mjs +++ b/scripts/build-dashboard.mjs @@ -80,6 +80,7 @@ const dashboard = { name: config.committee_name, quorum_rule: config.quorum_rule, members: config.members, + contributors: config.contributors || [], }, submissions, approved, diff --git a/test/committee-config.test.js b/test/committee-config.test.js new file mode 100644 index 0000000..ce2f2b0 --- /dev/null +++ b/test/committee-config.test.js @@ -0,0 +1,26 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; + +const cfg = JSON.parse(readFileSync(new URL('../data/committee-config.json', import.meta.url))); + +test('committee-config: members drive quorum, contributors do not', () => { + // Quorum is Math.floor(members.length / 2) + 1 in the vote workflows. + // Contributors MUST NOT be in members[], or they would change the quorum denominator. + assert.equal(cfg.members.length, 6, 'voting roster size (quorum denominator) unchanged'); + const quorum = Math.floor(cfg.members.length / 2) + 1; + assert.equal(quorum, 4, 'simple-majority quorum is 4'); +}); + +test('committee-config: Craftsman is a non-voting contributor', () => { + const contribLogins = (cfg.contributors || []).map((c) => c.login); + assert.ok(contribLogins.includes('CraftsMan-Labs'), 'Craftsman listed as contributor'); + const memberLogins = cfg.members.map((m) => m.login); + // Non-voting invariant: no login appears in both arrays. + for (const l of contribLogins) { + assert.ok(!memberLogins.includes(l), `${l} must not also be a voting member`); + } + for (const c of cfg.contributors || []) { + assert.equal(c.role, 'contributor', `${c.login} role is contributor`); + } +});