madeby 0.3.6 → 0.3.8

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 +31 -0
  2. package/dist/madeby.mjs +116 -33
  3. package/package.json +2 -2
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,35 @@ 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). A plain merge authors nothing and
107
+ isn't scored; a merge that changes content of its own (a conflict resolution) is, like any other
108
+ commit — chosen by what it changes, never by whether it discloses. Use it to see your gap, then
109
+ `madeby init --required` to close it going forward.
110
+
80
111
  ### `madeby me` — affirm human authorship
81
112
 
82
113
  Disclosure runs both ways: if you wrote code yourself, affirm it so your work is *disclosed-human*,
package/dist/madeby.mjs CHANGED
@@ -4,6 +4,18 @@
4
4
  import { execFileSync } from "node:child_process";
5
5
  var FIELD = "";
6
6
  var RECORD = "";
7
+ var GROUP = "";
8
+ var parseMeta = (rec) => {
9
+ const parts = rec.split(FIELD);
10
+ const sig = parts[3] ?? "N";
11
+ return {
12
+ sha: parts[0] ?? "",
13
+ authorName: parts[1] ?? "",
14
+ authorEmail: parts[2] ?? "",
15
+ signed: sig !== "N" && sig !== "",
16
+ message: parts.slice(4).join(FIELD).trimEnd()
17
+ };
18
+ };
7
19
  function readGitLog(repoPath, limit, range) {
8
20
  try {
9
21
  const args = [
@@ -11,7 +23,7 @@ function readGitLog(repoPath, limit, range) {
11
23
  repoPath,
12
24
  "log",
13
25
  "--no-merges",
14
- // merge commits aren't authored content — exclude them from attribution
26
+ // a merge is not authored content by default; readAuthoredMerges finds those that are
15
27
  ...limit ? ["-n", String(limit)] : [],
16
28
  // %G? is a signature-PRESENCE probe for the Disclosure Score: 'N' = unsigned; any other code
17
29
  // (G/U/E/…) = a signature is present (E = present but unverifiable here — no keyring). We only
@@ -22,19 +34,38 @@ function readGitLog(repoPath, limit, range) {
22
34
  ...range ? [range] : []
23
35
  ];
24
36
  const out = execFileSync("git", args, { encoding: "utf8", maxBuffer: 256 * 1024 * 1024, stdio: ["ignore", "pipe", "ignore"] });
25
- return out.split(RECORD).map((r) => r.replace(/^\s+/, "")).filter((r) => r.length > 0).map((rec) => {
26
- const parts = rec.split(FIELD);
27
- const sha = parts[0] ?? "";
28
- const authorName = parts[1] ?? "";
29
- const authorEmail = parts[2] ?? "";
30
- const sig = parts[3] ?? "N";
31
- const message = parts.slice(4).join(FIELD).trimEnd();
32
- return { sha, authorName, authorEmail, signed: sig !== "N" && sig !== "", message };
33
- });
37
+ return out.split(RECORD).map((r) => r.replace(/^\s+/, "")).filter((r) => r.length > 0).map(parseMeta);
34
38
  } catch {
35
39
  return [];
36
40
  }
37
41
  }
42
+ function readAuthoredMerges(repoPath, opts = {}) {
43
+ try {
44
+ const args = [
45
+ "-C",
46
+ repoPath,
47
+ "log",
48
+ "--merges",
49
+ "--cc",
50
+ "--name-only",
51
+ `--format=${RECORD}%H${FIELD}%an${FIELD}%ae${FIELD}%G?${FIELD}%B${GROUP}`,
52
+ ...opts.since !== void 0 && !opts.range ? [`--since=@${opts.since}`] : [],
53
+ ...opts.range ? [opts.range] : []
54
+ ];
55
+ const out = execFileSync("git", args, { encoding: "utf8", maxBuffer: 256 * 1024 * 1024, stdio: ["ignore", "pipe", "ignore"] });
56
+ return out.split(RECORD).filter((r) => r.includes(GROUP)).filter((r) => r.slice(r.lastIndexOf(GROUP) + 1).trim().length > 0).map((r) => parseMeta(r.slice(0, r.lastIndexOf(GROUP))));
57
+ } catch {
58
+ return [];
59
+ }
60
+ }
61
+ function commitTime(repoPath, sha) {
62
+ try {
63
+ const t = Number(execFileSync("git", ["-C", repoPath, "log", "-1", "--format=%ct", sha], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim());
64
+ return Number.isFinite(t) && t > 0 ? t : void 0;
65
+ } catch {
66
+ return void 0;
67
+ }
68
+ }
38
69
 
39
70
  // ../classify/src/classify.ts
40
71
  var AI_PATTERNS = [
@@ -1201,7 +1232,12 @@ function provenanceMap(root, opts = {}) {
1201
1232
 
1202
1233
  // ../analyzer/src/coverage.ts
1203
1234
  function repoCoverage(root, opts = {}) {
1204
- const commits = readGitLog(root, opts.range ? void 0 : opts.limit ?? 500, opts.range);
1235
+ const limit = opts.range ? void 0 : opts.limit ?? 500;
1236
+ const authored = readGitLog(root, limit, opts.range);
1237
+ const oldest = authored.at(-1)?.sha;
1238
+ const since = limit && oldest && authored.length >= limit ? commitTime(root, oldest) : void 0;
1239
+ const merges = authored.length > 0 && (!limit || authored.length < limit || since !== void 0) ? readAuthoredMerges(root, { range: opts.range, since }) : [];
1240
+ const commits = [...authored, ...merges];
1205
1241
  const byCategory = { human: 0, with_ai: 0, ai: 0, bot: 0 };
1206
1242
  const byTier = { asserted: 0, verified: 0 };
1207
1243
  let known = 0;
@@ -1217,8 +1253,8 @@ function repoCoverage(root, opts = {}) {
1217
1253
  }
1218
1254
  return {
1219
1255
  isEstimate: true,
1220
- methodology: "commit-level authorship disclosure (estimate); undisclosed = unknown, never inferred",
1221
- scope: opts.range ? `range ${opts.range}` : `last ${commits.length} commits`,
1256
+ methodology: "commit-level authorship disclosure (estimate); undisclosed = unknown, never inferred; merges scored only when they change content of their own",
1257
+ scope: (opts.range ? `range ${opts.range}` : `last ${authored.length} commits`) + (merges.length ? ` + ${merges.length} merges with changes of their own` : ""),
1222
1258
  total: commits.length,
1223
1259
  known,
1224
1260
  unknown: commits.length - known,
@@ -1992,9 +2028,20 @@ function whoCommand(args) {
1992
2028
  }
1993
2029
 
1994
2030
  // src/commands/coverage.ts
2031
+ import { existsSync as existsSync4, readFileSync as readFileSync6 } from "node:fs";
2032
+ import { join as join5 } from "node:path";
2033
+ function policyIsRequired(root) {
2034
+ const p = join5(root, ".madeby", "policy.json");
2035
+ if (!existsSync4(p)) return false;
2036
+ try {
2037
+ return parseDisclosurePolicy(JSON.parse(readFileSync6(p, "utf8"))).mode === "required";
2038
+ } catch {
2039
+ return false;
2040
+ }
2041
+ }
1995
2042
  var pct = (n, d) => d > 0 ? Math.round(n / d * 100) : 0;
1996
2043
  var bar = (p) => "\u2588".repeat(Math.round(p / 5)) + "\u2591".repeat(20 - Math.round(p / 5));
1997
- function render(r) {
2044
+ function render(r, required) {
1998
2045
  const knownPct = pct(r.known, r.total);
1999
2046
  const unknownPct = 100 - knownPct;
2000
2047
  console.log(`authorship coverage \u2014 ${r.scope} (estimate \xB7 disclosure, not detection)`);
@@ -2013,8 +2060,11 @@ function render(r) {
2013
2060
  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
2061
  }
2015
2062
  if (r.unknown > 0) {
2016
- console.log(`
2017
- \u2192 ${unknownPct}% unknown. Require disclosure going forward: madeby init --required`);
2063
+ console.log(
2064
+ required ? `
2065
+ \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).` : `
2066
+ \u2192 ${unknownPct}% unknown. Require disclosure going forward: madeby init --required`
2067
+ );
2018
2068
  }
2019
2069
  }
2020
2070
  function coverageCommand(args) {
@@ -2031,12 +2081,12 @@ function coverageCommand(args) {
2031
2081
  console.log(JSON.stringify(r, null, 2));
2032
2082
  return;
2033
2083
  }
2034
- render(r);
2084
+ render(r, policyIsRequired(root));
2035
2085
  }
2036
2086
 
2037
2087
  // src/init-repo.ts
2038
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "node:fs";
2039
- import { dirname as dirname2, join as join5 } from "node:path";
2088
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "node:fs";
2089
+ import { dirname as dirname2, join as join6 } from "node:path";
2040
2090
  var ACO_VERSION = 1;
2041
2091
  var ACO_OPEN = `<!-- madeby:aco v${ACO_VERSION} -->`;
2042
2092
  var ACO_CLOSE = `<!-- /madeby:aco -->`;
@@ -2087,7 +2137,7 @@ function replaceAcoBlock(content, loc) {
2087
2137
  return content.slice(0, loc.start) + ACO_BLOCK + content.slice(loc.end);
2088
2138
  }
2089
2139
  function writeIfAbsent(abs, rel, content, created, skipped) {
2090
- if (existsSync4(abs)) {
2140
+ if (existsSync5(abs)) {
2091
2141
  skipped.push(rel);
2092
2142
  return;
2093
2143
  }
@@ -2095,15 +2145,39 @@ function writeIfAbsent(abs, rel, content, created, skipped) {
2095
2145
  writeFileSync2(abs, content);
2096
2146
  created.push(rel);
2097
2147
  }
2148
+ function ensurePolicy(abs, rel, mode, res) {
2149
+ if (!existsSync5(abs)) {
2150
+ mkdirSync2(dirname2(abs), { recursive: true });
2151
+ writeFileSync2(abs, JSON.stringify({ version: 0, mode }, null, 2) + "\n");
2152
+ res.created.push(rel);
2153
+ return { mode, malformed: false };
2154
+ }
2155
+ let raw;
2156
+ try {
2157
+ raw = JSON.parse(readFileSync7(abs, "utf8"));
2158
+ } catch {
2159
+ res.skipped.push(rel);
2160
+ return { mode: "advisory", malformed: true };
2161
+ }
2162
+ const isObject = !!raw && typeof raw === "object" && !Array.isArray(raw);
2163
+ const current = parseDisclosurePolicy(raw).mode;
2164
+ if (mode === "required" && current !== "required" && isObject) {
2165
+ writeFileSync2(abs, JSON.stringify({ ...raw, mode: "required" }, null, 2) + "\n");
2166
+ res.raised.push(rel);
2167
+ return { mode: "required", malformed: false };
2168
+ }
2169
+ res.skipped.push(rel);
2170
+ return { mode: current, malformed: !isObject };
2171
+ }
2098
2172
  function ensureAco(abs, rel, res, opts) {
2099
- if (!existsSync4(abs)) {
2173
+ if (!existsSync5(abs)) {
2100
2174
  if (!opts.createIfAbsent) return;
2101
2175
  mkdirSync2(dirname2(abs), { recursive: true });
2102
2176
  writeFileSync2(abs, AGENTS_CONVENTION);
2103
2177
  res.created.push(rel);
2104
2178
  return;
2105
2179
  }
2106
- const content = readFileSync6(abs, "utf8");
2180
+ const content = readFileSync7(abs, "utf8");
2107
2181
  const status = acoStatus(content);
2108
2182
  if (status === "absent") {
2109
2183
  const sep = content.endsWith("\n") ? "" : "\n";
@@ -2129,14 +2203,21 @@ function initRepo(root, opts = {}) {
2129
2203
  const merged = [];
2130
2204
  const updated = [];
2131
2205
  const outdated = [];
2132
- const res = { created, skipped, merged, updated, outdated };
2133
- writeIfAbsent(join5(root, ".madeby", "policy.json"), ".madeby/policy.json", JSON.stringify({ version: 0, mode }, null, 2) + "\n", created, skipped);
2134
- ensureAco(join5(root, "AGENTS.md"), "AGENTS.md", res, { createIfAbsent: true, update });
2135
- ensureAco(join5(root, "CLAUDE.md"), "CLAUDE.md", res, { createIfAbsent: false, update });
2206
+ const raised = [];
2207
+ const res = { created, skipped, merged, updated, outdated, raised };
2208
+ const policy = ensurePolicy(join6(root, ".madeby", "policy.json"), ".madeby/policy.json", mode, res);
2209
+ ensureAco(join6(root, "AGENTS.md"), "AGENTS.md", res, { createIfAbsent: true, update });
2210
+ ensureAco(join6(root, "CLAUDE.md"), "CLAUDE.md", res, { createIfAbsent: false, update });
2136
2211
  if (host === "github") {
2137
- writeIfAbsent(join5(root, ".github", "workflows", "disclosure.yml"), ".github/workflows/disclosure.yml", GH_WORKFLOW, created, skipped);
2212
+ writeIfAbsent(join6(root, ".github", "workflows", "disclosure.yml"), ".github/workflows/disclosure.yml", GH_WORKFLOW, created, skipped);
2138
2213
  }
2139
2214
  const nextSteps = ["Commit the created files."];
2215
+ if (raised.length > 0) {
2216
+ 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).');
2217
+ }
2218
+ if (policy.malformed) {
2219
+ 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.');
2220
+ }
2140
2221
  if (created.includes("AGENTS.md")) {
2141
2222
  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.");
2142
2223
  }
@@ -2154,12 +2235,12 @@ function initRepo(root, opts = {}) {
2154
2235
  } else if (host !== "github") {
2155
2236
  nextSteps.push(`Add a CI step that runs 'npx madeby check' on pull requests for ${host}.`);
2156
2237
  }
2157
- if (mode !== "required") {
2158
- 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.`);
2238
+ if (policy.mode !== "required") {
2239
+ 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.`);
2159
2240
  }
2160
2241
  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.");
2161
2242
  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.");
2162
- return { created, skipped, merged, updated, outdated, policyMode: mode, host, nextSteps };
2243
+ return { created, skipped, merged, updated, outdated, raised, policyMode: policy.mode, host, nextSteps };
2163
2244
  }
2164
2245
  var GH_WORKFLOW = `name: Disclosure
2165
2246
  on: pull_request
@@ -2189,6 +2270,7 @@ function initCommand(args) {
2189
2270
  for (const f of r.created) console.log(` + ${f}`);
2190
2271
  for (const f of r.merged) console.log(` ~ ${f} (merged in the ACO rule)`);
2191
2272
  for (const f of r.updated) console.log(` \u2191 ${f} (ACO convention refreshed to the current version)`);
2273
+ for (const f of r.raised) console.log(` \u2191 ${f} (mode raised to required)`);
2192
2274
  for (const f of r.outdated) console.log(` ! ${f} (ACO convention OUTDATED \u2014 run 'madeby init --update')`);
2193
2275
  for (const f of r.skipped) console.log(` \xB7 ${f} (exists \u2014 left as-is)`);
2194
2276
  console.log(`
@@ -2201,7 +2283,7 @@ madeby init \u2014 policy: ${r.policyMode}, host: ${r.host}`);
2201
2283
  import { execFileSync as execFileSync6 } from "node:child_process";
2202
2284
  import { createInterface } from "node:readline";
2203
2285
  var DEFAULT_PROTOCOL = "2024-11-05";
2204
- var SERVER_INFO = { name: "madeby", version: "0.3.6" };
2286
+ var SERVER_INFO = { name: "madeby", version: "0.3.8" };
2205
2287
  function rootFor(path) {
2206
2288
  const cwd = path && path.length ? path : process.cwd();
2207
2289
  return execFileSync6("git", ["-C", cwd, "-c", "core.quotePath=false", "rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
@@ -2216,7 +2298,7 @@ var repoInput = {
2216
2298
  var TOOLS = [
2217
2299
  {
2218
2300
  name: "init",
2219
- 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.",
2301
+ 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.',
2220
2302
  inputSchema: {
2221
2303
  type: "object",
2222
2304
  properties: {
@@ -2300,7 +2382,8 @@ guesses whether code is AI. To align a repo:
2300
2382
 
2301
2383
  1. **Assess** \u2014 call \`check\` (or \`who\` with no path for a repo-wide map) to see current coverage.
2302
2384
  2. **Set up** \u2014 call \`init\`: it writes \`.madeby/policy.json\` (advisory) and the CI disclosure check.
2303
- Idempotent; it never clobbers an existing policy or workflow. Then commit the created files.
2385
+ Idempotent; it never clobbers an existing policy or workflow (\`mode: "required"\` only ever raises an
2386
+ advisory policy to required). Then commit the created files.
2304
2387
  3. **Disclose going forward** \u2014 contributors disclose origin with a Co-Authored-By / Generated-by
2305
2388
  trailer (AI), an \`Authored-by-human\` trailer (work they wrote themselves \u2014 the symmetric human
2306
2389
  claim; \`madeby me\` adds it), a DCO Signed-off-by, a signed commit, or \`ai\` (record your own AI
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "madeby",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
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,7 +41,7 @@
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",