teqo/scripts/lib/plansOnlyClosesGuard.mjs
Cursor Agent 1ad8863994
OPS12 — CI bloqueia Closes/Fixes em PR só docs/plans
- Lógica pura em plansOnlyClosesGuard.mjs + CLI check-plans-only-pr-closes
- Job plans-only-closes em ci-pr.yml (rollup checks)
- Unit tests com fixture do incidente #115
- plan-issue, agent-pr-workflow, PR template e GUARDRAILS atualizados

Closes #116

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-01 11:26:19 +00:00

71 lines
2 KiB
JavaScript

/**
* plansOnlyClosesGuard — pure policy for PRs that touch only docs/plans/.
* Plans-only PRs must not carry GitHub issue-closing keywords (Closes/Fixes/Resolves).
*/
const PLANS_ONLY_PREFIX = 'docs/plans/'
/**
* @param {string} path - repo-relative path with forward slashes
*/
export function isUnderDocsPlans(path) {
return path === PLANS_ONLY_PREFIX.slice(0, -1) || path.startsWith(PLANS_ONLY_PREFIX)
}
/**
* True when every changed path lives under docs/plans/ (non-empty diff).
*
* @param {string[]} paths
*/
export function isPlansOnlyDiff(paths) {
if (paths.length === 0) return false
return paths.every(isUnderDocsPlans)
}
/** GitHub closing keywords + resolves family (case-insensitive). */
const ISSUE_CLOSING_KEYWORD_RE =
/\b(close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\s+#(\d+)\b/gi
/**
* @param {string} body
* @returns {{ keyword: string, number: number }[]}
*/
export function findIssueClosingKeywords(body) {
if (!body) return []
const closers = []
for (const match of body.matchAll(ISSUE_CLOSING_KEYWORD_RE)) {
closers.push({ keyword: match[1], number: Number(match[2]) })
}
return closers
}
/**
* @param {{ paths: string[], body: string | null | undefined }} input
* @returns {{ ok: true } | { ok: false, closers: { keyword: string, number: number }[], message: string }}
*/
export function assertPlansOnlyPrAllowsBody({ paths, body }) {
if (!isPlansOnlyDiff(paths)) {
return { ok: true }
}
const closers = findIssueClosingKeywords(body ?? '')
if (closers.length === 0) {
return { ok: true }
}
const unique = [
...new Map(
closers.map((closer) => [`${closer.keyword.toLowerCase()}:${closer.number}`, closer]),
).values(),
]
const listed = unique.map((closer) => `${closer.keyword} #${closer.number}`).join(', ')
return {
ok: false,
closers: unique,
message:
`PR que altera apenas docs/plans/ não pode usar keywords de fechamento (${listed}). ` +
'Use "Related #N" ou remova — a implementação fecha a Issue em outro PR.',
}
}