madeby 0.3.5 → 0.3.7

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 (3) hide show
  1. package/README.md +29 -0
  2. package/dist/madeby.mjs +234 -20
  3. package/package.json +13 -12
package/README.md CHANGED
@@ -44,6 +44,8 @@ npm i -g madeby
44
44
  madeby init Set MadeBy up: write .madeby/policy.json + the CI disclosure check.
45
45
  madeby check [<range>] The gate: do commits meet .madeby/policy.json? (a verdict — exit code
46
46
  gates). <range> e.g. origin/main..HEAD — a PR's own commits. --json too.
47
+ madeby coverage [<range>] The scoreboard: how much of the repo's authorship is disclosed (known)
48
+ vs unknown, the human/with_ai/ai mix, and the asserted/verified split.
47
49
  madeby who [<path>] Made by whom? What's on the record about who made this — one file's
48
50
  (alias: whom) origin, or the whole repo with no path. --lines=A-B to scope. --json too.
49
51
  madeby ai [tool] Made by AI: attest AI authorship of HEAD (an Authored-by-ai trailer).
@@ -77,6 +79,33 @@ In CI, omit `<range>` — on a pull-request build `madeby check` **auto-scopes t
77
79
  commits** from the CI environment (GitHub today; other hosts via their adapter). Pass an explicit
78
80
  `<range>` to override.
79
81
 
82
+ ### `madeby coverage` — the authorship scoreboard
83
+
84
+ `check` is the gate; `coverage` is the picture. Run it in any repo to see how much of your
85
+ history's authorship is **on the record** — no policy, no config:
86
+
87
+ ```sh
88
+ madeby coverage # scores recent history
89
+ madeby coverage origin/main..HEAD # or a range
90
+ ```
91
+
92
+ ```
93
+ authorship coverage — last 500 commits (estimate · disclosure, not detection)
94
+ known 62% ████████████░░░░░░░░ disclosed — on the record
95
+ unknown 38% ███████░░░░░░░░░░░░░ no disclosure — origin unknown, never guessed
96
+
97
+ of the 310 known: human 41% · with_ai 38% · ai 21%
98
+ trust: asserted 88% · verified 12% — self-reported; sign commits to verify
99
+
100
+ → 38% unknown. Require disclosure going forward: madeby init --required
101
+ ```
102
+
103
+ It is a **labeled estimate**, honest by construction: an undisclosed commit is **unknown**, never
104
+ inferred as human — disclosure, not detection. The human/with_ai/ai mix is reported *of the known*,
105
+ never of the whole repo, and the asserted-vs-verified split shows how much is signed. `--json` emits
106
+ counts only (percentages are derived at the edge, never stored). Use it to see your gap, then
107
+ `madeby init --required` to close it going forward.
108
+
80
109
  ### `madeby me` — affirm human authorship
81
110
 
82
111
  Disclosure runs both ways: if you wrote code yourself, affirm it so your work is *disclosed-human*,
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,9 +1991,66 @@ function whoCommand(args) {
1879
1991
  }
1880
1992
  }
1881
1993
 
