chore: update repo policy to check tool co-authors
Some checks are pending
Checks / lint (push) Waiting to run
Checks / test (push) Waiting to run
Checks / content (push) Waiting to run

This commit is contained in:
ayo 2026-07-18 22:04:35 +02:00
parent f4ac3b0c12
commit 93e8258270
5 changed files with 42 additions and 31 deletions

View file

@ -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"
]
}

View file

@ -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.

View file

@ -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.

View file

@ -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

View file

@ -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 <jane@example.com>', policy)).toEqual([])
expect(checkStructural('Co-authored-by: Ayo <ayo@ayco.io>', policy)).toEqual([])
})
it('flags an unlisted one, whatever the casing', () => {
const findings = checkStructural('Co-Authored-By: Some Tool <tool@example.com>', policy)
it('flags a tool trailer, whatever the casing', () => {
const findings = checkStructural('Co-Authored-By: Some Model <noreply@anthropic.com>', 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', () => {