Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/core/evaluator.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ const GENERIC_TITLES = new Set([
"quick fix"
]);

const TOOL_GENERATION_DISCLOSURE = /\b(chatgpt|claude|copilot|llm|coding assistant|ai-generated|ai generated|generated by ai|generated by a tool|tool-generated|tool-suggested|coccinelle generated|checkpatch\.pl --fix)\b/i;

const SIGNALS = {
issueLink: /\b(fix(?:e[sd])?|close[sd]?|resolve[sd]?)\s+#\d+\b|github\.com\/[^/\s]+\/[^/\s]+\/issues\/\d+/i,
testMention: /\b(npm test|node --test|pytest|cargo test|go test|unit tests?|integration tests?|manual(?:ly)? test(?:ed)?|verified|verification)\b/i,
Expand All @@ -31,7 +33,7 @@ const SIGNALS = {
version: /\b(version|commit|sha|main|release|v?\d{4}\.\d+(?:\.\d+)?|v\d+\.\d+|node\s+\d+|python\s+\d+|os:|environment)\b/i,
logs: /```|stack trace|traceback|journal|log output|error output|exception|panic/i,
rootCause: /\b(root cause|cause[ds]?|because|bisect|regression|culprit|patch|proposed fix|able to fix|workaround|analysis)\b/i,
aiDisclosure: /\b(ai|llm|chatgpt|copilot|claude|gemini|generated)\b/i,
aiDisclosure: TOOL_GENERATION_DISCLOSURE,
aiReportClaim: /\b(?:asked\s+(?:an?\s+)?ai|ai\s+(?:said|says|found|reported|detected|suggested)|(?:chatgpt|copilot|claude|gemini|ai tool|llm)\s+(?:said|says|found|reported|detected|suggested|generated|wrote|claims?))\b/i,
humanAccountability: /\b(tested|verified|reviewed|reproduced|i understand|manual|locally)\b/i,
dependencyJustification: /\b(dependenc(?:y|ies)|package|lockfile|upgrade|security update|npm install|npm audit|vulnerability)\b/i,
Expand Down Expand Up @@ -1520,7 +1522,7 @@ function hasHumanSignedOff(text) {
}

function hasMeaningfulToolGeneration(text) {
return /\b(chatgpt|claude|copilot|llm|coding assistant|ai-generated|ai generated|generated by ai|generated by a tool|tool-generated|tool-suggested|coccinelle generated|checkpatch\.pl --fix)\b/i.test(text);
return TOOL_GENERATION_DISCLOSURE.test(text);
}

function analyzeProvenance(input) {
Expand Down
49 changes: 49 additions & 0 deletions test/evaluator.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,55 @@ test("ready pull request passes as reviewable", async () => {
assert.doesNotMatch(result.comment, /- undefined:/);
});

test("generated artifact wording is not treated as AI disclosure", () => {
const result = evaluateContribution({
kind: "pull_request",
title: "docs: update checked-in reference README",
body: [
"Closes #42.",
"",
"The generated README is checked in because repository policy requires the reference output to remain synchronized with its source."
].join("\n"),
authorAssociation: "MEMBER",
draft: false,
changedFiles: 1,
additions: 4,
deletions: 2,
files: [
{ filename: "docs/reference/README.md", additions: 4, deletions: 2 }
],
checks: [
{ name: "readme-consistency", conclusion: "success" }
]
});

assert.ok(result.checks.some((check) => check.id === "accountability" && check.status === "pass"));
assert.equal(result.provenance.toolGenerated, false);
});

test("explicit tool generation without human verification still fails accountability", () => {
const result = evaluateContribution({
kind: "pull_request",
title: "fix: handle empty parser input",
body: "Closes #42.\n\nChatGPT generated this patch. It should work.",
authorAssociation: "CONTRIBUTOR",
draft: false,
changedFiles: 2,
additions: 12,
deletions: 3,
files: [
{ filename: "src/parser.mjs", additions: 8, deletions: 3 },
{ filename: "test/parser.test.mjs", additions: 4, deletions: 0 }
],
checks: [
{ name: "test", conclusion: "success" }
]
});

assert.ok(result.labels.includes("needs-human-verification"));
assert.ok(result.checks.some((check) => check.id === "accountability" && check.status === "fail"));
});

test("unready issue requires reproducer and real evidence", async () => {
const result = evaluateContribution(await fixture("issue-unready"));
assert.equal(result.kind, "issue");
Expand Down