@heroiclands/package-build 22.9.1 → 22.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/docs/project-setup.md +23 -0
- package/githooks/attribution.mjs +82 -0
- package/githooks/commit-message.mjs +26 -0
- package/githooks/commit-msg +3 -35
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/docs/project-setup.md
CHANGED
|
@@ -95,6 +95,29 @@ precedence: a plain `git config` sets one clone, `--global` sets a machine.
|
|
|
95
95
|
The defaults differ deliberately: a guard that costs nothing is on unless
|
|
96
96
|
refused, and one that runs a container for minutes is off unless asked for.
|
|
97
97
|
|
|
98
|
+
The commit-message guard requires Node, like the rest of the toolchain. It checks
|
|
99
|
+
complete coauthor trailers and standalone credit clauses, with indentation,
|
|
100
|
+
bullets, emoji, Markdown emphasis and inline links permitted as decoration.
|
|
101
|
+
Supported identities are Codex, ChatGPT, OpenAI Codex, OpenAI ChatGPT, OpenAI,
|
|
102
|
+
Claude, Claude Code, Anthropic,
|
|
103
|
+
GitHub Copilot, Gemini, Cursor, and generic AI assistant credits. Signature forms
|
|
104
|
+
are generated by/with, assisted by, written by, AI-assisted and AI-generated.
|
|
105
|
+
Matching ignores case. Human coauthors such as Claudette Smith and Jean-Claude
|
|
106
|
+
Martin, explanatory prose, and ordinary mentions of these tools pass.
|
|
107
|
+
|
|
108
|
+
Specific assistant mailbox identities also identify a coauthor credit:
|
|
109
|
+
`codex@openai.com`, `chatgpt@openai.com`, `noreply@openai.com`,
|
|
110
|
+
`claude@anthropic.com` and `noreply@anthropic.com`. A provider's email domain
|
|
111
|
+
alone does not identify an assistant. This is an explicit set of credit forms,
|
|
112
|
+
not a classifier for arbitrary natural-language attribution.
|
|
113
|
+
|
|
114
|
+
Git comment lines and the verbose diff after a scissors comment are excluded.
|
|
115
|
+
Findings report the original message line and column. This local hook examines
|
|
116
|
+
commit messages only; the shared No Attribution workflow also checks pull
|
|
117
|
+
request titles and bodies. Neither examines repository files, issues, comments,
|
|
118
|
+
or Git author/committer metadata. The package-build repository's own hook uses
|
|
119
|
+
the same matching code and remains enabled independently of the consumer switch.
|
|
120
|
+
|
|
98
121
|
The branch guard is the one you meet first:
|
|
99
122
|
|
|
100
123
|
```console
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
3
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Bounded identities and standalone credit forms shared with the commit hooks. */
|
|
7
|
+
export const identities = [
|
|
8
|
+
"Codex",
|
|
9
|
+
"ChatGPT",
|
|
10
|
+
"OpenAI",
|
|
11
|
+
"OpenAI Codex",
|
|
12
|
+
"OpenAI ChatGPT",
|
|
13
|
+
"Claude",
|
|
14
|
+
"Claude Code",
|
|
15
|
+
"Anthropic",
|
|
16
|
+
"GitHub Copilot",
|
|
17
|
+
"Gemini",
|
|
18
|
+
"Cursor",
|
|
19
|
+
"AI",
|
|
20
|
+
"AI assistant",
|
|
21
|
+
"an AI assistant",
|
|
22
|
+
];
|
|
23
|
+
export const phrases = ["Generated by", "Generated with", "Assisted by", "Written by"];
|
|
24
|
+
export const mailboxes = [
|
|
25
|
+
"codex@openai.com",
|
|
26
|
+
"chatgpt@openai.com",
|
|
27
|
+
"noreply@openai.com",
|
|
28
|
+
"claude@anthropic.com",
|
|
29
|
+
"noreply@anthropic.com",
|
|
30
|
+
];
|
|
31
|
+
const identity = new RegExp(`^(?:${identities.join("|")})$`, "i");
|
|
32
|
+
const signature = new RegExp(`^(?:${phrases.join("|")})\\s+(.+?)[.!]?$`, "i");
|
|
33
|
+
|
|
34
|
+
/** Normalize decoration while keeping each character's original UTF-16 offset. */
|
|
35
|
+
function normalize(line) {
|
|
36
|
+
let chars = Array.from({ length: line.length }, (_, offset) => ({
|
|
37
|
+
text: line[offset],
|
|
38
|
+
offset,
|
|
39
|
+
}));
|
|
40
|
+
const replace = (pattern, keep) => {
|
|
41
|
+
const text = chars.map((char) => char.text).join("");
|
|
42
|
+
const matches = [...text.matchAll(pattern)];
|
|
43
|
+
for (const match of matches.reverse()) {
|
|
44
|
+
const retained = keep(match);
|
|
45
|
+
chars.splice(
|
|
46
|
+
match.index,
|
|
47
|
+
match[0].length,
|
|
48
|
+
...chars.slice(match.index + retained.start, match.index + retained.end),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
replace(/\[([^\]]+)\]\([^\s)]*\)/g, (match) => ({ start: 1, end: 1 + match[1].length }));
|
|
53
|
+
replace(/[*_`~]/g, () => ({ start: 0, end: 0 }));
|
|
54
|
+
replace(/^[^\p{L}]+/gu, () => ({ start: 0, end: 0 }));
|
|
55
|
+
return {
|
|
56
|
+
text: chars
|
|
57
|
+
.map((char) => char.text)
|
|
58
|
+
.join("")
|
|
59
|
+
.trimEnd(),
|
|
60
|
+
column: (chars[0]?.offset ?? 0) + 1,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Find a complete credit clause; ordinary prose and human identities pass. */
|
|
65
|
+
export function attribution(line) {
|
|
66
|
+
const normalized = normalize(line);
|
|
67
|
+
const trailer = normalized.text.match(/^co-authored-by:\s*(.*?)\s*(?:<([^<>]+)>)?$/i);
|
|
68
|
+
if (trailer && (identity.test(trailer[1]) || mailboxes.includes(trailer[2]?.toLowerCase()))) {
|
|
69
|
+
return { column: normalized.column, kind: "Co-Authored-By trailer naming an assistant" };
|
|
70
|
+
}
|
|
71
|
+
const credit = normalized.text.match(signature);
|
|
72
|
+
const generic = normalized.text.match(
|
|
73
|
+
/^AI-(?:assisted|generated)(?:\s+(?:by|with)\s+(.+?))?[.!]?$/i,
|
|
74
|
+
);
|
|
75
|
+
if (
|
|
76
|
+
(credit && identity.test(credit[1])) ||
|
|
77
|
+
(generic && (!generic[1] || identity.test(generic[1])))
|
|
78
|
+
) {
|
|
79
|
+
return { column: normalized.column, kind: "assistant signature" };
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
3
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { relative } from "node:path";
|
|
7
|
+
import { attribution } from "./attribution.mjs";
|
|
8
|
+
|
|
9
|
+
const file = process.argv[2];
|
|
10
|
+
const lines = readFileSync(file, "utf8").split(/\r?\n/);
|
|
11
|
+
let rejected = false;
|
|
12
|
+
for (const [index, line] of lines.entries()) {
|
|
13
|
+
// Git's verbose diff starts at the scissors comment; other comments are inert.
|
|
14
|
+
if (/^#.*>8/.test(line)) break;
|
|
15
|
+
if (line.startsWith("#")) continue;
|
|
16
|
+
const finding = attribution(line);
|
|
17
|
+
if (!finding) continue;
|
|
18
|
+
console.error(
|
|
19
|
+
`${relative(process.cwd(), file)}:${index + 1}:${finding.column}: error: ${finding.kind}; attribution is not allowed in commit messages.`,
|
|
20
|
+
);
|
|
21
|
+
rejected = true;
|
|
22
|
+
}
|
|
23
|
+
if (rejected) {
|
|
24
|
+
console.error("Remove the assistant credit and commit again.");
|
|
25
|
+
process.exitCode = 1;
|
|
26
|
+
}
|
package/githooks/commit-msg
CHANGED
|
@@ -1,40 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env sh
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
# This project does not want Co-Authored-By trailers naming an AI assistant, nor
|
|
6
|
-
# "Generated with Claude Code"-style signatures, in its history. The matching CI
|
|
7
|
-
# guard is the shared HeroicLands/.github/actions/no-attribution action, which
|
|
8
|
-
# this repository's .github/workflows/no-attribution.yml calls. That action holds
|
|
9
|
-
# the pattern for every repository; this hook is the only other copy, because a
|
|
10
|
-
# git hook runs from your checkout and cannot live in an action. Keep them in sync.
|
|
11
|
-
#
|
|
12
|
-
# Installed for everyone via the package.json "prepare" script, which points
|
|
13
|
-
# git at this directory (`git config core.hooksPath .githooks`) on `npm install`.
|
|
2
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
# Check the commit message using the same contract as the shared CI action.
|
|
14
4
|
|
|
15
5
|
. "$(dirname "$0")/hook-enabled.sh"
|
|
16
|
-
|
|
17
|
-
# On unless refused: it costs nothing, and the No Attribution workflow enforces
|
|
18
|
-
# the same rule on GitHub, so opting out locally only moves the failure later.
|
|
19
6
|
hook_enabled noAttribution true || exit 0
|
|
20
7
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
# Scan only the real message: drop the diff appended by `commit --verbose` (from
|
|
24
|
-
# the scissors comment onward) and strip remaining comment lines.
|
|
25
|
-
content="$(sed '/^#.*>8/,$d' "$msg_file" | grep -v '^#')"
|
|
26
|
-
|
|
27
|
-
# Anchored to the start of a line: real attribution is a trailer / signature at
|
|
28
|
-
# column 0, so prose that merely *mentions* these phrases mid-sentence (e.g. this
|
|
29
|
-
# hook's own docs) does not trip it. The generated-with branch tolerates a
|
|
30
|
-
# leading emoji/space.
|
|
31
|
-
pattern='^[[:space:]]*co-authored-by:.*(claude|anthropic)|^[^[:alpha:]]*generated with .*claude code'
|
|
32
|
-
|
|
33
|
-
if printf '%s\n' "$content" | grep -iEq "$pattern"; then
|
|
34
|
-
echo "commit-msg: AI/assistant attribution is not allowed in commit messages." >&2
|
|
35
|
-
printf '%s\n' "$content" | grep -iEn "$pattern" | sed 's/^/ /' >&2
|
|
36
|
-
echo "Remove the 'Co-Authored-By: …' / 'Generated with Claude Code' line and commit again." >&2
|
|
37
|
-
exit 1
|
|
38
|
-
fi
|
|
39
|
-
|
|
40
|
-
exit 0
|
|
8
|
+
exec node "$(dirname "$0")/commit-message.mjs" "$1"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "22.9.
|
|
3
|
+
"version": "22.9.2",
|
|
4
4
|
"description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|