madeby 0.3.5 → 0.3.6
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/dist/madeby.mjs +172 -5
- package/package.json +12 -11
package/dist/madeby.mjs
CHANGED
|
@@ -37,6 +37,36 @@ function readGitLog(repoPath, limit, range) {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
// ../classify/src/classify.ts
|
|
40
|
+
var AI_PATTERNS = [
|
|
41
|
+
{ re: /claude/i, provider: "anthropic", model: "claude" },
|
|
42
|
+
{ re: /copilot/i, provider: "github-copilot" },
|
|
43
|
+
{ re: /\bcursor(?:agent)?\b/i, provider: "cursor" },
|
|
44
|
+
{ re: /\bdevin\b/i, provider: "cognition" },
|
|
45
|
+
{ re: /gemini/i, provider: "google" },
|
|
46
|
+
{ re: /chatgpt|openai|codex|\bgpt-/i, provider: "openai" },
|
|
47
|
+
{ re: /codeium|windsurf/i, provider: "codeium" },
|
|
48
|
+
{ re: /\baider\b/i, provider: "aider" },
|
|
49
|
+
{ re: /codewhisperer/i, provider: "amazon" },
|
|
50
|
+
{ re: /tabnine/i, provider: "tabnine" }
|
|
51
|
+
];
|
|
52
|
+
function matchAi(text) {
|
|
53
|
+
for (const p of AI_PATTERNS) {
|
|
54
|
+
if (p.re.test(text)) return { provider: p.provider, ...p.model ? { model: p.model } : {}, raw: text.trim() };
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
var GENERIC_AI_TRAILER = /^(?:AI|an? AI|AI (?:assistant|tool|pair)|artificial intelligence)$/i;
|
|
59
|
+
function genericAiDisclosure(who) {
|
|
60
|
+
const w = who.trim();
|
|
61
|
+
return GENERIC_AI_TRAILER.test(w) ? { provider: "unspecified", raw: w } : null;
|
|
62
|
+
}
|
|
63
|
+
var TRAILER_RE = /^[ \t]*(?:Co-authored-by|Generated-by|Assisted-by):[ \t]*(.+)$/gim;
|
|
64
|
+
var AI_AUTHOR_TRAILER = /^[ \t]*Authored-by-ai:[ \t]*(.+)$/gim;
|
|
65
|
+
var HUMAN_ATTEST = /^[ \t]*(?:Authored-by-human|Human-authored(?:-by)?):/im;
|
|
66
|
+
var CONF_WITH_AI = 0.9;
|
|
67
|
+
var CONF_AI = 0.85;
|
|
68
|
+
var CONF_HUMAN = 0.55;
|
|
69
|
+
var CONF_BOT = 0.95;
|
|
40
70
|
var KNOWN_BOTS = /\b(?:dependabot|renovate(?:-bot)?|greenkeeper|snyk-bot|github-actions|semantic-release-bot|stainless-app|allcontributors|mergify|codecov-commenter|pre-commit-ci|imgbot|whitesource-bot|copybara)\b/i;
|
|
41
71
|
function isBotIdentity(name, email) {
|
|
42
72
|
const n = (name ?? "").trim();
|
|
@@ -45,6 +75,60 @@ function isBotIdentity(name, email) {
|
|
|
45
75
|
if (e === "actions@github.com") return true;
|
|
46
76
|
return KNOWN_BOTS.test(n) || KNOWN_BOTS.test(e);
|
|
47
77
|
}
|
|
78
|
+
function classifyCommit(commit) {
|
|
79
|
+
const signals = [];
|
|
80
|
+
const aiContributors = [];
|
|
81
|
+
if (isBotIdentity(commit.authorName, commit.authorEmail)) {
|
|
82
|
+
const botText = `${commit.authorName ?? ""} ${commit.authorEmail ?? ""}`.trim();
|
|
83
|
+
const agentAi = botText ? matchAi(botText) : null;
|
|
84
|
+
if (agentAi) {
|
|
85
|
+
return {
|
|
86
|
+
class: "ai",
|
|
87
|
+
confidence: CONF_AI,
|
|
88
|
+
signals: [`author:${agentAi.provider}`],
|
|
89
|
+
aiContributors: [{ ...agentAi, raw: botText }]
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return { class: "bot", confidence: CONF_BOT, signals: ["author:bot"], aiContributors };
|
|
93
|
+
}
|
|
94
|
+
const aiAuthored = [...commit.message.matchAll(AI_AUTHOR_TRAILER)].map((m) => m[1].trim());
|
|
95
|
+
if (aiAuthored.length > 0 && !HUMAN_ATTEST.test(commit.message)) {
|
|
96
|
+
const who = aiAuthored[0];
|
|
97
|
+
const c = matchAi(who) ?? { provider: "unspecified", raw: who };
|
|
98
|
+
return { class: "ai", confidence: CONF_AI, signals: [`authored-by-ai:${c.provider}`], aiContributors: [c] };
|
|
99
|
+
}
|
|
100
|
+
const trailers = [...commit.message.matchAll(TRAILER_RE)].map((m) => m[1].trim());
|
|
101
|
+
let humanCoAuthors = 0;
|
|
102
|
+
for (const who of trailers) {
|
|
103
|
+
const ai = matchAi(who) ?? genericAiDisclosure(who);
|
|
104
|
+
if (ai) {
|
|
105
|
+
aiContributors.push(ai);
|
|
106
|
+
signals.push(`co-authored-by:${ai.provider}`);
|
|
107
|
+
} else {
|
|
108
|
+
humanCoAuthors += 1;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
for (const who of aiAuthored) {
|
|
112
|
+
const ai = matchAi(who) ?? { provider: "unspecified", raw: who };
|
|
113
|
+
aiContributors.push(ai);
|
|
114
|
+
signals.push(`authored-by-ai:${ai.provider}`);
|
|
115
|
+
}
|
|
116
|
+
const authorText = `${commit.authorName ?? ""} ${commit.authorEmail ?? ""}`.trim();
|
|
117
|
+
const authorAi = authorText ? matchAi(authorText) : null;
|
|
118
|
+
if (authorAi) {
|
|
119
|
+
aiContributors.push({ ...authorAi, raw: authorText });
|
|
120
|
+
signals.push(`author:${authorAi.provider}`);
|
|
121
|
+
}
|
|
122
|
+
const hasAi = aiContributors.length > 0;
|
|
123
|
+
const humanPresent = authorAi === null || humanCoAuthors > 0;
|
|
124
|
+
if (!hasAi) {
|
|
125
|
+
return { class: "human", confidence: CONF_HUMAN, signals, aiContributors };
|
|
126
|
+
}
|
|
127
|
+
if (authorAi !== null && !humanPresent) {
|
|
128
|
+
return { class: "ai", confidence: CONF_AI, signals, aiContributors };
|
|
129
|
+
}
|
|
130
|
+
return { class: "with_ai", confidence: CONF_WITH_AI, signals, aiContributors };
|
|
131
|
+
}
|
|
48
132
|
|
|
49
133
|
// ../classify/src/benchmark/benchmark-v0.json
|
|
50
134
|
var benchmark_v0_default = {
|
|
@@ -1091,7 +1175,7 @@ function provenanceMap(root, opts = {}) {
|
|
|
1091
1175
|
}
|
|
1092
1176
|
const commits = readDisclosures(root, { limit: opts.commitLimit ?? 500 });
|
|
1093
1177
|
const disclosed = commits.filter((c) => c.disclosures.length > 0).length;
|
|
1094
|
-
const
|
|
1178
|
+
const pct2 = commits.length ? Math.round(disclosed / commits.length * 100) : 0;
|
|
1095
1179
|
const scope = prefix ?? "repo";
|
|
1096
1180
|
const allModels = [...new Set(witnessed.flatMap((w) => w.models))];
|
|
1097
1181
|
let aiDefault;
|
|
@@ -1110,11 +1194,39 @@ function provenanceMap(root, opts = {}) {
|
|
|
1110
1194
|
witnessed,
|
|
1111
1195
|
commitCoverage: { disclosed, total: commits.length },
|
|
1112
1196
|
aiDisclosureDefault: aiDefault,
|
|
1113
|
-
summary: `${scope}: ${trackedFiles} tracked file${trackedFiles === 1 ? "" : "s"}; ${witnessed.length} with witnessed AI provenance${allModels.length ? ` (${allModels.join(", ")})` : ""}; ${
|
|
1197
|
+
summary: `${scope}: ${trackedFiles} tracked file${trackedFiles === 1 ? "" : "s"}; ${witnessed.length} with witnessed AI provenance${allModels.length ? ` (${allModels.join(", ")})` : ""}; ${pct2}% of the last ${commits.length} commits disclose origin.`,
|
|
1114
1198
|
note: "Witnessed spans are precise (region-level, model-named); commit coverage is commit-level, not per-file authorship. Files without a witnessed span have unknown origin \u2014 disclosure, not detection: we report what's disclosed and never infer origin we can't see. Resolution is a configurable convention (ARCH \xA712)."
|
|
1115
1199
|
};
|
|
1116
1200
|
}
|
|
1117
1201
|
|
|
1202
|
+
// ../analyzer/src/coverage.ts
|
|
1203
|
+
function repoCoverage(root, opts = {}) {
|
|
1204
|
+
const commits = readGitLog(root, opts.range ? void 0 : opts.limit ?? 500, opts.range);
|
|
1205
|
+
const byCategory = { human: 0, with_ai: 0, ai: 0, bot: 0 };
|
|
1206
|
+
const byTier = { asserted: 0, verified: 0 };
|
|
1207
|
+
let known = 0;
|
|
1208
|
+
for (const c of commits) {
|
|
1209
|
+
const kinds = commitDisclosureKinds({ message: c.message, signed: c.signed });
|
|
1210
|
+
const cls = classifyCommit({ message: c.message, authorName: c.authorName, authorEmail: c.authorEmail, signed: c.signed, sha: c.sha }).class;
|
|
1211
|
+
const category = cls === "human" && !kinds.includes("human-attestation") ? "unknown" : cls;
|
|
1212
|
+
if (category === "unknown") continue;
|
|
1213
|
+
known += 1;
|
|
1214
|
+
byCategory[category] += 1;
|
|
1215
|
+
if (c.signed) byTier.verified += 1;
|
|
1216
|
+
else byTier.asserted += 1;
|
|
1217
|
+
}
|
|
1218
|
+
return {
|
|
1219
|
+
isEstimate: true,
|
|
1220
|
+
methodology: "commit-level authorship disclosure (estimate); undisclosed = unknown, never inferred",
|
|
1221
|
+
scope: opts.range ? `range ${opts.range}` : `last ${commits.length} commits`,
|
|
1222
|
+
total: commits.length,
|
|
1223
|
+
known,
|
|
1224
|
+
unknown: commits.length - known,
|
|
1225
|
+
byCategory,
|
|
1226
|
+
byTier
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1118
1230
|
// ../analyzer/src/record-spans.ts
|
|
1119
1231
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
1120
1232
|
import { existsSync as existsSync3, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync, writeFileSync } from "node:fs";
|
|
@@ -1658,7 +1770,7 @@ function parseWithAi(args) {
|
|
|
1658
1770
|
function git3(root, args) {
|
|
1659
1771
|
return execFileSync5("git", ["-C", root, ...args], { encoding: "utf8" }).trim();
|
|
1660
1772
|
}
|
|
1661
|
-
var
|
|
1773
|
+
var HUMAN_ATTEST2 = /^[ \t]*(?:Authored-by-human|Human-authored(?:-by)?)[ \t]*:/im;
|
|
1662
1774
|
var ASSIST = /^[ \t]*Assisted-by[ \t]*:/im;
|
|
1663
1775
|
var AI_AUTHOR = /^[ \t]*Authored-by-ai[ \t]*:/im;
|
|
1664
1776
|
function attestAiAuthorship(root, opts = {}) {
|
|
@@ -1713,7 +1825,7 @@ function affirmAuthorship(root, opts = {}) {
|
|
|
1713
1825
|
} catch {
|
|
1714
1826
|
return { ...base, reason: "no-commit" };
|
|
1715
1827
|
}
|
|
1716
|
-
const hasHuman =
|
|
1828
|
+
const hasHuman = HUMAN_ATTEST2.test(message);
|
|
1717
1829
|
const hasAssist = ASSIST.test(message);
|
|
1718
1830
|
const trailers = [];
|
|
1719
1831
|
const added = [];
|
|
@@ -1879,6 +1991,49 @@ function whoCommand(args) {
|
|
|
1879
1991
|
}
|
|
1880
1992
|
}
|
|
1881
1993
|
|
|
1994
|
+
// src/commands/coverage.ts
|
|
1995
|
+
var pct = (n, d) => d > 0 ? Math.round(n / d * 100) : 0;
|
|
1996
|
+
var bar = (p) => "\u2588".repeat(Math.round(p / 5)) + "\u2591".repeat(20 - Math.round(p / 5));
|
|
1997
|
+
function render(r) {
|
|
1998
|
+
const knownPct = pct(r.known, r.total);
|
|
1999
|
+
const unknownPct = 100 - knownPct;
|
|
2000
|
+
console.log(`authorship coverage \u2014 ${r.scope} (estimate \xB7 disclosure, not detection)`);
|
|
2001
|
+
if (r.total === 0) {
|
|
2002
|
+
console.log(" (no commits to score)");
|
|
2003
|
+
return;
|
|
2004
|
+
}
|
|
2005
|
+
console.log(` known ${String(knownPct).padStart(3)}% ${bar(knownPct)} disclosed \u2014 on the record`);
|
|
2006
|
+
console.log(` unknown ${String(unknownPct).padStart(3)}% ${bar(unknownPct)} no disclosure \u2014 origin unknown, never guessed`);
|
|
2007
|
+
if (r.known > 0) {
|
|
2008
|
+
const c = r.byCategory;
|
|
2009
|
+
const parts = [`human ${pct(c.human, r.known)}%`, `with_ai ${pct(c.with_ai, r.known)}%`, `ai ${pct(c.ai, r.known)}%`];
|
|
2010
|
+
if (c.bot > 0) parts.push(`bot ${pct(c.bot, r.known)}%`);
|
|
2011
|
+
console.log(`
|
|
2012
|
+
of the ${r.known} known: ${parts.join(" \xB7 ")}`);
|
|
2013
|
+
console.log(` trust: asserted ${pct(r.byTier.asserted, r.known)}% \xB7 verified ${pct(r.byTier.verified, r.known)}% \u2014 self-reported; sign commits to verify`);
|
|
2014
|
+
}
|
|
2015
|
+
if (r.unknown > 0) {
|
|
2016
|
+
console.log(`
|
|
2017
|
+
\u2192 ${unknownPct}% unknown. Require disclosure going forward: madeby init --required`);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
function coverageCommand(args) {
|
|
2021
|
+
const json = args.includes("--json");
|
|
2022
|
+
const range = args.find((a) => !a.startsWith("--"));
|
|
2023
|
+
const root = repoRoot();
|
|
2024
|
+
if (!root) {
|
|
2025
|
+
console.error("madeby coverage: not a git repository.");
|
|
2026
|
+
process.exit(2);
|
|
2027
|
+
return;
|
|
2028
|
+
}
|
|
2029
|
+
const r = repoCoverage(root, { range });
|
|
2030
|
+
if (json) {
|
|
2031
|
+
console.log(JSON.stringify(r, null, 2));
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
2034
|
+
render(r);
|
|
2035
|
+
}
|
|
2036
|
+
|
|
1882
2037
|
// src/init-repo.ts
|
|
1883
2038
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1884
2039
|
import { dirname as dirname2, join as join5 } from "node:path";
|
|
@@ -2046,7 +2201,7 @@ madeby init \u2014 policy: ${r.policyMode}, host: ${r.host}`);
|
|
|
2046
2201
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
2047
2202
|
import { createInterface } from "node:readline";
|
|
2048
2203
|
var DEFAULT_PROTOCOL = "2024-11-05";
|
|
2049
|
-
var SERVER_INFO = { name: "madeby", version: "0.3.
|
|
2204
|
+
var SERVER_INFO = { name: "madeby", version: "0.3.6" };
|
|
2050
2205
|
function rootFor(path) {
|
|
2051
2206
|
const cwd = path && path.length ? path : process.cwd();
|
|
2052
2207
|
return execFileSync6("git", ["-C", cwd, "-c", "core.quotePath=false", "rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
|
|
@@ -2079,6 +2234,12 @@ var TOOLS = [
|
|
|
2079
2234
|
inputSchema: repoInput,
|
|
2080
2235
|
handler: (args) => evaluateRepoDisclosure(rootFor(args.path), { range: args.range })
|
|
2081
2236
|
},
|
|
2237
|
+
{
|
|
2238
|
+
name: "coverage",
|
|
2239
|
+
description: "Authorship coverage scoreboard for a repo \u2014 the honest estimate of how much of the code's authorship is ON THE RECORD (known) vs undisclosed (unknown \u2014 origin unknown, NEVER guessed), the human/with_ai/ai mix OF THE KNOWN (never of the whole repo), and the asserted-vs-verified trust split. Disclosure, not detection; the report is a labeled estimate (counts only \u2014 derive percents at the edge). Use it to show a maintainer their provenance picture and motivate requiring disclosure.",
|
|
2240
|
+
inputSchema: repoInput,
|
|
2241
|
+
handler: (args) => repoCoverage(rootFor(args.path), { range: args.range })
|
|
2242
|
+
},
|
|
2082
2243
|
{
|
|
2083
2244
|
name: "who",
|
|
2084
2245
|
description: "Made by whom? Read what's ON THE RECORD about who made this. With `path`: one file's origin \u2014 witnessed AI spans (model/tool, precise) where captured, last-touch commit disclosure from git blame elsewhere (approximate, labeled), unknown otherwise. With no `path`: a repo-wide map (the orientation to read BEFORE you work) \u2014 the witnessed AI surface plus commit-level disclosure coverage, cheap (no per-file blame). The universal contract is the line range: for a symbol, turn it into a line range with your own tooling (tree-sitter/LSP/ctags) and pass it \u2014 MadeBy never parses code. Disclosure, not detection: it reports what's disclosed and never asserts 'human'.",
|
|
@@ -2342,6 +2503,9 @@ Usage:
|
|
|
2342
2503
|
--json for a machine-readable result.
|
|
2343
2504
|
madeby who [<path>] Made by whom? Read what's on the record about who made this \u2014 one
|
|
2344
2505
|
(alias: whom) file's origin, or the whole repo with no path. --lines=A-B to scope.
|
|
2506
|
+
madeby coverage [<range>] The scoreboard: how much of your repo's authorship is disclosed (known)
|
|
2507
|
+
vs unknown, the human/with_ai/ai mix, and the asserted/verified split.
|
|
2508
|
+
--json for a machine-readable result.
|
|
2345
2509
|
madeby ai [<log>] [<ref>] Made by AI: record your AI session's witnessed spans into .madeby/spans.
|
|
2346
2510
|
<log> defaults to the auto-discovered Claude Code transcript.
|
|
2347
2511
|
madeby me Made by me: affirm you authored HEAD yourself (an Authored-by-human
|
|
@@ -2366,6 +2530,9 @@ switch (cmd) {
|
|
|
2366
2530
|
case "whom":
|
|
2367
2531
|
whoCommand(rest);
|
|
2368
2532
|
break;
|
|
2533
|
+
case "coverage":
|
|
2534
|
+
coverageCommand(rest);
|
|
2535
|
+
break;
|
|
2369
2536
|
case "ai":
|
|
2370
2537
|
aiCommand(rest);
|
|
2371
2538
|
break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "madeby",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"mcpName": "io.github.MadeByFYI/madeby",
|
|
5
5
|
"description": "Disclose AI authorship of code. `npx madeby ai` adds an Authored-by-ai git trailer; AI coding agents disclose per the ACO standard (human/with_ai/ai) from an AGENTS.md convention. Disclosure, not detection. Gate PRs with `madeby check`.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -47,18 +47,19 @@
|
|
|
47
47
|
"dist",
|
|
48
48
|
"README.md"
|
|
49
49
|
],
|
|
50
|
-
"devDependencies": {
|
|
51
|
-
"@types/node": "^22.10.0",
|
|
52
|
-
"esbuild": "^0.25.0",
|
|
53
|
-
"typescript": "^5.7.0",
|
|
54
|
-
"vitest": "^4.1.9",
|
|
55
|
-
"@madeby/analyzer": "0.0.0",
|
|
56
|
-
"@madeby/core": "0.0.0"
|
|
57
|
-
},
|
|
58
50
|
"scripts": {
|
|
59
51
|
"build": "esbuild src/madeby.ts --bundle --platform=node --format=esm --target=node20 --outfile=dist/madeby.mjs --banner:js='#!/usr/bin/env node' && chmod +x dist/madeby.mjs",
|
|
60
52
|
"typecheck": "tsc --noEmit",
|
|
61
53
|
"pretest": "pnpm build",
|
|
62
|
-
"test": "vitest run"
|
|
54
|
+
"test": "vitest run",
|
|
55
|
+
"prepublishOnly": "pnpm build"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@madeby/analyzer": "workspace:*",
|
|
59
|
+
"@madeby/core": "workspace:*",
|
|
60
|
+
"@types/node": "^22.10.0",
|
|
61
|
+
"esbuild": "^0.25.0",
|
|
62
|
+
"typescript": "^5.7.0",
|
|
63
|
+
"vitest": "^4.1.9"
|
|
63
64
|
}
|
|
64
|
-
}
|
|
65
|
+
}
|