From 024bb7d09f68f01e4f13fd281015aa97618f6d2a Mon Sep 17 00:00:00 2001 From: Fuwad Busari <114140933+cythecode@users.noreply.github.com> Date: Mon, 28 Sep 2026 18:43:07 +0100 Subject: [PATCH] docs(governance): add emeritus transition process Adds Governance/processes/EMERITUS_TRANSITION.md defining when a member moves to emeritus, the privileges retained and withdrawn, and the path back to active status, with Governance/processes/EMERITUS_TRANSITION.test.ts pinning the guarantees to the document. --- Governance/processes/EMERITUS_TRANSITION.md | 154 +++++++++++ .../processes/EMERITUS_TRANSITION.test.ts | 248 ++++++++++++++++++ 2 files changed, 402 insertions(+) create mode 100644 Governance/processes/EMERITUS_TRANSITION.md create mode 100644 Governance/processes/EMERITUS_TRANSITION.test.ts diff --git a/Governance/processes/EMERITUS_TRANSITION.md b/Governance/processes/EMERITUS_TRANSITION.md new file mode 100644 index 00000000..de4af227 --- /dev/null +++ b/Governance/processes/EMERITUS_TRANSITION.md @@ -0,0 +1,154 @@ +# Emeritus Transition Process + +## Purpose + +This process defines how a member of TeachLink Web moves from an active +governance role to emeritus status, which privileges survive the transition, +and how a member returns to active status. It is the operational companion to +`Governance/roles/EMERITUS.md`: that document defines what emeritus status is, +and this one defines the transition itself, with an owner, a deadline, and a +public record for every step. + +## Scope + +This process applies to every role defined under `Governance/roles/`, +including the contributor, reviewer, moderator, maintainer, and treasurer +roles, and to any working-group lead or domain steward recognised in a +governance document. + +It covers the voluntary, good-standing transition into emeritus that +`Governance/roles/EMERITUS.md` grants. It does not cover a role removed for +inactivity, which follows the notification and consequence steps in +`Governance/policies/INACTIVITY.md`, nor a role removed for misconduct, which +follows `Governance/CODE_OF_CONDUCT.md` and its enforcement documents. A +member who was removed through either path is not placed in emeritus status +by this process. + +Emeritus is opt-in. No member is moved to emeritus status without their +agreement, and this process never removes a role as a disciplinary measure. + +## When a Member Moves to Emeritus + +The transition starts from one of three triggers, and each one runs the same +steps below. + +- **Voluntary step-down.** A member decides to step back from an active role + and asks the maintainers for emeritus status. +- **End of a fixed term.** A role held for a fixed term, such as treasurer, + ends and the outgoing member does not stand for another term but wishes to + stay associated with the project. +- **Role consolidation.** A governance change retires or merges the member's + former role, and the member consents to emeritus status instead of a + different active role. + +Every trigger produces the same recorded sequence: + +1. **Request or proposal.** The member requests the transition, or the + maintainers propose it and record that the member consents. A proposal + without consent does not proceed. +2. **Acknowledgement within 3 business days.** A maintainer opens a public + transition issue titled with the member's handle and the word `Emeritus`, + acknowledges the request, and confirms the retained and withdrawn + privileges with the member. +3. **Recorded decision within 10 business days.** The maintainers record the + decision and its reasoning on the transition issue, in line with the + decision cadence in `Governance/processes/NOMINATION.md`. +4. **Applied within 5 business days of the decision.** The maintainers update + the relevant `Governance/roles/` entry, the CODEOWNERS file, and the + repository access list, and confirm the change back to the member. + +If a deadline above cannot be met, the member is told on the transition issue +before it passes, with the reason and a new date. The transition issue is the +single audit record: it names who decided, when, and why. + +## Privileges Retained + +Emeritus status keeps the recognition earned by the former role without +recreating its authority. + +- Being listed in project recognition materials, following the tiers in + `Governance/RECOGNITION.md`. +- Attribution for past work, including continued credit on merged changes. +- Repository read access, so the member can follow the project. +- Participation in issues, pull requests, and discussions as a community + member, exactly as `Governance/roles/CONTRIBUTOR.md` describes. +- An invitation to open governance meetings, with a voice to advise but no + vote and no effect on quorum. +- Informal consultation when the member consents, including review comments + offered as advice rather than as a required approval. +- The emeritus label or recognition marker where the project maintains one. + +## Privileges Withdrawn + +Emeritus status removes the duties, access, and decision authority of the +former role unless the member is reappointed through the process below. + +- Voting rights and quorum on decisions reserved to active role-holders. +- Decision authority attached to the role, including triage, review + approval, moderation, treasury signing, and security response duties. +- Membership of the maintainer team and any elevated repository permissions + (write and admin), which are revoked or reduced to community-level read + access. +- Entries in the CODEOWNERS file and any review-assignment rotation. +- Access to private channels, including security and conduct matters that are + not public. +- Authority to represent the project externally or to speak for it. + +Withdrawn privileges are listed explicitly on the transition issue so the +member knows exactly what changes, and any access change is applied only +after the recorded decision. + +## Returning to Active Status + +Emeritus status is reversible and is never a bar to returning. A member may +ask to return at any time by opening an issue, and the path depends on the +role sought. + +- **Contributor.** A merged pull request reactivates the member; no issue or + approval is required, as `Governance/policies/INACTIVITY.md` provides. +- **Reviewer or moderator.** The member demonstrates re-engagement through at + least two completed reviews or moderation actions, and one active + maintainer records approval on the return issue. +- **Maintainer.** The member demonstrates re-engagement through at least four + completed reviews or merged pull requests within 60 days, and two active + maintainers who are not the requestor record approval, followed by the + onboarding steps in `Governance/roles/MAINTAINER.md`. +- **Appointment instead of reinstatement.** A return may also be made by + nomination under `Governance/processes/NOMINATION.md`; prior emeritus + service is evidence of the role's expectations and does not shorten the + seconding requirement. + +A return request is acknowledged within 3 business days and decided within 10 +business days, or the member is told before the deadline passes with a new +date. A returning member has no waiting period and no penalty: the transition +back restores the full privileges of the role from the recorded decision. + +## Ownership + +- Maintainers own this process, decide each transition, and keep the + transition issue and the role records in step. +- Changes to this process are proposed in a pull request that touches only + the `Governance/` folder. +- This process is reviewed alongside `Governance/roles/EMERITUS.md` and + `Governance/policies/INACTIVITY.md` whenever either is revised. + +## Success + +This process succeeds when every emeritus transition has a public record and +a named owner, when the member knows before the change exactly which +privileges are retained and which are withdrawn, when recognition is kept +without recreating authority, and when a member who wishes to return finds a +clear, welcoming path back. + +## Regression Tests + +Coverage is provided by `Governance/processes/EMERITUS_TRANSITION.test.ts`, +which pins the triggers, the retained and withdrawn privileges, and the +return-to-active path in this document to the guarantees made by +`Governance/roles/EMERITUS.md` and `Governance/policies/INACTIVITY.md`. + +## Revision History + +| Version | Date | Change | Author | +| ------- | ---------- | ---------------- | --------------------- | +| 1.0 | 2026-09-28 | Initial version. | TeachLink maintainers | diff --git a/Governance/processes/EMERITUS_TRANSITION.test.ts b/Governance/processes/EMERITUS_TRANSITION.test.ts new file mode 100644 index 00000000..6b688619 --- /dev/null +++ b/Governance/processes/EMERITUS_TRANSITION.test.ts @@ -0,0 +1,248 @@ +/** + * Regression tests for the Emeritus Transition process + * (Governance/processes/EMERITUS_TRANSITION.md). + * + * The process is documentation, but it makes concrete guarantees: three + * triggers that all run one recorded sequence, an explicit list of retained + * and withdrawn privileges, and a return path whose thresholds match + * `Governance/policies/INACTIVITY.md`. These tests pin those guarantees to + * the checked-in document so an edit cannot silently drop a trigger, keep a + * withdrawn privilege, or put a barrier in front of a returning member, and + * they keep the document consistent with the rest of `Governance/`. + */ +import { describe, expect, it } from 'vitest'; +import { readFileSync } from 'node:fs'; +import path from 'node:path'; + +const PROCESS_PATH = path.resolve(__dirname, 'EMERITUS_TRANSITION.md'); +const processDoc = readFileSync(PROCESS_PATH, 'utf8'); + +/** Strip Markdown syntax so keyword assertions match prose, not formatting. */ +function plainProse(markdown: string): string { + return markdown + .replace(/`([^`]*)`/g, '$1') // inline code keeps its text + .replace(/\*\*([^*]*)\*\*/g, '$1') // bold keeps its text + .replace(/\[([^\]]*)\]\(([^)]*)\)/g, '$1 $2') // links keep text and target + .replace(/\s+/g, ' ') // line wrapping must not affect prose matching + .toLowerCase(); +} + +/** Every "## Heading" in the document, in order. */ +const sections = [...processDoc.matchAll(/^## (.+)$/gm)].map((match) => match[1]); + +/** Extract the body of a single "## Section" (text up to the next heading). */ +function sectionBody(title: string): string { + const start = processDoc.indexOf(`## ${title}\n`); + expect(start, `section "${title}" is missing`).toBeGreaterThanOrEqual(0); + const next = processDoc.indexOf('\n## ', start + 1); + const body = next === -1 ? processDoc.slice(start) : processDoc.slice(start, next); + return plainProse(body); +} + +describe('EMERITUS_TRANSITION process document structure', () => { + it('is titled "Emeritus Transition Process"', () => { + expect(processDoc.startsWith('# Emeritus Transition Process\n')).toBe(true); + }); + + it('keeps the canonical governance document sections', () => { + expect(sections).toEqual([ + 'Purpose', + 'Scope', + 'When a Member Moves to Emeritus', + 'Privileges Retained', + 'Privileges Withdrawn', + 'Returning to Active Status', + 'Ownership', + 'Success', + 'Regression Tests', + 'Revision History', + ]); + }); + + it('covers the three areas the issue requires', () => { + expect(sections).toContain('When a Member Moves to Emeritus'); + expect(sections).toContain('Privileges Retained'); + expect(sections).toContain('Returning to Active Status'); + }); + + it('has no unresolved template placeholders', () => { + expect(processDoc).not.toMatch(/TBD|TODO|FIXME|<[a-z-]+>|XXX/); + }); + + it('stays within the house documentation line width (max 82 columns)', () => { + const longest = Math.max(...processDoc.split('\n').map((line) => line.length)); + expect(longest).toBeLessThanOrEqual(82); + }); +}); + +describe('transition trigger guarantees', () => { + const body = sectionBody('When a Member Moves to Emeritus'); + + it('names the three triggers that start the same transition', () => { + const required = ['voluntary step-down', 'end of a fixed term', 'role consolidation']; + for (const trigger of required) { + expect(body).toContain(trigger); + } + }); + + it('starts the transition from a request or a consented proposal only', () => { + expect(body).toContain('the member requests the transition'); + expect(body).toContain('a proposal without consent does not proceed'); + }); + + it('records the sequence with an owner and a deadline at each step', () => { + expect(body).toContain('acknowledgement within 3 business days'); + expect(body).toContain('recorded decision within 10 business days'); + expect(body).toContain('applied within 5 business days of the decision'); + expect(body).toContain('public transition issue'); + }); + + it('requires notice before a deadline is missed', () => { + expect(body).toContain('told on the transition issue before it passes'); + expect(body).toContain('the reason and a new date'); + }); +}); + +describe('scope guarantees', () => { + const body = sectionBody('Scope'); + + it('makes emeritus opt-in and never a disciplinary outcome', () => { + expect(body).toContain('emeritus is opt-in'); + expect(body).toContain('without their agreement'); + expect(body).toContain('never removes a role as a disciplinary measure'); + }); + + it('keeps the transition out of the inactivity and misconduct paths', () => { + expect(body).toContain('governance/policies/inactivity.md'); + expect(body).toContain('governance/code_of_conduct.md'); + expect(body).toContain('not placed in emeritus status by this process'); + }); +}); + +describe('retained privilege guarantees', () => { + const body = sectionBody('Privileges Retained'); + + it('keeps recognition, attribution, and read access', () => { + expect(body).toContain('governance/recognition.md'); + expect(body).toContain('attribution for past work'); + expect(body).toContain('repository read access'); + }); + + it('keeps community participation without recreating authority', () => { + expect(body).toContain('governance/roles/contributor.md'); + expect(body).toContain('a voice to advise'); + expect(body).toContain('no vote and no effect on quorum'); + }); + + it('keeps informal consultation only with consent', () => { + expect(body).toContain('when the member consents'); + expect(body).toContain('advice rather than as a required approval'); + }); +}); + +describe('withdrawn privilege guarantees', () => { + const body = sectionBody('Privileges Withdrawn'); + + it('withdraws decision authority and quorum', () => { + expect(body).toContain('voting rights and quorum'); + expect(body).toContain('decision authority attached to the role'); + }); + + it('withdraws elevated access and CODEOWNERS placement', () => { + expect(body).toContain('write and admin'); + expect(body).toContain('reduced to community-level read access'); + expect(body).toContain('codeowners'); + expect(body).toContain('review-assignment rotation'); + }); + + it('withdraws private and representative privileges', () => { + expect(body).toContain('private channels'); + expect(body).toContain('represent the project externally'); + }); + + it('lists the withdrawn privileges and applies them after the decision', () => { + expect(body).toContain('listed explicitly on the transition issue'); + expect(body).toContain('only after the recorded decision'); + }); +}); + +describe('return-to-active guarantees', () => { + const body = sectionBody('Returning to Active Status'); + + it('makes emeritus reversible and never a bar to returning', () => { + expect(body).toContain('reversible and is never a bar to returning'); + expect(body).toContain('ask to return at any time'); + }); + + it('matches the reinstatement path in the inactivity policy', () => { + const inactivity = plainProse( + readFileSync(path.resolve(__dirname, '../policies/INACTIVITY.md'), 'utf8'), + ); + // Contributor returns with a merged pull request, no approval required. + expect(body).toContain('merged pull request reactivates the member'); + expect(inactivity).toContain('reinstatement is automatic on the next merged pull request'); + // Reviewer returns after two completed reviews with one maintainer. + expect(body).toContain('at least two completed reviews'); + expect(inactivity).toContain('two completed reviews'); + // Maintainer returns after four reviews or pulls in 60 days, two approvals. + expect(body).toContain('at least four completed reviews'); + expect(body).toContain('within 60 days'); + expect(inactivity).toContain('four completed reviews or'); + expect(inactivity).toContain('60-day window'); + }); + + it('offers appointment by nomination as an alternative to reinstatement', () => { + expect(body).toContain('governance/processes/nomination.md'); + expect(body).toContain('prior emeritus service is evidence'); + expect(body).toContain('does not shorten the seconding requirement'); + }); + + it('puts no waiting period or penalty on a returning member', () => { + expect(body).toContain('no waiting period and no penalty'); + expect(body).toContain('restores the full privileges of the role'); + }); + + it('gives the return request a deadline', () => { + expect(body).toContain('acknowledged within 3 business days'); + expect(body).toContain('decided within 10 business days'); + }); +}); + +describe('process consistency with the governance folder', () => { + it('defers to companion documents that must exist', () => { + const referenced = [ + '../roles/EMERITUS.md', + '../roles/CONTRIBUTOR.md', + '../roles/MAINTAINER.md', + '../policies/INACTIVITY.md', + '../RECOGNITION.md', + '../CODE_OF_CONDUCT.md', + 'NOMINATION.md', + ]; + for (const file of referenced) { + const name = path.basename(file).toLowerCase(); + expect(plainProse(processDoc), `must reference ${name}`).toContain(name); + expect(() => readFileSync(path.resolve(__dirname, file), 'utf8')).not.toThrow(); + } + }); + + it('matches the retaining and removing rule stated by the emeritus role', () => { + const role = plainProse(readFileSync(path.resolve(__dirname, '../roles/EMERITUS.md'), 'utf8')); + // Both documents agree that authority is not retained without reappointment. + expect(role).toContain('unless explicitly reappointed'); + expect(plainProse(processDoc)).toContain('unless the member is reappointed'); + // Both documents agree emeritus is opt-in. + expect(role).toContain('honorary and opt-in'); + expect(plainProse(processDoc)).toContain('emeritus is opt-in'); + }); + + it('mentions the governance-folder-only rule for changes to this process', () => { + const body = sectionBody('Ownership'); + expect(body).toContain('only the governance/ folder'); + }); + + it('pins regression coverage to the companion test file', () => { + const body = sectionBody('Regression Tests'); + expect(body).toContain('governance/processes/emeritus_transition.test.ts'); + }); +});