1994
+ // src/commands/coverage.ts
1995
+ import { existsSync as existsSync4, readFileSync as readFileSync6 } from "node:fs";
1996
+ import { join as join5 } from "node:path";
1997
+ function policyIsRequired(root) {
1998
+ const p = join5(root, ".madeby", "policy.json");
1999
+ if (!existsSync4(p)) return false;
2000
+ try {
2001
+ return parseDisclosurePolicy(JSON.parse(readFileSync6(p, "utf8"))).mode === "required";
2002
+ } catch {
2003
+ return false;
2004
+ }
2005
+ }
2006
+ var pct = (n, d) => d > 0 ? Math.round(n / d * 100) : 0;
2007
+ var bar = (p) => "\u2588".repeat(Math.round(p / 5)) + "\u2591".repeat(20 - Math.round(p / 5));
2008
+ function render(r, required) {
2009
+ const knownPct = pct(r.known, r.total);
2010
+ const unknownPct = 100 - knownPct;
2011
+ console.log(`authorship coverage \u2014 ${r.scope} (estimate \xB7 disclosure, not detection)`);
2012
+ if (r.total === 0) {
2013
+ console.log(" (no commits to score)");
2014
+ return;
2015
+ }
2016
+ console.log(` known ${String(knownPct).padStart(3)}% ${bar(knownPct)} disclosed \u2014 on the record`);
2017
+ console.log(` unknown ${String(unknownPct).padStart(3)}% ${bar(unknownPct)} no disclosure \u2014 origin unknown, never guessed`);
2018
+ if (r.known > 0) {
2019
+ const c = r.byCategory;
2020
+ const parts = [`human ${pct(c.human, r.known)}%`, `with_ai ${pct(c.with_ai, r.known)}%`, `ai ${pct(c.ai, r.known)}%`];
2021
+ if (c.bot > 0) parts.push(`bot ${pct(c.bot, r.known)}%`);
2022
+ console.log(`
2023
+ of the ${r.known} known: ${parts.join(" \xB7 ")}`);
2024
+ 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`);
2025
+ }
2026
+ if (r.unknown > 0) {
2027
+ console.log(
2028
+ required ? `
2029
+ \u2192 ${unknownPct}% unknown. Disclosure is already required on pull requests \u2014 the unknown is history the gate didn\u2019t cover (e.g. from before it was on).` : `
2030
+ \u2192 ${unknownPct}% unknown. Require disclosure going forward: madeby init --required`
2031
+ );
2032
+ }
2033
+ }
2034
+ function coverageCommand(args) {
2035
+ const json = args.includes("--json");
2036
+ const range = args.find((a) => !a.startsWith("--"));
2037
+ const root = repoRoot();
2038
+ if (!root) {
2039
+ console.error("madeby coverage: not a git repository.");
2040
+ process.exit(2);
2041
+ return;
2042
+ }
2043
+ const r = repoCoverage(root, { range });
2044
+ if (json) {
2045
+ console.log(JSON.stringify(r, null, 2));
2046
+ return;
2047
+ }
2048
+ render(r, policyIsRequired(root));
2049
+ }
2050
+
1882
2051
  // src/init-repo.ts
1883
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "node:fs";
1884
- import { dirname as dirname2, join as join5 } from "node:path";
2052
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "node:fs";
2053
+ import { dirname as dirname2, join as join6 } from "node:path";
1885
2054
  var ACO_VERSION = 1;
1886
2055
  var ACO_OPEN = `<!-- madeby:aco v${ACO_VERSION} -->`;
1887
2056
  var ACO_CLOSE = `<!-- /madeby:aco -->`;
@@ -1932,7 +2101,7 @@ function replaceAcoBlock(content, loc) {
1932
2101
  return content.slice(0, loc.start) + ACO_BLOCK + content.slice(loc.end);
1933
2102
  }
1934
2103
  function writeIfAbsent(abs, rel, content, created, skipped) {
1935
- if (existsSync4(abs)) {
2104
+ if (existsSync5(abs)) {
1936
2105
  skipped.push(rel);
1937
2106
  return;
1938
2107
  }
@@ -1940,15 +2109,39 @@ function writeIfAbsent(abs, rel, content, created, skipped) {
1940
2109
  writeFileSync2(abs, content);
1941
2110
  created.push(rel);
1942
2111
  }
2112
+ function ensurePolicy(abs, rel, mode, res) {
2113
+ if (!existsSync5(abs)) {
2114
+ mkdirSync2(dirname2(abs), { recursive: true });
2115
+ writeFileSync2(abs, JSON.stringify({ version: 0, mode }, null, 2) + "\n");
2116
+ res.created.push(rel);
2117
+ return { mode, malformed: false };
2118
+ }
2119
+ let raw;
2120
+ try {
2121
+ raw = JSON.parse(readFileSync7(abs, "utf8"));
2122
+ } catch {
2123
+ res.skipped.push(rel);
2124
+ return { mode: "advisory", malformed: true };
2125
+ }
2126
+ const isObject = !!raw && typeof raw === "object" && !Array.isArray(raw);
2127
+ const current = parseDisclosurePolicy(raw).mode;
2128
+ if (mode === "required" && current !== "required" && isObject) {
2129
+ writeFileSync2(abs, JSON.stringify({ ...raw, mode: "required" }, null, 2) + "\n");
2130
+ res.raised.push(rel);
2131
+ return { mode: "required", malformed: false };
2132
+ }
2133
+ res.skipped.push(rel);
2134
+ return { mode: current, malformed: !isObject };
2135
+ }
1943
2136
  function ensureAco(abs, rel, res, opts) {
1944
- if (!existsSync4(abs)) {
2137
+ if (!existsSync5(abs)) {
1945
2138
  if (!opts.createIfAbsent) return;
1946
2139
  mkdirSync2(dirname2(abs), { recursive: true });
1947
2140
  writeFileSync2(abs, AGENTS_CONVENTION);
1948
2141
  res.created.push(rel);
1949
2142
  return;
1950
2143
  }
1951
- const content = readFileSync6(abs, "utf8");
2144
+ const content = readFileSync7(abs, "utf8");
1952
2145
  const status = acoStatus(content);
1953
2146
  if (status === "absent") {
1954
2147
  const sep = content.endsWith("\n") ? "" : "\n";
@@ -1974,14 +2167,21 @@ function initRepo(root, opts = {}) {
1974
2167
  const merged = [];
1975
2168
  const updated = [];
1976
2169
  const outdated = [];
1977
- const res = { created, skipped, merged, updated, outdated };
1978
- writeIfAbsent(join5(root, ".madeby", "policy.json"), ".madeby/policy.json", JSON.stringify({ version: 0, mode }, null, 2) + "\n", created, skipped);
1979
- ensureAco(join5(root, "AGENTS.md"), "AGENTS.md", res, { createIfAbsent: true, update });
1980
- ensureAco(join5(root, "CLAUDE.md"), "CLAUDE.md", res, { createIfAbsent: false, update });
2170
+ const raised = [];
2171
+ const res = { created, skipped, merged, updated, outdated, raised };
2172
+ const policy = ensurePolicy(join6(root, ".madeby", "policy.json"), ".madeby/policy.json", mode, res);
2173
+ ensureAco(join6(root, "AGENTS.md"), "AGENTS.md", res, { createIfAbsent: true, update });
2174
+ ensureAco(join6(root, "CLAUDE.md"), "CLAUDE.md", res, { createIfAbsent: false, update });
1981
2175
  if (host === "github") {
1982
- writeIfAbsent(join5(root, ".github", "workflows", "disclosure.yml"), ".github/workflows/disclosure.yml", GH_WORKFLOW, created, skipped);
2176
+ writeIfAbsent(join6(root, ".github", "workflows", "disclosure.yml"), ".github/workflows/disclosure.yml", GH_WORKFLOW, created, skipped);
1983
2177
  }
1984
2178
  const nextSteps = ["Commit the created files."];
2179
+ if (raised.length > 0) {
2180
+ nextSteps.push('Raised .madeby/policy.json to "required" \u2014 undisclosed commits now fail the check. To make it block merges, also mark the check required on the protected branch (see madeby://guide/enforce).');
2181
+ }
2182
+ if (policy.malformed) {
2183
+ nextSteps.push('.madeby/policy.json is not a valid policy, so the check treats it as advisory. It was left untouched \u2014 fix it (e.g. { "version": 0, "mode": "required" }) to enforce.');
2184
+ }
1985
2185
  if (created.includes("AGENTS.md")) {
1986
2186
  nextSteps.push("An AGENTS.md ACO convention was added \u2014 agents that read it disclose authorship (e.g. 'npx madeby@latest ai [tool]'). Tune it to your repo's tools.");
1987
2187
  }
@@ -1999,12 +2199,12 @@ function initRepo(root, opts = {}) {
1999
2199
  } else if (host !== "github") {
2000
2200
  nextSteps.push(`Add a CI step that runs 'npx madeby check' on pull requests for ${host}.`);
2001
2201
  }
2002
- if (mode !== "required") {
2003
- nextSteps.push(`When ready to enforce: set mode to "required", then make the check required on the protected branch \u2014 the host's control plane (GitHub: gh api branch protection; Azure: a build-validation policy). See the MCP resource madeby://guide/enforce for exact commands.`);
2202
+ if (policy.mode !== "required") {
2203
+ nextSteps.push(`When ready to enforce: run 'madeby init --required' (or set "mode": "required" in .madeby/policy.json), then make the check required on the protected branch \u2014 the host's control plane (GitHub: gh api branch protection; Azure: a build-validation policy). See the MCP resource madeby://guide/enforce for exact commands.`);
2004
2204
  }
2005
2205
  nextSteps.push("Contributors disclose origin with a Co-Authored-By / Generated-by trailer (AI), an Authored-by-human trailer ('madeby me'), an Authored-by-ai trailer ('madeby ai' \u2014 AI authored, committer accountable), a DCO Signed-off-by, or a signed commit. Evidence upgrade: 'madeby ai --witness' records your own AI session's witnessed spans.");
2006
2206
  nextSteps.push("Existing history is accounted for honestly, not fabricated: run 'madeby check' over full history to recognize what's already disclosed; pre-adoption commits stay unknown. See madeby://guide/backfill.");
2007
- return { created, skipped, merged, updated, outdated, policyMode: mode, host, nextSteps };
2207
+ return { created, skipped, merged, updated, outdated, raised, policyMode: policy.mode, host, nextSteps };
2008
2208
  }
2009
2209
  var GH_WORKFLOW = `name: Disclosure
2010
2210
  on: pull_request
@@ -2034,6 +2234,7 @@ function initCommand(args) {
2034
2234
  for (const f of r.created) console.log(` + ${f}`);
2035
2235
  for (const f of r.merged) console.log(` ~ ${f} (merged in the ACO rule)`);
2036
2236
  for (const f of r.updated) console.log(` \u2191 ${f} (ACO convention refreshed to the current version)`);
2237
+ for (const f of r.raised) console.log(` \u2191 ${f} (mode raised to required)`);
2037
2238
  for (const f of r.outdated) console.log(` ! ${f} (ACO convention OUTDATED \u2014 run 'madeby init --update')`);
2038
2239
  for (const f of r.skipped) console.log(` \xB7 ${f} (exists \u2014 left as-is)`);
2039
2240
  console.log(`
@@ -2046,7 +2247,7 @@ madeby init \u2014 policy: ${r.policyMode}, host: ${r.host}`);
2046
2247
  import { execFileSync as execFileSync6 } from "node:child_process";
2047
2248
  import { createInterface } from "node:readline";
2048
2249
  var DEFAULT_PROTOCOL = "2024-11-05";
2049
- var SERVER_INFO = { name: "madeby", version: "0.3.5" };
2250
+ var SERVER_INFO = { name: "madeby", version: "0.3.7" };
2050
2251
  function rootFor(path) {
2051
2252
  const cwd = path && path.length ? path : process.cwd();
2052
2253
  return execFileSync6("git", ["-C", cwd, "-c", "core.quotePath=false", "rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
@@ -2061,7 +2262,7 @@ var repoInput = {
2061
2262
  var TOOLS = [
2062
2263
  {
2063
2264
  name: "init",
2064
- description: "Align a repository with MadeBy in one call: create .madeby/policy.json (advisory by default \u2014 reports, never fails) and the CI disclosure check for the host (github default; azure-devops guided). Idempotent \u2014 never clobbers an existing policy or workflow. Returns the files created/skipped and the recommended next steps. Read the resources: madeby://guide/align (full workflow), madeby://guide/enforce (make the check required \u2014 the host's control plane, exact commands), madeby://guide/backfill (account for existing history honestly \u2014 never fabricate). The ACO convention block is versioned: the result flags an `outdated` AGENTS.md/CLAUDE.md (drift); call again with update:true to refresh just that block in place \u2014 how an agent keeps the convention current.",
2265
+ description: 'Align a repository with MadeBy in one call: create .madeby/policy.json (advisory by default \u2014 reports, never fails) and the CI disclosure check for the host (github default; azure-devops guided). Idempotent \u2014 never clobbers an existing policy or workflow; the one change it makes is mode: "required" raising an existing advisory policy to required (it never lowers one). Returns the files created/skipped and the recommended next steps. Read the resources: madeby://guide/align (full workflow), madeby://guide/enforce (make the check required \u2014 the host\'s control plane, exact commands), madeby://guide/backfill (account for existing history honestly \u2014 never fabricate). The ACO convention block is versioned: the result flags an `outdated` AGENTS.md/CLAUDE.md (drift); call again with update:true to refresh just that block in place \u2014 how an agent keeps the convention current.',
2065
2266
  inputSchema: {
2066
2267
  type: "object",
2067
2268
  properties: {
@@ -2079,6 +2280,12 @@ var TOOLS = [
2079
2280
  inputSchema: repoInput,
2080
2281
  handler: (args) => evaluateRepoDisclosure(rootFor(args.path), { range: args.range })
2081
2282
  },
2283
+ {
2284
+ name: "coverage",
2285
+ 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.",
2286
+ inputSchema: repoInput,
2287
+ handler: (args) => repoCoverage(rootFor(args.path), { range: args.range })
2288
+ },
2082
2289
  {
2083
2290
  name: "who",
2084
2291
  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'.",
@@ -2139,7 +2346,8 @@ guesses whether code is AI. To align a repo:
2139
2346
 
2140
2347
  1. **Assess** \u2014 call \`check\` (or \`who\` with no path for a repo-wide map) to see current coverage.
2141
2348
  2. **Set up** \u2014 call \`init\`: it writes \`.madeby/policy.json\` (advisory) and the CI disclosure check.
2142
- Idempotent; it never clobbers an existing policy or workflow. Then commit the created files.
2349
+ Idempotent; it never clobbers an existing policy or workflow (\`mode: "required"\` only ever raises an
2350
+ advisory policy to required). Then commit the created files.
2143
2351
  3. **Disclose going forward** \u2014 contributors disclose origin with a Co-Authored-By / Generated-by
2144
2352
  trailer (AI), an \`Authored-by-human\` trailer (work they wrote themselves \u2014 the symmetric human
2145
2353
  claim; \`madeby me\` adds it), a DCO Signed-off-by, a signed commit, or \`ai\` (record your own AI
@@ -2342,6 +2550,9 @@ Usage:
2342
2550
  --json for a machine-readable result.
2343
2551
  madeby who [<path>] Made by whom? Read what's on the record about who made this \u2014 one
2344
2552
  (alias: whom) file's origin, or the whole repo with no path. --lines=A-B to scope.
2553
+ madeby coverage [<range>] The scoreboard: how much of your repo's authorship is disclosed (known)
2554
+ vs unknown, the human/with_ai/ai mix, and the asserted/verified split.
2555
+ --json for a machine-readable result.
2345
2556
  madeby ai [<log>] [<ref>] Made by AI: record your AI session's witnessed spans into .madeby/spans.
2346
2557
  <log> defaults to the auto-discovered Claude Code transcript.
2347
2558
  madeby me Made by me: affirm you authored HEAD yourself (an Authored-by-human
@@ -2366,6 +2577,9 @@ switch (cmd) {
2366
2577
  case "whom":
2367
2578
  whoCommand(rest);
2368
2579
  break;
2580
+ case "coverage":
2581
+ coverageCommand(rest);
2582
+ break;
2369
2583
  case "ai":
2370
2584
  aiCommand(rest);
2371
2585
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "madeby",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
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",
@@ -41,24 +41,25 @@
41
41
  "node": ">=20"
42
42
  },
43
43
  "bin": {
44
- "madeby": "./dist/madeby.mjs"
44
+ "madeby": "dist/madeby.mjs"
45
45
  },
46
46
  "files": [
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
+ }