diff --git a/Governance/processes/OFFBOARDING.md b/Governance/processes/OFFBOARDING.md new file mode 100644 index 00000000..a53ec3f3 --- /dev/null +++ b/Governance/processes/OFFBOARDING.md @@ -0,0 +1,157 @@ +# Contributor Offboarding Process + +## Purpose + +This process defines how TeachLink Web handles the transition when a +contributor, reviewer, or maintainer leaves the project — voluntarily, +through inactivity, or through removal. It specifies the access revocation +steps, the knowledge handover expectations, and the timeline within which +each step must be completed. Writing it down means departures are handled +consistently, the project's history is preserved, and former contributors +leave with a clear record of their work. + +## Scope + +This process applies to every named role defined under `Governance/roles/`: +Contributor, Reviewer, Maintainer, and any working-group lead or domain +steward recognised in a governance document. It covers three offboarding +triggers: + +- **Voluntary departure.** The role-holder informs the project that they + are stepping down or reducing participation below the threshold defined + in `Governance/policies/INACTIVITY.md`. +- **Inactivity-triggered.** The role-holder has been found inactive under + `Governance/policies/INACTIVITY.md` and the notification window has + closed without a qualifying response. +- **Role removal.** The role-holder has been removed through a maintainer + decision following `Governance/processes/CONFLICT_RESOLUTION.md` or a + privilege revocation process. + +It does not cover account deletion, repository archival, or the +dissolution of a working group, which are governed by separate documents. + +## Offboarding Triggers and Initiation + +Offboarding begins when one of the following events is recorded on a +public GitHub issue: + +- The role-holder opens or comments on an issue stating their intent to + depart (voluntary departure). +- A maintainer closes the inactivity tracking issue with a confirmed + status change (inactivity-triggered). +- A maintainer records a removal decision on the relevant issue + (role removal). + +A maintainer acknowledges the offboarding within **3 business days** of +the trigger event, opens a dedicated offboarding tracking issue titled +`Offboarding: [role] — @[handle]`, and links the trigger. All subsequent +steps are recorded on that issue. + +## Access Revocation + +Access revocation is applied in proportion to the role being offboarded. +All steps must be completed within **5 business days** of the offboarding +issue being opened. + +- **Contributor.** No repository access is changed. The contributor's + entry in any contributor list or attribution file is marked inactive, + and the contribution history is retained. +- **Reviewer.** The role-holder is removed from the CODEOWNERS file and + any review-assignment rotation. Repository read access is retained. +- **Maintainer.** The role-holder is removed from the CODEOWNERS file, + the maintainer team, and any elevated repository permissions (write, + admin). Repository read access is retained unless a removal decision + specifies otherwise. Individually held third-party service tokens and + secrets (CI, cloud credentials, package-registry tokens) are rotated + within **24 hours** of offboarding being confirmed. Shared credentials + are rotated within **5 business days**. +- **Working-group lead / domain steward.** The role-holder is removed + from the lead position. If no active co-lead exists, the working group + moves to a caretaker state under the maintainer team until a new lead + is appointed per the relevant working-group charter. + +Every revocation step is ticked off on the offboarding tracking issue by +the maintainer who completed it. The issue is not closed until all steps +are recorded. + +## Knowledge Handover + +Before or concurrently with access revocation, the departing role-holder +is asked to complete a knowledge handover. The handover is optional for +contributors (the merged history is self-documenting) but expected for +reviewers and required for maintainers and working-group leads. + +A maintainer contacts the departing role-holder on the offboarding issue +within **3 business days** of it being opened and asks for: + +- **Open work.** A comment or linked document listing any open pull + requests, in-progress issues, or unresolved action items the + role-holder owns, so they can be reassigned or closed. +- **Institutional knowledge.** Any context not captured in the + repository — recurring tasks, vendor contacts, tooling notes, or + decisions that live only in the role-holder's memory — documented in + a comment or a pull request to the relevant `Governance/` or `docs/` + file. +- **Keys and credentials.** Confirmation that any individually held + secrets have been rotated or transferred. + +The handover window is **10 business days** from the offboarding issue +opening. If the role-holder does not respond, the maintainer documents +what is known on the issue and proceeds. The project does not hold +offboarding open indefinitely waiting for a response. + +## Timeline Summary + +The table below lists every step, its owner, and the deadline from the +point each step's clock starts. + +- Acknowledge trigger; open tracking issue — Maintainer, + within 3 business days of trigger. +- Contact role-holder for knowledge handover — Maintainer, + within 3 business days of issue opening. +- Complete access revocation — Maintainer, + within 5 business days of issue opening. +- Rotate individually held secrets (maintainer role) — Maintainer, + within 24 hours of offboarding being confirmed. +- Rotate shared credentials (maintainer role) — Maintainer, + within 5 business days of offboarding being confirmed. +- Knowledge handover window closes — Role-holder, + 10 business days after issue opening. +- Close offboarding tracking issue — Maintainer, + after all steps are recorded on the issue. + +If any deadline cannot be met, the reason and a revised date are posted +on the offboarding tracking issue before the deadline passes. + +## Ownership + +- Maintainers own this process and are responsible for opening and + closing offboarding tracking issues. The responsibilities of the + Maintainer role are defined in `Governance/roles/MAINTAINER.md`. +- A change to this process is proposed in a pull request that touches + only the `Governance/` folder. +- This process is reviewed alongside `Governance/policies/INACTIVITY.md` + whenever inactivity thresholds or consequence steps are revised. + +## Success + +This process succeeds when every departure is recorded on a public +tracking issue, no role-holder loses access without a documented reason, +secrets are rotated on the committed timeline, and open work is handed +over so no in-flight contribution is lost when a role-holder leaves. + +## Regression Tests + +Regression coverage for this process lives in +`Governance/processes/OFFBOARDING.test.ts`. It pins the offboarding +triggers, access revocation steps, knowledge handover requirements, and +the timeline commitments to this document so they cannot silently +regress — for example, an edit that removes the secret-rotation deadline, +drops the tracking-issue requirement, or relaxes the handover window +fails CI. + +## Revision History + +| Version | Date | Change | Author | +| ------- | ---------- | ---------------- | --------------------- | +| 1.0 | 2026-09-28 | Initial version. | TeachLink maintainers | diff --git a/Governance/processes/OFFBOARDING.test.ts b/Governance/processes/OFFBOARDING.test.ts new file mode 100644 index 00000000..5237f79c --- /dev/null +++ b/Governance/processes/OFFBOARDING.test.ts @@ -0,0 +1,246 @@ +/** + * Regression tests for the Contributor Offboarding Process + * (Governance/processes/OFFBOARDING.md). + * + * The process is documentation, but it makes concrete, enforceable + * guarantees: three offboarding triggers each backed by a public issue, + * access revocation proportional to role, a knowledge handover window, + * and secret-rotation deadlines that protect the project after a + * maintainer departs. These tests pin those guarantees to the + * checked-in document so they cannot silently regress — for example, + * an edit that removes the secret-rotation deadline, drops the + * tracking-issue requirement, or relaxes the handover window fails CI. + */ +import { describe, expect, it } from 'vitest'; +import { readFileSync } from 'node:fs'; +import path from 'node:path'; + +const PROCESS_PATH = path.resolve(__dirname, 'OFFBOARDING.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(); +} + +const prose = plainProse(processDoc); + +/** Every "## Heading" in the document, in order. */ +const sections = [...processDoc.matchAll(/^## (.+)$/gm)].map((m) => m[1]); + +/** Extract the body of a single "## Section" (text up to 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); +} + +// --------------------------------------------------------------------------- +// Document structure +// --------------------------------------------------------------------------- + +describe('OFFBOARDING process document structure', () => { + it('is titled "Contributor Offboarding Process"', () => { + expect(processDoc.startsWith('# Contributor Offboarding Process\n')).toBe( + true, + ); + }); + + it('keeps the canonical governance document sections in order', () => { + expect(sections).toEqual([ + 'Purpose', + 'Scope', + 'Offboarding Triggers and Initiation', + 'Access Revocation', + 'Knowledge Handover', + 'Timeline Summary', + 'Ownership', + 'Success', + 'Regression Tests', + 'Revision History', + ]); + }); + + it('covers the three areas the issue requires', () => { + expect(sections).toContain('Offboarding Triggers and Initiation'); + expect(sections).toContain('Access Revocation'); + expect(sections).toContain('Knowledge Handover'); + }); + + 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); + }); +}); + +// --------------------------------------------------------------------------- +// Trigger and initiation guarantees +// --------------------------------------------------------------------------- + +describe('offboarding trigger guarantees', () => { + const body = sectionBody('Offboarding Triggers and Initiation'); + + it('requires a public tracking issue for every departure', () => { + expect(body).toContain('offboarding: [role] — @[handle]'); + expect(body).toContain('public github issue'); + }); + + it('names all three offboarding triggers', () => { + expect(body).toContain('voluntary departure'); + expect(body).toContain('inactivity-triggered'); + expect(body).toContain('role removal'); + }); + + it('requires a maintainer to acknowledge within 3 business days', () => { + expect(body).toContain('3 business days'); + expect(body).toContain('acknowledges the offboarding'); + }); + + it('links the trigger event on the tracking issue', () => { + expect(body).toContain('links the trigger'); + }); +}); + +// --------------------------------------------------------------------------- +// Access revocation guarantees +// --------------------------------------------------------------------------- + +describe('access revocation guarantees', () => { + const body = sectionBody('Access Revocation'); + + it('completes revocation within 5 business days of the issue opening', () => { + expect(body).toContain('5 business days'); + expect(body).toContain('offboarding issue being opened'); + }); + + it('applies revocation proportional to the role', () => { + expect(body).toContain('contributor'); + expect(body).toContain('reviewer'); + expect(body).toContain('maintainer'); + expect(body).toContain('working-group lead'); + }); + + it('retains repository read access for contributors and reviewers', () => { + expect(body).toContain('repository read access is retained'); + }); + + it('rotates individually held maintainer secrets within 24 hours', () => { + expect(body).toContain('24 hours'); + expect(body).toContain('individually held'); + expect(body).toContain('rotated'); + }); + + it('rotates shared credentials within 5 business days', () => { + expect(body).toContain('shared credentials'); + expect(body).toContain('5 business days'); + }); + + it('ticks off every revocation step on the tracking issue', () => { + expect(body).toContain('ticked off on the offboarding tracking issue'); + expect(body).toContain('not closed until all steps are recorded'); + }); + + it('places a working group in caretaker state if no co-lead exists', () => { + expect(body).toContain('caretaker state'); + expect(body).toContain('maintainer team'); + }); +}); + +// --------------------------------------------------------------------------- +// Knowledge handover guarantees +// --------------------------------------------------------------------------- + +describe('knowledge handover guarantees', () => { + const body = sectionBody('Knowledge Handover'); + + it('requires handover for maintainers and working-group leads', () => { + expect(body).toContain('required for maintainers'); + expect(body).toContain('working-group leads'); + }); + + it('asks for open work, institutional knowledge, and credential status', () => { + expect(body).toContain('open work'); + expect(body).toContain('institutional knowledge'); + expect(body).toContain('keys and credentials'); + }); + + it('closes the handover window after 10 business days', () => { + expect(body).toContain('10 business days'); + expect(body).toContain('handover window'); + }); + + it('does not hold offboarding open indefinitely for a non-response', () => { + expect(body).toContain('does not hold offboarding open indefinitely'); + expect(body).toContain('proceeds'); + }); +}); + +// --------------------------------------------------------------------------- +// Timeline summary guarantees +// --------------------------------------------------------------------------- + +describe('timeline summary guarantees', () => { + const body = sectionBody('Timeline Summary'); + + it('commits deadlines in the timeline summary', () => { + const required = [ + '3 business days', + '5 business days', + '24 hours', + '10 business days', + ]; + for (const deadline of required) { + expect(body, `must list ${deadline}`).toContain(deadline); + } + }); + + it('requires a revised date on the issue if a deadline is missed', () => { + expect(body).toContain('revised date'); + expect(body).toContain('before the deadline passes'); + }); +}); + +// --------------------------------------------------------------------------- +// Consistency with the broader Governance folder +// --------------------------------------------------------------------------- + +describe('process consistency with the governance folder', () => { + it('defers to the governance documents it builds on, which must exist', () => { + const referenced = [ + 'CONFLICT_RESOLUTION.md', // same folder + '../policies/INACTIVITY.md', + '../roles/MAINTAINER.md', + ]; + for (const file of referenced) { + const name = path.basename(file).toLowerCase(); + expect(prose, `must reference ${name}`).toContain(name); + expect( + () => readFileSync(path.resolve(__dirname, file), 'utf8'), + ).not.toThrow(); + } + }); + + 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/offboarding.test.ts'); + }); +}); diff --git a/Governance/processes/PROMOTION_CRITERIA.md b/Governance/processes/PROMOTION_CRITERIA.md new file mode 100644 index 00000000..68b85239 --- /dev/null +++ b/Governance/processes/PROMOTION_CRITERIA.md @@ -0,0 +1,162 @@ +# Role Promotion Criteria + +## Purpose + +This document defines the objective criteria a contributor must meet +before being eligible for promotion to a higher project role in TeachLink +Web, who may nominate and approve a promotion, and the evidence that must +be supplied to support the nomination. It supplements the nomination and +decision process in `Governance/processes/NOMINATION.md` by specifying +what achievement looks like at each step of the contributor ladder. Without +published criteria, role boundaries are invisible and promotions appear +arbitrary. + +## Scope + +This document covers promotions within the contributor ladder for TeachLink +Web: from community participant to Contributor, from Contributor to +Reviewer, and from Reviewer to Maintainer. It does not cover the Treasurer +role (see `Governance/roles/TREASURER.md`) or working-group leads, whose +criteria are defined in the relevant working-group charter. + +Promotion is separate from recognition, which is handled quarterly under +`Governance/RECOGNITION.md`. Meeting the criteria makes a person eligible +for promotion; it does not automatically grant the role. + +## Criteria by Role + +### Contributor + +A community participant is eligible to become a Contributor when they have: + +- At least **one merged pull request** in this repository that closes an + assigned issue. A pull request with no assigned issue, or one that + modifies only documentation with no associated change to functionality, + does not satisfy this criterion on its own; at least one merged pull + request must address a labelled, triaged issue. +- Demonstrated adherence to the project's contribution standards: changes + are small and focused, pass the required quality gates (`type-check`, + `lint`, `build`, `test`), use `lucide-react` icons, and produce no + console errors. +- No unresolved conduct matter under `Governance/CODE_OF_CONDUCT.md`. + +The Contributor role is granted automatically: the first pull request +that meets the above conditions establishes the role with no nomination +required. It is documented here for completeness and so the expectation +is explicit. + +### Reviewer + +A Contributor is eligible for the Reviewer role when they have: + +- At least **five merged pull requests** in this repository spanning at + least **two different areas** of the codebase (features, components, + services, infrastructure, documentation, governance). +- At least **five substantive review comments** left on other + contributors' pull requests — comments that identify a correctness + issue, a performance concern, an accessibility gap, or a clear code + quality improvement, not purely approvals or nits. +- Demonstrated familiarity with the project's architecture, coding + conventions, and accessibility requirements as evidenced by review + comments or pull-request descriptions. +- At least **three months of active participation** in the repository + (issues, pull requests, or reviews) with no gap longer than 30 + consecutive days. +- No unresolved conduct matter. + +### Maintainer + +A Reviewer is eligible for the Maintainer role when they have: + +- At least **fifteen merged pull requests** in this repository, of which + at least **five** addressed non-trivial scope: new features, refactors + covering more than two files, or governance documents with a companion + test suite. +- At least **twenty substantive review comments** on other contributors' + pull requests, at least **five** of which led to a revision or were + explicitly cited as influential by the PR author. +- Demonstrated familiarity with the full contributor workflow and the + project's governance: how issues are triaged, how nominations work, how + the release cadence operates, and how conflicts are resolved. +- At least **six months of active participation** at Reviewer level with + no gap longer than 30 consecutive days. +- Availability to respond to review assignments, security disclosures, + and time-sensitive governance decisions within the response windows + committed in `Governance/processes/ESCALATION_PATH.md`. +- No unresolved conduct matter. + +## Nomination and Approval + +Promotions to Reviewer and Maintainer follow the full nomination and +decision process defined in `Governance/processes/NOMINATION.md`: + +1. A nomination is opened as a public issue by any eligible nominator. +2. At least one second is posted within the 10-business-day seconding + window. +3. The maintainers record a decision within 5 business days of the + seconding window closing. + +The nomination evidence must include direct links to the pull requests and +review comments cited against the criteria above. Assertions without links +do not satisfy the evidence requirement. + +**Who may nominate.** Anyone who satisfies the definition of Contributor in +`Governance/roles/CONTRIBUTOR.md` may nominate, including the candidate +themselves. Self-nominations follow the same process as any other. + +**Who approves.** The existing Maintainers approve promotions, recorded by +simple majority of those who respond within the decision window. An +abstention is not a no. A decision reached with two or fewer participating +Maintainers is valid but should be reviewed at the next maintainer sync. + +## Evidence Requirements + +The nomination issue must include, at minimum: + +- A link to each pull request cited as evidence, with the merge date + visible. +- A link to each review comment cited as substantive, with the pull + request visible. +- A plain-language summary of the candidate's contribution to areas + outside their own pull requests (triaging, documentation, governance, + community support). +- The conflict declaration required by + `Governance/policies/CONFLICT_OF_INTEREST.md`. + +Evidence assembled after the nomination is opened is acceptable; it is +added as a comment on the nomination issue and is considered part of the +nomination record. Assertions without links do not satisfy the evidence +requirement, regardless of when they are added. + +## Ownership + +- Maintainers own this document and are responsible for keeping the + criteria aligned with the project's actual expectations. +- A change to the criteria is proposed in a pull request that touches + only the `Governance/` folder. +- When the project's contribution standards change in a way that affects + what counts as a qualifying pull request or review, this document is + updated in the same pull request. + +## Success + +This document succeeds when any contributor can read it and determine, +from their own public contribution record, whether they are eligible for +promotion; when no promotion is granted to a candidate who does not meet +the criteria; and when every maintainer can point to the criteria when +declining a nomination. + +## Regression Tests + +Regression coverage for this document lives in +`Governance/processes/PROMOTION_CRITERIA.test.ts`. It pins the per-role +criteria counts, the nomination and approval process, and the evidence +requirements to this document so they cannot silently regress — for +example, an edit that removes a criterion, lowers a threshold, or drops +the evidence requirement fails CI. + +## Revision History + +| Version | Date | Change | Author | +| ------- | ---------- | ---------------- | --------------------- | +| 1.0 | 2026-09-28 | Initial version. | TeachLink maintainers | diff --git a/Governance/processes/PROMOTION_CRITERIA.test.ts b/Governance/processes/PROMOTION_CRITERIA.test.ts new file mode 100644 index 00000000..331962dd --- /dev/null +++ b/Governance/processes/PROMOTION_CRITERIA.test.ts @@ -0,0 +1,264 @@ +/** + * Regression tests for the Role Promotion Criteria document + * (Governance/processes/PROMOTION_CRITERIA.md). + * + * The document makes concrete, enforceable guarantees: objective + * criteria for each step on the contributor ladder, a named nominator + * and approver, and explicit evidence requirements. These tests pin + * those guarantees to the checked-in document so they cannot silently + * regress — for example, an edit that removes a criterion, lowers a + * threshold, or drops the evidence requirement fails CI. + */ +import { describe, expect, it } from 'vitest'; +import { readFileSync } from 'node:fs'; +import path from 'node:path'; + +const DOC_PATH = path.resolve(__dirname, 'PROMOTION_CRITERIA.md'); +const doc = readFileSync(DOC_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(); +} + +const prose = plainProse(doc); + +/** Every "## Heading" in the document, in order. */ +const sections = [...doc.matchAll(/^## (.+)$/gm)].map((m) => m[1]); + +/** Extract the body of a single "## Section" (text up to next heading). */ +function sectionBody(title: string): string { + const start = doc.indexOf(`## ${title}\n`); + expect(start, `section "${title}" is missing`).toBeGreaterThanOrEqual(0); + const next = doc.indexOf('\n## ', start + 1); + const body = next === -1 ? doc.slice(start) : doc.slice(start, next); + return plainProse(body); +} + +// --------------------------------------------------------------------------- +// Document structure +// --------------------------------------------------------------------------- + +describe('PROMOTION_CRITERIA document structure', () => { + it('is titled "Role Promotion Criteria"', () => { + expect(doc.startsWith('# Role Promotion Criteria\n')).toBe(true); + }); + + it('keeps the canonical governance document sections in order', () => { + expect(sections).toEqual([ + 'Purpose', + 'Scope', + 'Criteria by Role', + 'Nomination and Approval', + 'Evidence Requirements', + 'Ownership', + 'Success', + 'Regression Tests', + 'Revision History', + ]); + }); + + it('covers the three areas the issue requires', () => { + expect(sections).toContain('Criteria by Role'); + expect(sections).toContain('Nomination and Approval'); + expect(sections).toContain('Evidence Requirements'); + }); + + it('has no unresolved template placeholders', () => { + expect(doc).not.toMatch(/TBD|TODO|FIXME|<[a-z-]+>|XXX/); + }); + + it('stays within the house documentation line width (max 82 columns)', () => { + const longest = Math.max(...doc.split('\n').map((line) => line.length)); + expect(longest).toBeLessThanOrEqual(82); + }); +}); + +// --------------------------------------------------------------------------- +// Criteria by role +// --------------------------------------------------------------------------- + +describe('contributor criteria', () => { + it('requires at least one merged pull request closing an assigned issue', () => { + expect(prose).toContain('one merged pull request'); + expect(prose).toContain('closes an assigned issue'); + }); + + it('requires adherence to quality gates', () => { + const required = ['type-check', 'lint', 'build', 'test']; + for (const gate of required) { + expect(prose, `must name quality gate ${gate}`).toContain(gate); + } + }); + + it('requires no unresolved conduct matter', () => { + expect(prose).toContain('no unresolved conduct matter'); + }); + + it('grants the contributor role automatically on first merged pr', () => { + expect(prose).toContain('granted automatically'); + expect(prose).toContain('no nomination required'); + }); +}); + +describe('reviewer criteria', () => { + const body = sectionBody('Criteria by Role'); + + it('requires at least five merged pull requests', () => { + expect(body).toContain('five merged pull requests'); + }); + + it('requires coverage of at least two different areas of the codebase', () => { + expect(body).toContain('two different areas'); + }); + + it('requires at least five substantive review comments', () => { + // appears twice (reviewer + maintainer), just check it is present + expect(body).toContain('five substantive review comments'); + }); + + it('requires at least three months of active participation', () => { + expect(body).toContain('three months of active participation'); + }); + + it('requires no gap longer than 30 consecutive days', () => { + expect(body).toContain('30 consecutive days'); + }); +}); + +describe('maintainer criteria', () => { + const body = sectionBody('Criteria by Role'); + + it('requires at least fifteen merged pull requests', () => { + expect(body).toContain('fifteen merged pull requests'); + }); + + it('requires at least five non-trivial pull requests', () => { + expect(body).toContain('at least five'); + expect(body).toContain('non-trivial scope'); + }); + + it('requires at least twenty substantive review comments', () => { + expect(body).toContain('twenty substantive review comments'); + }); + + it('requires at least five reviews cited as influential', () => { + expect(body).toContain('five'); + expect(body).toContain('cited as influential'); + }); + + it('requires at least six months of active participation at reviewer level', () => { + expect(body).toContain('six months of active participation'); + expect(body).toContain('reviewer level'); + }); + + it('requires availability within escalation response windows', () => { + expect(body).toContain('escalation_path.md'); + }); +}); + +// --------------------------------------------------------------------------- +// Nomination and approval guarantees +// --------------------------------------------------------------------------- + +describe('nomination and approval guarantees', () => { + const body = sectionBody('Nomination and Approval'); + + it('requires a public nomination issue', () => { + expect(body).toContain('public issue'); + }); + + it('requires at least one second within the 10-business-day window', () => { + expect(body).toContain('at least one second'); + expect(body).toContain('10-business-day'); + }); + + it('requires a decision within 5 business days of the window closing', () => { + expect(body).toContain('5 business days'); + expect(body).toContain('seconding window closing'); + }); + + it('allows self-nominations on the same terms', () => { + expect(body).toContain('self-nominations follow the same process'); + }); + + it('approves by simple majority of responding maintainers', () => { + expect(body).toContain('simple majority'); + expect(body).toContain('abstention is not a no'); + }); + + it('defers the full process to nomination.md', () => { + expect(body).toContain('nomination.md'); + }); +}); + +// --------------------------------------------------------------------------- +// Evidence requirements +// --------------------------------------------------------------------------- + +describe('evidence requirement guarantees', () => { + const body = sectionBody('Evidence Requirements'); + + it('requires links to every pull request cited', () => { + expect(body).toContain('link to each pull request'); + expect(body).toContain('merge date visible'); + }); + + it('requires links to every review comment cited', () => { + expect(body).toContain('link to each review comment'); + expect(body).toContain('pull request visible'); + }); + + it('requires a conflict declaration', () => { + expect(body).toContain('conflict_of_interest.md'); + }); + + it('rejects assertions without links', () => { + expect(body).toContain('assertions without links do not satisfy'); + }); + + it('allows evidence added after nomination opens', () => { + expect(body).toContain('added as a comment on the nomination issue'); + expect(body).toContain('part of the nomination record'); + }); +}); + +// --------------------------------------------------------------------------- +// Consistency with the broader governance folder +// --------------------------------------------------------------------------- + +describe('consistency with the governance folder', () => { + it('defers to the documents it builds on, which must exist', () => { + const referenced = [ + 'NOMINATION.md', // same folder + 'ESCALATION_PATH.md', // same folder + '../roles/CONTRIBUTOR.md', + '../roles/TREASURER.md', + '../policies/CONFLICT_OF_INTEREST.md', + '../RECOGNITION.md', + '../CODE_OF_CONDUCT.md', + ]; + for (const file of referenced) { + const name = path.basename(file).toLowerCase(); + expect(prose, `must reference ${name}`).toContain(name); + expect( + () => readFileSync(path.resolve(__dirname, file), 'utf8'), + ).not.toThrow(); + } + }); + + it('mentions the governance-folder-only rule for changes to this doc', () => { + 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/promotion_criteria.test.ts'); + }); +});