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.
- package/README.md +29 -0
- package/dist/madeby.mjs +234 -20
- 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
|
|
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,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
|
|
1884
|
-
import { dirname as dirname2, join as
|
|
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 (
|
|
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 (!
|
|
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 =
|
|
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
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
ensureAco(
|
|
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(
|
|
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
|
|
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.
|
|
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:
|
|
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
|
|
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.
|
|
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": "
|
|
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
|
+
}
|