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.
Files changed (2) hide show
  1. package/dist/madeby.mjs +172 -5
  2. 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 pct = commits.length ? Math.round(disclosed / commits.length * 100) : 0;
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(", ")})` : ""}; ${pct}% of the last ${commits.length} commits disclose origin.`,
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 HUMAN_ATTEST = /^[ \t]*(?:Authored-by-human|Human-authored(?:-by)?)[ \t]*:/im;
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 = HUMAN_ATTEST.test(message);
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.5" };
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.5",
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
+ }