From 93e8258270e2498f091e27cf33e63732d059bc7c Mon Sep 17 00:00:00 2001 From: Ayo Date: Sat, 18 Jul 2026 22:04:35 +0200 Subject: [PATCH] chore: update repo policy to check tool co-authors --- .repo-policy.json | 9 ++++--- AGENTS.md | 2 +- CONTRIBUTING.md | 7 +++--- scripts/check-content.mjs | 15 ++++++----- scripts/test/check-content.test.js | 40 +++++++++++++++++------------- 5 files changed, 42 insertions(+), 31 deletions(-) diff --git a/.repo-policy.json b/.repo-policy.json index 3d6897e..bfddd7b 100644 --- a/.repo-policy.json +++ b/.repo-policy.json @@ -2,9 +2,10 @@ "version": 1, "salt": "40f076a2ae183c348be3e11a8b58ebac", "digests": [], - "allowedCoAuthors": [ - "ayo@ayco.io", - "ramon.aycojr@gmail.com", - "*@users.noreply.github.com" + "toolCoAuthors": [ + "*@anthropic.com", + "*@openai.com", + "*@cursor.com", + "*@devin.ai" ] } diff --git a/AGENTS.md b/AGENTS.md index cc9f52f..0371d70 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -126,7 +126,7 @@ App-owned modules in `modules/`: `UserService` (`user/user.js`) derives a non-cr - **Code style is enforced by ESLint Stylistic**, not Prettier: 2-space indent, single quotes, **no semicolons**, no trailing commas, spaces inside `{ braces }` but not `[brackets]`. Run `pnpm lint:fix` before committing. Both `**/*.js` and `**/*.css` are linted (CSS via `@eslint/css`). - `modules/` uses ES classes; `scripts/` uses plain functions. Match the surrounding style of the file you edit. -- **Content policy.** Commit messages, branch names, PR text, and contributed lines are checked by `scripts/check-content.mjs` (hooks + the `Checks` workflow) against `.repo-policy.json`. Write commit messages in plain project voice; no tool-attribution trailers or footers, no `Co-Authored-By:` line for anyone outside the policy's `allowedCoAuthors`, no session links. +- **Content policy.** Commit messages, branch names, PR text, and contributed lines are checked by `scripts/check-content.mjs` (hooks + the `Checks` workflow) against `.repo-policy.json`. Write commit messages in plain project voice; no tool-attribution trailers or footers, no `Co-Authored-By:` line for a non-human contributor (the policy's `toolCoAuthors` list — human co-authors are always fine), no session links. - The same scanner matches text against a maintainer-managed reserved-terms list. Findings report a location and a masked preview, never the term. If one flags your change, reword it or ask a maintainer — don't edit `.repo-policy.json`. - **Source stays JS + JSDoc (`// @ts-check`)** — no TypeScript. Nothing is published from this repo, so there is no type-generation step here; the `.d.ts` files shipped by `@cozy-games/*` are generated in the cozy-games repo. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 443a9b1..15681d0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -112,9 +112,10 @@ For anything large, open an issue to discuss the approach first. ## Commit & PR hygiene Write commit messages in plain project voice — what changed and why. Keep them -free of tool-attribution trailers or footers (including `Co-Authored-By:` lines -for anyone who isn't a human contributor) and of session links; the same goes for -PR titles and bodies. +free of AI-tool attribution trailers or footers (including `Co-Authored-By:` +lines crediting a coding assistant) and of session links; the same goes for PR +titles and bodies. Co-authoring with another *person* is always fine — use any +address you like, there is no contributor allowlist. CI also checks your changes against a maintainer-managed reserved-terms list. If a check flags your change, reword it — or ask a maintainer if it isn't clear why. diff --git a/scripts/check-content.mjs b/scripts/check-content.mjs index b57f0cc..edbad65 100644 --- a/scripts/check-content.mjs +++ b/scripts/check-content.mjs @@ -33,7 +33,8 @@ const ALLOW_MARKER = 'content-policy: allow-next-line' * @property {number} version * @property {string} salt - HMAC key for the digest list. * @property {string[]} digests - Reserved-term digests, maintainer-managed. - * @property {string[]} allowedCoAuthors - Emails; a leading `*` matches a suffix. + * @property {string[]} toolCoAuthors - Non-human co-author addresses to reject; + * a leading `*` matches a suffix. Every other address is a human, and passes. */ /** @@ -131,14 +132,16 @@ const CO_AUTHOR_PATTERN = /^\s*co-authored-by:\s*(.+)$/i const EMAIL_PATTERN = /<([^>]+)>/ /** + * Is this co-author address a tool rather than a person? Humans are the default: + * only addresses matching the policy's tool list are rejected. * @param {string} email - * @param {string[]} [allowed] - Exact emails, or `*@domain` suffix patterns. + * @param {string[]} [tools] - Exact emails, or `*@domain` suffix patterns. * @returns {boolean} */ -export function isAllowedCoAuthor(email, allowed = []) { +export function isToolCoAuthor(email, tools = []) { const value = String(email).trim().toLowerCase() if (!value) return false - return allowed.some((entry) => { + return tools.some((entry) => { const pattern = String(entry).trim().toLowerCase() return pattern.startsWith('*') ? value.endsWith(pattern.slice(1)) : value === pattern }) @@ -171,8 +174,8 @@ export function checkStructural(line, policy) { const trailer = CO_AUTHOR_PATTERN.exec(line) if (trailer) { const email = (EMAIL_PATTERN.exec(trailer[1]) || [])[1] || '' - if (!isAllowedCoAuthor(email, policy.allowedCoAuthors)) { - findings.push({ rule: 'unlisted-co-author', detail: preview(line) }) + if (isToolCoAuthor(email, policy.toolCoAuthors)) { + findings.push({ rule: 'tool-co-author', detail: preview(line) }) } } return findings diff --git a/scripts/test/check-content.test.js b/scripts/test/check-content.test.js index f930bff..b90e2b5 100644 --- a/scripts/test/check-content.test.js +++ b/scripts/test/check-content.test.js @@ -7,7 +7,7 @@ import { digestCandidate, maskTerm, matchReserved, - isAllowedCoAuthor, + isToolCoAuthor, checkStructural, scanText, isAllowedByMarker, @@ -116,36 +116,42 @@ describe('maskTerm', () => { }) }) -describe('isAllowedCoAuthor', () => { - it('accepts the repo\'s listed humans, case-insensitively', () => { - expect(isAllowedCoAuthor('ayo@ayco.io', policy.allowedCoAuthors)).toBe(true) - expect(isAllowedCoAuthor('Ramon.AycoJr@gmail.com', policy.allowedCoAuthors)).toBe(true) +describe('isToolCoAuthor', () => { + it('matches a tool address by domain, case-insensitively', () => { + expect(isToolCoAuthor('noreply@anthropic.com', policy.toolCoAuthors)).toBe(true) + expect(isToolCoAuthor('NoReply@OpenAI.com', policy.toolCoAuthors)).toBe(true) }) - it('accepts a `*` suffix pattern', () => { - expect(isAllowedCoAuthor('1234+someone@users.noreply.github.com', policy.allowedCoAuthors)).toBe(true) + it('leaves dependabot alone — it is honest automation, not AI attribution', () => { + expect(isToolCoAuthor('49699333+dependabot[bot]@users.noreply.github.com', policy.toolCoAuthors)).toBe(false) }) - it('rejects anyone else, and an empty address', () => { - expect(isAllowedCoAuthor('someone@example.com', policy.allowedCoAuthors)).toBe(false) - expect(isAllowedCoAuthor('ayo@ayco.io.example.com', policy.allowedCoAuthors)).toBe(false) - expect(isAllowedCoAuthor('', policy.allowedCoAuthors)).toBe(false) + it('treats every other address as a human — contributors are not allowlisted', () => { + expect(isToolCoAuthor('jane@example.com', policy.toolCoAuthors)).toBe(false) + expect(isToolCoAuthor('ayo@ayco.io', policy.toolCoAuthors)).toBe(false) + expect(isToolCoAuthor('1234+someone@users.noreply.github.com', policy.toolCoAuthors)).toBe(false) + expect(isToolCoAuthor('', policy.toolCoAuthors)).toBe(false) + }) + + it('does not match a lookalike domain', () => { + expect(isToolCoAuthor('someone@anthropic.com.example.com', policy.toolCoAuthors)).toBe(false) }) }) describe('checkStructural: co-author trailers', () => { - it('allows a listed co-author', () => { + it('allows an outside human contributor using any address', () => { + expect(checkStructural('Co-authored-by: Jane Dev ', policy)).toEqual([]) expect(checkStructural('Co-authored-by: Ayo ', policy)).toEqual([]) }) - it('flags an unlisted one, whatever the casing', () => { - const findings = checkStructural('Co-Authored-By: Some Tool ', policy) + it('flags a tool trailer, whatever the casing', () => { + const findings = checkStructural('Co-Authored-By: Some Model ', policy) expect(findings).toHaveLength(1) - expect(findings[0].rule).toBe('unlisted-co-author') + expect(findings[0].rule).toBe('tool-co-author') }) - it('flags a trailer with no address at all', () => { - expect(checkStructural('Co-authored-by: Some Tool', policy)).toHaveLength(1) + it('allows a trailer with no address — a human typo, not a tool', () => { + expect(checkStructural('Co-authored-by: Jane Dev', policy)).toEqual([]) }) it('ignores prose that merely mentions co-authors', () => {