madeby 0.3.4 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/madeby.mjs +264 -67
- package/package.json +12 -11
package/dist/madeby.mjs
CHANGED
|
@@ -37,6 +37,36 @@ function readGitLog(repoPath, limit, range) {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
// ../classify/src/classify.ts
|
|
40
|
+
var AI_PATTERNS = [
|
|
41
|
+
{ re: /claude/i, provider: "anthropic", model: "claude" },
|
|
42
|
+
{ re: /copilot/i, provider: "github-copilot" },
|
|
43
|
+
{ re: /\bcursor(?:agent)?\b/i, provider: "cursor" },
|
|
44
|
+
{ re: /\bdevin\b/i, provider: "cognition" },
|
|
45
|
+
{ re: /gemini/i, provider: "google" },
|
|
46
|
+
{ re: /chatgpt|openai|codex|\bgpt-/i, provider: "openai" },
|
|
47
|
+
{ re: /codeium|windsurf/i, provider: "codeium" },
|
|
48
|
+
{ re: /\baider\b/i, provider: "aider" },
|
|
49
|
+
{ re: /codewhisperer/i, provider: "amazon" },
|
|
50
|
+
{ re: /tabnine/i, provider: "tabnine" }
|
|
51
|
+
];
|
|
52
|
+
function matchAi(text) {
|
|
53
|
+
for (const p of AI_PATTERNS) {
|
|
54
|
+
if (p.re.test(text)) return { provider: p.provider, ...p.model ? { model: p.model } : {}, raw: text.trim() };
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
var GENERIC_AI_TRAILER = /^(?:AI|an? AI|AI (?:assistant|tool|pair)|artificial intelligence)$/i;
|
|
59
|
+
function genericAiDisclosure(who) {
|
|
60
|
+
const w = who.trim();
|
|
61
|
+
return GENERIC_AI_TRAILER.test(w) ? { provider: "unspecified", raw: w } : null;
|
|
62
|
+
}
|
|
63
|
+
var TRAILER_RE = /^[ \t]*(?:Co-authored-by|Generated-by|Assisted-by):[ \t]*(.+)$/gim;
|
|
64
|
+
var AI_AUTHOR_TRAILER = /^[ \t]*Authored-by-ai:[ \t]*(.+)$/gim;
|
|
65
|
+
var HUMAN_ATTEST = /^[ \t]*(?:Authored-by-human|Human-authored(?:-by)?):/im;
|
|
66
|
+
var CONF_WITH_AI = 0.9;
|
|
67
|
+
var CONF_AI = 0.85;
|
|
68
|
+
var CONF_HUMAN = 0.55;
|
|
69
|
+
var CONF_BOT = 0.95;
|
|
40
70
|
var KNOWN_BOTS = /\b(?:dependabot|renovate(?:-bot)?|greenkeeper|snyk-bot|github-actions|semantic-release-bot|stainless-app|allcontributors|mergify|codecov-commenter|pre-commit-ci|imgbot|whitesource-bot|copybara)\b/i;
|
|
41
71
|
function isBotIdentity(name, email) {
|
|
42
72
|
const n = (name ?? "").trim();
|
|
@@ -45,6 +75,60 @@ function isBotIdentity(name, email) {
|
|
|
45
75
|
if (e === "actions@github.com") return true;
|
|
46
76
|
return KNOWN_BOTS.test(n) || KNOWN_BOTS.test(e);
|
|
47
77
|
}
|
|
78
|
+
function classifyCommit(commit) {
|
|
79
|
+
const signals = [];
|
|
80
|
+
const aiContributors = [];
|
|
81
|
+
if (isBotIdentity(commit.authorName, commit.authorEmail)) {
|
|
82
|
+
const botText = `${commit.authorName ?? ""} ${commit.authorEmail ?? ""}`.trim();
|
|
83
|
+
const agentAi = botText ? matchAi(botText) : null;
|
|
84
|
+
if (agentAi) {
|
|
85
|
+
return {
|
|
86
|
+
class: "ai",
|
|
87
|
+
confidence: CONF_AI,
|
|
88
|
+
signals: [`author:${agentAi.provider}`],
|
|
89
|
+
aiContributors: [{ ...agentAi, raw: botText }]
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return { class: "bot", confidence: CONF_BOT, signals: ["author:bot"], aiContributors };
|
|
93
|
+
}
|
|
94
|
+
const aiAuthored = [...commit.message.matchAll(AI_AUTHOR_TRAILER)].map((m) => m[1].trim());
|
|
95
|
+
if (aiAuthored.length > 0 && !HUMAN_ATTEST.test(commit.message)) {
|
|
96
|
+
const who = aiAuthored[0];
|
|
97
|
+
const c = matchAi(who) ?? { provider: "unspecified", raw: who };
|
|
98
|
+
return { class: "ai", confidence: CONF_AI, signals: [`authored-by-ai:${c.provider}`], aiContributors: [c] };
|
|
99
|
+
}
|
|
100
|
+
const trailers = [...commit.message.matchAll(TRAILER_RE)].map((m) => m[1].trim());
|
|
101
|
+
let humanCoAuthors = 0;
|
|
102
|
+
for (const who of trailers) {
|
|
103
|
+
const ai = matchAi(who) ?? genericAiDisclosure(who);
|
|
104
|
+
if (ai) {
|
|
105
|
+
aiContributors.push(ai);
|
|
106
|
+
signals.push(`co-authored-by:${ai.provider}`);
|
|
107
|
+
} else {
|
|
108
|
+
humanCoAuthors += 1;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
for (const who of aiAuthored) {
|
|
112
|
+
const ai = matchAi(who) ?? { provider: "unspecified", raw: who };
|
|
113
|
+
aiContributors.push(ai);
|
|
114
|
+
signals.push(`authored-by-ai:${ai.provider}`);
|
|
115
|
+
}
|
|
116
|
+
const authorText = `${commit.authorName ?? ""} ${commit.authorEmail ?? ""}`.trim();
|
|
117
|
+
const authorAi = authorText ? matchAi(authorText) : null;
|
|
118
|
+
if (authorAi) {
|
|
119
|
+
aiContributors.push({ ...authorAi, raw: authorText });
|
|
120
|
+
signals.push(`author:${authorAi.provider}`);
|
|
121
|
+
}
|
|
122
|
+
const hasAi = aiContributors.length > 0;
|
|
123
|
+
const humanPresent = authorAi === null || humanCoAuthors > 0;
|
|
124
|
+
if (!hasAi) {
|
|
125
|
+
return { class: "human", confidence: CONF_HUMAN, signals, aiContributors };
|
|
126
|
+
}
|
|
127
|
+
if (authorAi !== null && !humanPresent) {
|
|
128
|
+
return { class: "ai", confidence: CONF_AI, signals, aiContributors };
|
|
129
|
+
}
|
|
130
|
+
return { class: "with_ai", confidence: CONF_WITH_AI, signals, aiContributors };
|
|
131
|
+
}
|
|
48
132
|
|
|
49
133
|
// ../classify/src/benchmark/benchmark-v0.json
|
|
50
134
|
var benchmark_v0_default = {
|
|
@@ -1091,7 +1175,7 @@ function provenanceMap(root, opts = {}) {
|
|
|
1091
1175
|
}
|
|
1092
1176
|
const commits = readDisclosures(root, { limit: opts.commitLimit ?? 500 });
|
|
1093
1177
|
const disclosed = commits.filter((c) => c.disclosures.length > 0).length;
|
|
1094
|
-
const
|
|
1178
|
+
const pct2 = commits.length ? Math.round(disclosed / commits.length * 100) : 0;
|
|
1095
1179
|
const scope = prefix ?? "repo";
|
|
1096
1180
|
const allModels = [...new Set(witnessed.flatMap((w) => w.models))];
|
|
1097
1181
|
let aiDefault;
|
|
@@ -1110,11 +1194,39 @@ function provenanceMap(root, opts = {}) {
|
|
|
1110
1194
|
witnessed,
|
|
1111
1195
|
commitCoverage: { disclosed, total: commits.length },
|
|
1112
1196
|
aiDisclosureDefault: aiDefault,
|
|
1113
|
-
summary: `${scope}: ${trackedFiles} tracked file${trackedFiles === 1 ? "" : "s"}; ${witnessed.length} with witnessed AI provenance${allModels.length ? ` (${allModels.join(", ")})` : ""}; ${
|
|
1197
|
+
summary: `${scope}: ${trackedFiles} tracked file${trackedFiles === 1 ? "" : "s"}; ${witnessed.length} with witnessed AI provenance${allModels.length ? ` (${allModels.join(", ")})` : ""}; ${pct2}% of the last ${commits.length} commits disclose origin.`,
|
|
1114
1198
|
note: "Witnessed spans are precise (region-level, model-named); commit coverage is commit-level, not per-file authorship. Files without a witnessed span have unknown origin \u2014 disclosure, not detection: we report what's disclosed and never infer origin we can't see. Resolution is a configurable convention (ARCH \xA712)."
|
|
1115
1199
|
};
|
|
1116
1200
|
}
|
|
1117
1201
|
|
|
1202
|
+
// ../analyzer/src/coverage.ts
|
|
1203
|
+
function repoCoverage(root, opts = {}) {
|
|
1204
|
+
const commits = readGitLog(root, opts.range ? void 0 : opts.limit ?? 500, opts.range);
|
|
1205
|
+
const byCategory = { human: 0, with_ai: 0, ai: 0, bot: 0 };
|
|
1206
|
+
const byTier = { asserted: 0, verified: 0 };
|
|
1207
|
+
let known = 0;
|
|
1208
|
+
for (const c of commits) {
|
|
1209
|
+
const kinds = commitDisclosureKinds({ message: c.message, signed: c.signed });
|
|
1210
|
+
const cls = classifyCommit({ message: c.message, authorName: c.authorName, authorEmail: c.authorEmail, signed: c.signed, sha: c.sha }).class;
|
|
1211
|
+
const category = cls === "human" && !kinds.includes("human-attestation") ? "unknown" : cls;
|
|
1212
|
+
if (category === "unknown") continue;
|
|
1213
|
+
known += 1;
|
|
1214
|
+
byCategory[category] += 1;
|
|
1215
|
+
if (c.signed) byTier.verified += 1;
|
|
1216
|
+
else byTier.asserted += 1;
|
|
1217
|
+
}
|
|
1218
|
+
return {
|
|
1219
|
+
isEstimate: true,
|
|
1220
|
+
methodology: "commit-level authorship disclosure (estimate); undisclosed = unknown, never inferred",
|
|
1221
|
+
scope: opts.range ? `range ${opts.range}` : `last ${commits.length} commits`,
|
|
1222
|
+
total: commits.length,
|
|
1223
|
+
known,
|
|
1224
|
+
unknown: commits.length - known,
|
|
1225
|
+
byCategory,
|
|
1226
|
+
byTier
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1118
1230
|
// ../analyzer/src/record-spans.ts
|
|
1119
1231
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
1120
1232
|
import { existsSync as existsSync3, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync, writeFileSync } from "node:fs";
|
|
@@ -1658,7 +1770,7 @@ function parseWithAi(args) {
|
|
|
1658
1770
|
function git3(root, args) {
|
|
1659
1771
|
return execFileSync5("git", ["-C", root, ...args], { encoding: "utf8" }).trim();
|
|
1660
1772
|
}
|
|
1661
|
-
var
|
|
1773
|
+
var HUMAN_ATTEST2 = /^[ \t]*(?:Authored-by-human|Human-authored(?:-by)?)[ \t]*:/im;
|
|
1662
1774
|
var ASSIST = /^[ \t]*Assisted-by[ \t]*:/im;
|
|
1663
1775
|
var AI_AUTHOR = /^[ \t]*Authored-by-ai[ \t]*:/im;
|
|
1664
1776
|
function attestAiAuthorship(root, opts = {}) {
|
|
@@ -1713,7 +1825,7 @@ function affirmAuthorship(root, opts = {}) {
|
|
|
1713
1825
|
} catch {
|
|
1714
1826
|
return { ...base, reason: "no-commit" };
|
|
1715
1827
|
}
|
|
1716
|
-
const hasHuman =
|
|
1828
|
+
const hasHuman = HUMAN_ATTEST2.test(message);
|
|
1717
1829
|
const hasAssist = ASSIST.test(message);
|
|
1718
1830
|
const trailers = [];
|
|
1719
1831
|
const added = [];
|
|
@@ -1879,61 +1991,101 @@ 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";
|
|
1885
|
-
var
|
|
1886
|
-
var
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
runs-on: ubuntu-latest
|
|
1891
|
-
steps:
|
|
1892
|
-
- uses: actions/checkout@v4
|
|
1893
|
-
with:
|
|
1894
|
-
fetch-depth: 0
|
|
1895
|
-
- uses: MadeByFYI/MadeBy/packages/action@v0.2.0
|
|
1896
|
-
`;
|
|
1897
|
-
var AGENTS_CONVENTION = `# AGENTS.md \u2014 Authorship disclosure convention (ACO)
|
|
1898
|
-
${ACO_SENTINEL}
|
|
1899
|
-
|
|
1900
|
-
This repository asks any AI agent \u2014 or a human working with one \u2014 to **disclose who authored the
|
|
1901
|
-
changes they make**, per **ACO, the Authorship Certificate of Origin**
|
|
1902
|
-
(https://github.com/MadeByFYI/MadeBy/blob/main/ACO.md).
|
|
1903
|
-
|
|
1904
|
-
## The rule
|
|
1905
|
-
|
|
1906
|
-
When you author or modify content here, disclose the authorship **category** on your commit \u2014 one of:
|
|
1907
|
-
|
|
1908
|
-
- \`human\` \u2014 a human authored it, no AI involvement.
|
|
1909
|
-
- \`with_ai\` \u2014 a human authored it with disclosed AI assistance.
|
|
1910
|
-
- \`ai\` \u2014 an AI agent authored it (a human remains accountable).
|
|
1911
|
-
|
|
1912
|
-
Disclose **per commit, honestly** \u2014 the category describes what actually happened for that change.
|
|
1913
|
-
|
|
1914
|
-
## The one-line way (asserted tier \u2014 no keys, no setup)
|
|
1915
|
-
|
|
1916
|
-
After you commit, run the matching command (works with no install via \`npx\`):
|
|
2040
|
+
var ACO_VERSION = 1;
|
|
2041
|
+
var ACO_OPEN = `<!-- madeby:aco v${ACO_VERSION} -->`;
|
|
2042
|
+
var ACO_CLOSE = `<!-- /madeby:aco -->`;
|
|
2043
|
+
var ACO_OPEN_RE = /<!-- madeby:aco(?: v(\d+))? -->/;
|
|
2044
|
+
var ACO_BODY = `## Authorship disclosure (ACO)
|
|
1917
2045
|
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
| \`with_ai\` | \`npx madeby@latest me --with-ai <tool>\` |
|
|
1922
|
-
| \`ai\` | \`npx madeby@latest ai [tool]\` |
|
|
1923
|
-
|
|
1924
|
-
Undisclosed content stays *unknown*, never guessed. See ACO for the standard.
|
|
1925
|
-
`;
|
|
1926
|
-
var ACO_SECTION = `
|
|
1927
|
-
${ACO_SENTINEL}
|
|
1928
|
-
## Authorship disclosure (ACO)
|
|
1929
|
-
|
|
1930
|
-
Disclose who authored your changes, per [ACO](https://github.com/MadeByFYI/MadeBy/blob/main/ACO.md).
|
|
1931
|
-
After committing, run the command for the honest category (no install needed):
|
|
2046
|
+
When you author or modify content here, **disclose who authored it** \u2014 per
|
|
2047
|
+
[ACO, the Authorship Certificate of Origin](https://github.com/MadeByFYI/MadeBy/blob/main/ACO.md).
|
|
2048
|
+
After you commit, run the command for the honest category (no install, no setup):
|
|
1932
2049
|
|
|
1933
2050
|
- **ai** \u2014 an AI authored it (the committer stays accountable): \`npx madeby@latest ai [tool]\`
|
|
1934
2051
|
- **with_ai** \u2014 a human authored it with AI assistance: \`npx madeby@latest me --with-ai [tool]\`
|
|
1935
2052
|
- **human** \u2014 a human authored it: \`npx madeby@latest me\`
|
|
2053
|
+
|
|
2054
|
+
Disclose per commit, honestly. Undisclosed content stays *unknown*, never guessed.
|
|
2055
|
+
`;
|
|
2056
|
+
var ACO_BLOCK = `${ACO_OPEN}
|
|
2057
|
+
${ACO_BODY}${ACO_CLOSE}
|
|
1936
2058
|
`;
|
|
2059
|
+
var AGENTS_CONVENTION = `# AGENTS.md
|
|
2060
|
+
|
|
2061
|
+
${ACO_BLOCK}`;
|
|
2062
|
+
var ACO_SECTION = `
|
|
2063
|
+
${ACO_BLOCK}`;
|
|
2064
|
+
function findAcoBlock(content) {
|
|
2065
|
+
const m = ACO_OPEN_RE.exec(content);
|
|
2066
|
+
if (!m) return null;
|
|
2067
|
+
const start = m.index;
|
|
2068
|
+
const version = m[1] !== void 0 ? Number(m[1]) : null;
|
|
2069
|
+
const closeIdx = content.indexOf(ACO_CLOSE, start);
|
|
2070
|
+
let end;
|
|
2071
|
+
if (closeIdx !== -1) {
|
|
2072
|
+
end = closeIdx + ACO_CLOSE.length;
|
|
2073
|
+
if (content[end] === "\n") end += 1;
|
|
2074
|
+
} else {
|
|
2075
|
+
end = content.length;
|
|
2076
|
+
}
|
|
2077
|
+
return { version, start, end };
|
|
2078
|
+
}
|
|
2079
|
+
function acoStatus(content) {
|
|
2080
|
+
const loc = findAcoBlock(content);
|
|
2081
|
+
if (!loc) return "absent";
|
|
2082
|
+
if (loc.version === null || loc.version < ACO_VERSION) return "outdated";
|
|
2083
|
+
if (loc.version > ACO_VERSION) return "newer";
|
|
2084
|
+
return "current";
|
|
2085
|
+
}
|
|
2086
|
+
function replaceAcoBlock(content, loc) {
|
|
2087
|
+
return content.slice(0, loc.start) + ACO_BLOCK + content.slice(loc.end);
|
|
2088
|
+
}
|
|
1937
2089
|
function writeIfAbsent(abs, rel, content, created, skipped) {
|
|
1938
2090
|
if (existsSync4(abs)) {
|
|
1939
2091
|
skipped.push(rel);
|
|
@@ -1951,25 +2103,36 @@ function ensureAco(abs, rel, res, opts) {
|
|
|
1951
2103
|
res.created.push(rel);
|
|
1952
2104
|
return;
|
|
1953
2105
|
}
|
|
1954
|
-
const
|
|
1955
|
-
|
|
2106
|
+
const content = readFileSync6(abs, "utf8");
|
|
2107
|
+
const status = acoStatus(content);
|
|
2108
|
+
if (status === "absent") {
|
|
2109
|
+
const sep = content.endsWith("\n") ? "" : "\n";
|
|
2110
|
+
writeFileSync2(abs, content + sep + ACO_SECTION);
|
|
2111
|
+
res.merged.push(rel);
|
|
2112
|
+
} else if (status === "outdated") {
|
|
2113
|
+
if (opts.update) {
|
|
2114
|
+
writeFileSync2(abs, replaceAcoBlock(content, findAcoBlock(content)));
|
|
2115
|
+
res.updated.push(rel);
|
|
2116
|
+
} else {
|
|
2117
|
+
res.outdated.push(rel);
|
|
2118
|
+
}
|
|
2119
|
+
} else {
|
|
1956
2120
|
res.skipped.push(rel);
|
|
1957
|
-
return;
|
|
1958
2121
|
}
|
|
1959
|
-
const sep = current.endsWith("\n") ? "\n" : "\n\n";
|
|
1960
|
-
writeFileSync2(abs, current + sep + ACO_SECTION);
|
|
1961
|
-
res.merged.push(rel);
|
|
1962
2122
|
}
|
|
1963
2123
|
function initRepo(root, opts = {}) {
|
|
1964
2124
|
const mode = opts.mode ?? "advisory";
|
|
1965
2125
|
const host = opts.host ?? "github";
|
|
2126
|
+
const update = opts.update ?? false;
|
|
1966
2127
|
const created = [];
|
|
1967
2128
|
const skipped = [];
|
|
1968
2129
|
const merged = [];
|
|
1969
|
-
const
|
|
2130
|
+
const updated = [];
|
|
2131
|
+
const outdated = [];
|
|
2132
|
+
const res = { created, skipped, merged, updated, outdated };
|
|
1970
2133
|
writeIfAbsent(join5(root, ".madeby", "policy.json"), ".madeby/policy.json", JSON.stringify({ version: 0, mode }, null, 2) + "\n", created, skipped);
|
|
1971
|
-
ensureAco(join5(root, "AGENTS.md"), "AGENTS.md", res, { createIfAbsent: true });
|
|
1972
|
-
ensureAco(join5(root, "CLAUDE.md"), "CLAUDE.md", res, { createIfAbsent: false });
|
|
2134
|
+
ensureAco(join5(root, "AGENTS.md"), "AGENTS.md", res, { createIfAbsent: true, update });
|
|
2135
|
+
ensureAco(join5(root, "CLAUDE.md"), "CLAUDE.md", res, { createIfAbsent: false, update });
|
|
1973
2136
|
if (host === "github") {
|
|
1974
2137
|
writeIfAbsent(join5(root, ".github", "workflows", "disclosure.yml"), ".github/workflows/disclosure.yml", GH_WORKFLOW, created, skipped);
|
|
1975
2138
|
}
|
|
@@ -1980,6 +2143,12 @@ function initRepo(root, opts = {}) {
|
|
|
1980
2143
|
if (merged.length > 0) {
|
|
1981
2144
|
nextSteps.push(`Merged the ACO disclosure rule into your existing ${merged.join(" + ")} \u2014 review the appended "Authorship disclosure (ACO)" section and tune it to your repo.`);
|
|
1982
2145
|
}
|
|
2146
|
+
if (updated.length > 0) {
|
|
2147
|
+
nextSteps.push(`Refreshed an outdated ACO convention in ${updated.join(" + ")} to the current version \u2014 review the change before committing.`);
|
|
2148
|
+
}
|
|
2149
|
+
if (outdated.length > 0) {
|
|
2150
|
+
nextSteps.push(`${outdated.join(" + ")} carries an OUTDATED ACO convention \u2014 run 'madeby init --update' to refresh it in place (only the ACO block changes).`);
|
|
2151
|
+
}
|
|
1983
2152
|
if (host === "azure-devops") {
|
|
1984
2153
|
nextSteps.push("Add the Azure Pipelines template (packages/action/azure-pipelines-disclosure.yml) and wire it as a PR build-validation branch policy.");
|
|
1985
2154
|
} else if (host !== "github") {
|
|
@@ -1990,8 +2159,19 @@ function initRepo(root, opts = {}) {
|
|
|
1990
2159
|
}
|
|
1991
2160
|
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.");
|
|
1992
2161
|
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.");
|
|
1993
|
-
return { created, skipped, merged, policyMode: mode, host, nextSteps };
|
|
2162
|
+
return { created, skipped, merged, updated, outdated, policyMode: mode, host, nextSteps };
|
|
1994
2163
|
}
|
|
2164
|
+
var GH_WORKFLOW = `name: Disclosure
|
|
2165
|
+
on: pull_request
|
|
2166
|
+
jobs:
|
|
2167
|
+
madeby:
|
|
2168
|
+
runs-on: ubuntu-latest
|
|
2169
|
+
steps:
|
|
2170
|
+
- uses: actions/checkout@v4
|
|
2171
|
+
with:
|
|
2172
|
+
fetch-depth: 0
|
|
2173
|
+
- uses: MadeByFYI/MadeBy/packages/action@v0.2.0
|
|
2174
|
+
`;
|
|
1995
2175
|
|
|
1996
2176
|
// src/commands/init.ts
|
|
1997
2177
|
function initCommand(args) {
|
|
@@ -2002,11 +2182,14 @@ function initCommand(args) {
|
|
|
2002
2182
|
return;
|
|
2003
2183
|
}
|
|
2004
2184
|
const mode = args.includes("--required") ? "required" : "advisory";
|
|
2185
|
+
const update = args.includes("--update");
|
|
2005
2186
|
const hostIdx = args.indexOf("--host");
|
|
2006
2187
|
const host = hostIdx >= 0 ? args[hostIdx + 1] : void 0;
|
|
2007
|
-
const r = initRepo(root, { mode, host });
|
|
2188
|
+
const r = initRepo(root, { mode, host, update });
|
|
2008
2189
|
for (const f of r.created) console.log(` + ${f}`);
|
|
2009
2190
|
for (const f of r.merged) console.log(` ~ ${f} (merged in the ACO rule)`);
|
|
2191
|
+
for (const f of r.updated) console.log(` \u2191 ${f} (ACO convention refreshed to the current version)`);
|
|
2192
|
+
for (const f of r.outdated) console.log(` ! ${f} (ACO convention OUTDATED \u2014 run 'madeby init --update')`);
|
|
2010
2193
|
for (const f of r.skipped) console.log(` \xB7 ${f} (exists \u2014 left as-is)`);
|
|
2011
2194
|
console.log(`
|
|
2012
2195
|
madeby init \u2014 policy: ${r.policyMode}, host: ${r.host}`);
|
|
@@ -2018,7 +2201,7 @@ madeby init \u2014 policy: ${r.policyMode}, host: ${r.host}`);
|
|
|
2018
2201
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
2019
2202
|
import { createInterface } from "node:readline";
|
|
2020
2203
|
var DEFAULT_PROTOCOL = "2024-11-05";
|
|
2021
|
-
var SERVER_INFO = { name: "madeby", version: "0.3.
|
|
2204
|
+
var SERVER_INFO = { name: "madeby", version: "0.3.6" };
|
|
2022
2205
|
function rootFor(path) {
|
|
2023
2206
|
const cwd = path && path.length ? path : process.cwd();
|
|
2024
2207
|
return execFileSync6("git", ["-C", cwd, "-c", "core.quotePath=false", "rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
|
|
@@ -2033,16 +2216,17 @@ var repoInput = {
|
|
|
2033
2216
|
var TOOLS = [
|
|
2034
2217
|
{
|
|
2035
2218
|
name: "init",
|
|
2036
|
-
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).",
|
|
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.",
|
|
2037
2220
|
inputSchema: {
|
|
2038
2221
|
type: "object",
|
|
2039
2222
|
properties: {
|
|
2040
2223
|
path: { type: "string", description: "path to the repo (default: the server's working directory)" },
|
|
2041
2224
|
mode: { enum: ["advisory", "required"], description: "policy mode to seed (default: advisory)" },
|
|
2042
|
-
host: { type: "string", description: "CI host to wire: github (default) or azure-devops" }
|
|
2225
|
+
host: { type: "string", description: "CI host to wire: github (default) or azure-devops" },
|
|
2226
|
+
update: { type: "boolean", description: "refresh an outdated ACO convention block in place (drift update); default false (report only)" }
|
|
2043
2227
|
}
|
|
2044
2228
|
},
|
|
2045
|
-
handler: (args) => initRepo(rootFor(args.path), { mode: args.mode, host: args.host })
|
|
2229
|
+
handler: (args) => initRepo(rootFor(args.path), { mode: args.mode, host: args.host, update: args.update })
|
|
2046
2230
|
},
|
|
2047
2231
|
{
|
|
2048
2232
|
name: "check",
|
|
@@ -2050,6 +2234,12 @@ var TOOLS = [
|
|
|
2050
2234
|
inputSchema: repoInput,
|
|
2051
2235
|
handler: (args) => evaluateRepoDisclosure(rootFor(args.path), { range: args.range })
|
|
2052
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
|
+
},
|
|
2053
2243
|
{
|
|
2054
2244
|
name: "who",
|
|
2055
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'.",
|
|
@@ -2305,13 +2495,17 @@ function help() {
|
|
|
2305
2495
|
console.log(`madeby \u2014 verifiable content provenance ("made by whom?")
|
|
2306
2496
|
|
|
2307
2497
|
Usage:
|
|
2308
|
-
madeby init Set MadeBy up: write .madeby/policy.json + the
|
|
2309
|
-
|
|
2498
|
+
madeby init Set MadeBy up: write .madeby/policy.json + the ACO AGENTS.md convention
|
|
2499
|
+
+ the CI disclosure check. --update refreshes an outdated ACO block in
|
|
2500
|
+
place; --required to enforce now; --host azure-devops for Azure Pipelines.
|
|
2310
2501
|
madeby check [<range>] The gate: do commits meet .madeby/policy.json? (a verdict \u2014 exit
|
|
2311
2502
|
code gates). <range> e.g. origin/main..HEAD; in CI it's auto-detected.
|
|
2312
2503
|
--json for a machine-readable result.
|
|
2313
2504
|
madeby who [<path>] Made by whom? Read what's on the record about who made this \u2014 one
|
|
2314
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.
|
|
2315
2509
|
madeby ai [<log>] [<ref>] Made by AI: record your AI session's witnessed spans into .madeby/spans.
|
|
2316
2510
|
<log> defaults to the auto-discovered Claude Code transcript.
|
|
2317
2511
|
madeby me Made by me: affirm you authored HEAD yourself (an Authored-by-human
|
|
@@ -2336,6 +2530,9 @@ switch (cmd) {
|
|
|
2336
2530
|
case "whom":
|
|
2337
2531
|
whoCommand(rest);
|
|
2338
2532
|
break;
|
|
2533
|
+
case "coverage":
|
|
2534
|
+
coverageCommand(rest);
|
|
2535
|
+
break;
|
|
2339
2536
|
case "ai":
|
|
2340
2537
|
aiCommand(rest);
|
|
2341
2538
|
break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "madeby",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"mcpName": "io.github.MadeByFYI/madeby",
|
|
5
5
|
"description": "Disclose AI authorship of code. `npx madeby ai` adds an Authored-by-ai git trailer; AI coding agents disclose per the ACO standard (human/with_ai/ai) from an AGENTS.md convention. Disclosure, not detection. Gate PRs with `madeby check`.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -47,18 +47,19 @@
|
|
|
47
47
|
"dist",
|
|
48
48
|
"README.md"
|
|
49
49
|
],
|
|
50
|
-
"devDependencies": {
|
|
51
|
-
"@types/node": "^22.10.0",
|
|
52
|
-
"esbuild": "^0.25.0",
|
|
53
|
-
"typescript": "^5.7.0",
|
|
54
|
-
"vitest": "^4.1.9",
|
|
55
|
-
"@madeby/analyzer": "0.0.0",
|
|
56
|
-
"@madeby/core": "0.0.0"
|
|
57
|
-
},
|
|
58
50
|
"scripts": {
|
|
59
51
|
"build": "esbuild src/madeby.ts --bundle --platform=node --format=esm --target=node20 --outfile=dist/madeby.mjs --banner:js='#!/usr/bin/env node' && chmod +x dist/madeby.mjs",
|
|
60
52
|
"typecheck": "tsc --noEmit",
|
|
61
53
|
"pretest": "pnpm build",
|
|
62
|
-
"test": "vitest run"
|
|
54
|
+
"test": "vitest run",
|
|
55
|
+
"prepublishOnly": "pnpm build"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@madeby/analyzer": "workspace:*",
|
|
59
|
+
"@madeby/core": "workspace:*",
|
|
60
|
+
"@types/node": "^22.10.0",
|
|
61
|
+
"esbuild": "^0.25.0",
|
|
62
|
+
"typescript": "^5.7.0",
|
|
63
|
+
"vitest": "^4.1.9"
|
|
63
64
|
}
|
|
64
|
-
}
|
|
65
|
+
}
|