madeby 0.3.11 → 0.3.13
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 +23 -12
- package/dist/madeby.mjs +189 -37
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -67,22 +67,30 @@ sides of it, and `init`/`check` are the setup and the gate.
|
|
|
67
67
|
### `madeby check` — the disclosure gate
|
|
68
68
|
|
|
69
69
|
Reads `.madeby/policy.json`, recognizes each commit's disclosure signals, and reports coverage.
|
|
70
|
-
A commit **discloses
|
|
70
|
+
A commit **discloses who wrote it** if it carries any of:
|
|
71
71
|
|
|
72
72
|
- an **AI-authorship trailer** — `Co-Authored-By:` / `Generated-by:` / `Assisted-by:` naming an AI
|
|
73
|
-
tool (Claude Code, Copilot, Cursor, aider, … already emit these)
|
|
73
|
+
tool (Claude Code, Copilot, Cursor, aider, … already emit these), or `Authored-by-ai:`;
|
|
74
74
|
- an **affirmative human-authorship trailer** — `Authored-by-human: <you>` (see below);
|
|
75
|
-
- a **DCO `Signed-off-by:`** line;
|
|
76
|
-
- a **commit signature**;
|
|
77
75
|
- a **`madeby ai`** attestation (your own AI session's witnessed spans).
|
|
78
76
|
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
A **DCO `Signed-off-by:`** line or a **commit signature** is recognized too, but on its own it says
|
|
78
|
+
who is accountable for the commit, not who wrote it, so it counts only if the policy's `accept`
|
|
79
|
+
lists it. `check` reports commits that carry only one of those apart from those that carry nothing.
|
|
80
|
+
|
|
81
|
+
Exit codes: **0** = pass · **1** = a `required` policy failed on an undisclosed commit, or couldn't
|
|
82
|
+
read the history (below) · **2** = not a git repository. Under `advisory` it only reports (always exit 0) — so it's safe to add to CI
|
|
81
83
|
before you decide to enforce anything.
|
|
82
84
|
|
|
83
85
|
In CI, omit `<range>` — on a pull-request build `madeby check` **auto-scopes to the PR's own
|
|
84
|
-
commits** from the CI environment (GitHub
|
|
85
|
-
|
|
86
|
+
commits** from the CI environment (GitHub, Azure DevOps and GitLab). Pass an explicit `<range>` to
|
|
87
|
+
override.
|
|
88
|
+
|
|
89
|
+
**The history has to be there.** On a shallow clone, when a range endpoint isn't in the clone (a
|
|
90
|
+
target branch that was never fetched), or on a pull-request build whose range can't be read, `check`
|
|
91
|
+
says so: under `required` it fails rather than score whatever happens to be present, and under
|
|
92
|
+
`advisory` it prints a warning. Check out the full history (`fetch-depth: 0` on GitHub,
|
|
93
|
+
`fetchDepth: 0` on Azure Pipelines); the files `madeby init` writes already do.
|
|
86
94
|
|
|
87
95
|
### `madeby coverage` — the authorship scoreboard
|
|
88
96
|
|
|
@@ -92,6 +100,7 @@ history's authorship is **on the record** — no policy, no config:
|
|
|
92
100
|
```sh
|
|
93
101
|
madeby coverage # scores recent history
|
|
94
102
|
madeby coverage origin/main..HEAD # or a range
|
|
103
|
+
madeby coverage --by author # the same, one row per commit author
|
|
95
104
|
```
|
|
96
105
|
|
|
97
106
|
```
|
|
@@ -115,7 +124,8 @@ inferred as human — disclosure, not detection. The human/with_ai/ai mix is rep
|
|
|
115
124
|
never of the whole repo, and the asserted-vs-verified split shows how much is signed. The agents and
|
|
116
125
|
model versions are listed exactly as the AI commits name them — a commit naming a tool and its model
|
|
117
126
|
(`Assisted-by: Claude Code` + `Co-Authored-By: Claude Opus 5.5`) counts under both, so those rows can
|
|
118
|
-
sum past 100%. `--
|
|
127
|
+
sum past 100%. `--by author` shows one row per commit author (grouped by email): how many of their
|
|
128
|
+
commits disclose, and the mix — who discloses and who never does. `--json` (which includes `byAuthor`) emits
|
|
119
129
|
counts only (percentages are derived at the edge, never stored). A plain merge authors nothing and
|
|
120
130
|
isn't scored; a merge that changes content of its own (a conflict resolution) is, like any other
|
|
121
131
|
commit — chosen by what it changes, never by whether it discloses. Use it to see your gap, then
|
|
@@ -151,8 +161,8 @@ Two modes, nothing more:
|
|
|
151
161
|
|
|
152
162
|
- `advisory` — report coverage on each run; never fails. **The default** (and what you get with no file).
|
|
153
163
|
- `required` — fail on any commit that discloses nothing your policy accepts.
|
|
154
|
-
- optional `"accept": ["dco-signoff", "ai-trailer", …]` —
|
|
155
|
-
(omit ⇒ any
|
|
164
|
+
- optional `"accept": ["dco-signoff", "ai-trailer", …]` — set what satisfies the policy
|
|
165
|
+
(omit ⇒ any authorship disclosure counts; a DCO sign-off or signature alone counts only if listed).
|
|
156
166
|
- optional `"recordModel": true` — written by `madeby init --record-model`: the repo asks AI
|
|
157
167
|
disclosures to name the model version as well as the tool. It adds one line to the AGENTS.md rule,
|
|
158
168
|
and `me --with-ai` / `ai` then read the model from the **tool's own record of the session** — Cursor's
|
|
@@ -166,7 +176,8 @@ Two modes, nothing more:
|
|
|
166
176
|
(There's no "off" — that's just not running the check. Advisory is the floor.)
|
|
167
177
|
|
|
168
178
|
Fail-safe by design: a missing or malformed policy degrades to `advisory`. `madeby` never blocks a PR
|
|
169
|
-
on its own error, and never invents a stricter gate than the maintainer wrote.
|
|
179
|
+
on its own error, and never invents a stricter gate than the maintainer wrote. It never passes a
|
|
180
|
+
`required` check it couldn't actually run, either: missing history fails it (see `check` above).
|
|
170
181
|
|
|
171
182
|
## Use it as a PR check
|
|
172
183
|
|
package/dist/madeby.mjs
CHANGED
|
@@ -16,6 +16,26 @@ var parseMeta = (rec) => {
|
|
|
16
16
|
message: parts.slice(4).join(FIELD).trimEnd()
|
|
17
17
|
};
|
|
18
18
|
};
|
|
19
|
+
function historyProblem(repoPath, range) {
|
|
20
|
+
const git4 = (...args) => {
|
|
21
|
+
try {
|
|
22
|
+
return execFileSync("git", ["-C", repoPath, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
if (git4("rev-parse", "--is-shallow-repository") === "true") {
|
|
28
|
+
return "this is a shallow clone, so the history may be cut short. Fetch the full history (Azure Pipelines: fetchDepth: 0 on the checkout step; GitHub Actions: fetch-depth: 0).";
|
|
29
|
+
}
|
|
30
|
+
if (range) {
|
|
31
|
+
for (const end of range.split(/\.{2,3}/).filter(Boolean)) {
|
|
32
|
+
if (git4("rev-parse", "--verify", "--quiet", `${end}^{commit}`) === null) {
|
|
33
|
+
return `the range ${range} can't be read: ${end} isn't in this clone. Fetch it (full history), or pass a range that exists.`;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
19
39
|
function readGitLog(repoPath, limit, range) {
|
|
20
40
|
try {
|
|
21
41
|
const args = [
|
|
@@ -873,6 +893,27 @@ var conformance_v0_default = {
|
|
|
873
893
|
var CONFORMANCE_VERSION = conformance_v0_default.version;
|
|
874
894
|
|
|
875
895
|
// ../core/src/disclosure.ts
|
|
896
|
+
var DISCLOSURE_KINDS = {
|
|
897
|
+
"ai-trailer": { label: "AI-authorship trailer", nativeness: "external", ceiling: "asserted", parser: "recognized", authorship: true },
|
|
898
|
+
// The AFFIRMATIVE human-authorship claim — the symmetric counterpart of the AI trailer, NOT a
|
|
899
|
+
// separate mechanism: same recognizer, same ladder. A bare trailer is `asserted` (identical to the
|
|
900
|
+
// AI trailer); sign it → verified, swear it → sworn — the signature authenticates WHO is claiming
|
|
901
|
+
// (invariant #10), equally for human or AI disclosure. The one genuine asymmetry is EVIDENCE vs.
|
|
902
|
+
// tier: AI authorship can leave a re-checkable ARTIFACT (a session log matched to the code,
|
|
903
|
+
// `prove`); human authorship is the author's firsthand TESTIMONY — real, but not independently
|
|
904
|
+
// re-checkable, so to a third party it lands as an attestation (asserted → signed → sworn testimony).
|
|
905
|
+
"human-attestation": { label: "human-authorship attestation", nativeness: "madeby", ceiling: "asserted", parser: "recognized", authorship: true },
|
|
906
|
+
"dco-signoff": { label: "DCO Signed-off-by", nativeness: "external", ceiling: "asserted", parser: "recognized", authorship: false },
|
|
907
|
+
"commit-signature": { label: "commit signature", nativeness: "external", ceiling: "verified", parser: "recognized", authorship: false },
|
|
908
|
+
"ai-tool-config": { label: "committed AI-tool config", nativeness: "external", ceiling: "asserted", parser: "recognized", authorship: false },
|
|
909
|
+
"spdx-license": { label: "SPDX license metadata", nativeness: "external", ceiling: "asserted", parser: "recognized", authorship: false },
|
|
910
|
+
"spdx-ai-disclosure": { label: "SPDX-AI-Disclosure tag", nativeness: "external", ceiling: "asserted", parser: "recognized", authorship: true },
|
|
911
|
+
reuse: { label: "REUSE licensing", nativeness: "external", ceiling: "asserted", parser: "planned", authorship: false },
|
|
912
|
+
"in-toto": { label: "in-toto attestation", nativeness: "external", ceiling: "verified", parser: "planned", authorship: false },
|
|
913
|
+
slsa: { label: "SLSA provenance", nativeness: "external", ceiling: "verified", parser: "planned", authorship: false },
|
|
914
|
+
"madeby-manifest": { label: "MadeBy witnessed manifest", nativeness: "madeby", ceiling: "asserted", parser: "recognized", authorship: true },
|
|
915
|
+
"madeby-declaration": { label: "MadeBy self-hosted declaration", nativeness: "madeby", ceiling: "sworn", parser: "recognized", authorship: true }
|
|
916
|
+
};
|
|
876
917
|
var AI_TRAILER_RE = /^[ \t]*(?:Co-authored-by|Generated-by|Assisted-by)[ \t]*:[ \t]*(.+?)[ \t]*$/gim;
|
|
877
918
|
var AI_MARKER_RE = /\b(?:claude|copilot|cursor(?:agent)?|devin|gemini|chatgpt|openai|codex|gpt-|codeium|windsurf|aider|codewhisperer|tabnine)\b/i;
|
|
878
919
|
function hasAiTrailer(message) {
|
|
@@ -947,18 +988,22 @@ function parseDisclosurePolicy(input) {
|
|
|
947
988
|
...o.recordModel === true ? { recordModel: true } : {}
|
|
948
989
|
};
|
|
949
990
|
}
|
|
991
|
+
function acceptsKind(policy, kind) {
|
|
992
|
+
return policy.accept ? policy.accept.includes(kind) : DISCLOSURE_KINDS[kind]?.authorship === true;
|
|
993
|
+
}
|
|
950
994
|
function evaluateDisclosurePolicy(policy, commits) {
|
|
951
995
|
const accept = policy.accept;
|
|
952
|
-
const isAccepted = (k) => !accept || accept.includes(k);
|
|
953
996
|
const verdicts = commits.map((c) => {
|
|
954
|
-
const satisfiedBy = c.kinds.filter(
|
|
955
|
-
|
|
997
|
+
const satisfiedBy = c.kinds.filter((k) => acceptsKind(policy, k));
|
|
998
|
+
const notAccepted = c.kinds.filter((k) => !acceptsKind(policy, k));
|
|
999
|
+
return { ref: c.ref, compliant: satisfiedBy.length > 0, satisfiedBy, notAccepted };
|
|
956
1000
|
});
|
|
957
1001
|
const nonCompliant = verdicts.filter((v) => !v.compliant);
|
|
958
1002
|
const compliant = verdicts.length - nonCompliant.length;
|
|
959
1003
|
const pass = policy.mode !== "required" || nonCompliant.length === 0;
|
|
960
|
-
const wanted = accept ? accept.join(", ") : "any
|
|
961
|
-
const
|
|
1004
|
+
const wanted = accept ? accept.join(", ") : "any authorship disclosure";
|
|
1005
|
+
const otherOnly = nonCompliant.filter((v) => v.notAccepted.length > 0).length;
|
|
1006
|
+
const summary = `${compliant}/${verdicts.length} commits disclose origin (${policy.mode}; accepts: ${wanted}).` + (nonCompliant.length ? ` ${nonCompliant.length} undisclosed` + (otherOnly ? ` (${otherOnly} carry only a sign-off or signature, which don't say who wrote the code).` : ".") : "");
|
|
962
1007
|
return { mode: policy.mode, pass, total: verdicts.length, compliant, nonCompliant, summary };
|
|
963
1008
|
}
|
|
964
1009
|
|
|
@@ -977,6 +1022,10 @@ function detectCiRange(env = process.env) {
|
|
|
977
1022
|
}
|
|
978
1023
|
return null;
|
|
979
1024
|
}
|
|
1025
|
+
function detectCiPullRequest(env = process.env) {
|
|
1026
|
+
for (const adapter of registry.values()) if (adapter.ciPullRequest?.(env)) return adapter.id;
|
|
1027
|
+
return null;
|
|
1028
|
+
}
|
|
980
1029
|
|
|
981
1030
|
// ../analyzer/src/host/github.ts
|
|
982
1031
|
var NOREPLY = /^(?:(\d+)\+)?([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))@users\.noreply\.github\.com$/i;
|
|
@@ -996,8 +1045,11 @@ var githubAdapter = {
|
|
|
996
1045
|
// On a `pull_request` event GitHub sets GITHUB_EVENT_NAME + GITHUB_EVENT_PATH (a JSON payload with
|
|
997
1046
|
// the PR's base/head SHAs). Reading base..head here replaces the bash range computation the Action
|
|
998
1047
|
// used to carry — the range logic now lives once, in the adapter. Needs the PR history present
|
|
999
|
-
// (the quickstart's `fetch-depth: 0`). Any absence/parse failure → null (
|
|
1000
|
-
//
|
|
1048
|
+
// (the quickstart's `fetch-depth: 0`). Any absence/parse failure → null (`check` reports the
|
|
1049
|
+
// unreadable PR build, see ciPullRequest); never throws.
|
|
1050
|
+
ciPullRequest(env) {
|
|
1051
|
+
return env.GITHUB_EVENT_NAME === "pull_request" || env.GITHUB_EVENT_NAME === "pull_request_target";
|
|
1052
|
+
},
|
|
1001
1053
|
ciRange(env) {
|
|
1002
1054
|
const event = env.GITHUB_EVENT_NAME;
|
|
1003
1055
|
if (event !== "pull_request" && event !== "pull_request_target") return null;
|
|
@@ -1266,11 +1318,18 @@ function repoCoverage(root, opts = {}) {
|
|
|
1266
1318
|
const byTier = { asserted: 0, verified: 0 };
|
|
1267
1319
|
let known = 0;
|
|
1268
1320
|
const agents = /* @__PURE__ */ new Map();
|
|
1321
|
+
const authors = /* @__PURE__ */ new Map();
|
|
1269
1322
|
for (const c of commits) {
|
|
1270
1323
|
const kinds = commitDisclosureKinds({ message: c.message, signed: c.signed });
|
|
1271
1324
|
const { class: cls, aiContributors } = classifyCommit({ message: c.message, authorName: c.authorName, authorEmail: c.authorEmail, signed: c.signed, sha: c.sha });
|
|
1272
1325
|
const category = cls === "human" && !kinds.includes("human-attestation") ? "unknown" : cls;
|
|
1326
|
+
const email = (c.authorEmail ?? "").toLowerCase();
|
|
1327
|
+
const author = authors.get(email) ?? { name: c.authorName ?? email, email, commits: 0, known: 0, byCategory: { human: 0, with_ai: 0, ai: 0, bot: 0 } };
|
|
1328
|
+
author.commits += 1;
|
|
1329
|
+
authors.set(email, author);
|
|
1273
1330
|
if (category === "unknown") continue;
|
|
1331
|
+
author.known += 1;
|
|
1332
|
+
author.byCategory[category] += 1;
|
|
1274
1333
|
known += 1;
|
|
1275
1334
|
byCategory[category] += 1;
|
|
1276
1335
|
if (c.signed) byTier.verified += 1;
|
|
@@ -1298,7 +1357,8 @@ function repoCoverage(root, opts = {}) {
|
|
|
1298
1357
|
unknown: commits.length - known,
|
|
1299
1358
|
byCategory,
|
|
1300
1359
|
byTier,
|
|
1301
|
-
byAgent: [...agents.values()].sort((a, b) => b.commits - a.commits || a.name.localeCompare(b.name))
|
|
1360
|
+
byAgent: [...agents.values()].sort((a, b) => b.commits - a.commits || a.name.localeCompare(b.name)),
|
|
1361
|
+
byAuthor: [...authors.values()].sort((a, b) => b.commits - a.commits || a.email.localeCompare(b.email))
|
|
1302
1362
|
};
|
|
1303
1363
|
}
|
|
1304
1364
|
|
|
@@ -1745,7 +1805,11 @@ var azureAdapter = {
|
|
|
1745
1805
|
// these optional precisely so an adapter can omit what its host doesn't offer.
|
|
1746
1806
|
// Azure Pipelines sets these predefined variables on a PR-triggered build. The target branch is
|
|
1747
1807
|
// given as a ref (not a SHA), so the range references origin/<target>; the pipeline template uses
|
|
1748
|
-
// fetchDepth: 0 so that ref + the PR history are present. Any absence → null
|
|
1808
|
+
// fetchDepth: 0 so that ref + the PR history are present. Any absence → null; `check` then reports
|
|
1809
|
+
// the PR build whose range it couldn't read (ciPullRequest) instead of passing on recent history.
|
|
1810
|
+
ciPullRequest(env) {
|
|
1811
|
+
return env.BUILD_REASON === "PullRequest";
|
|
1812
|
+
},
|
|
1749
1813
|
ciRange(env) {
|
|
1750
1814
|
if (env.BUILD_REASON !== "PullRequest") return null;
|
|
1751
1815
|
const targetRef = env.SYSTEM_PULLREQUEST_TARGETBRANCH;
|
|
@@ -1771,7 +1835,10 @@ var gitlabAdapter = {
|
|
|
1771
1835
|
},
|
|
1772
1836
|
// GitLab CI sets these predefined variables on a merge-request pipeline. CI_MERGE_REQUEST_DIFF_BASE_SHA
|
|
1773
1837
|
// is the merge-base of the MR; CI_COMMIT_SHA is the head — so base..head is the MR's own commits.
|
|
1774
|
-
// (Needs the MR pipeline / a deep-enough fetch for those SHAs.) Any absence → null (
|
|
1838
|
+
// (Needs the MR pipeline / a deep-enough fetch for those SHAs.) Any absence → null (reported by `check`).
|
|
1839
|
+
ciPullRequest(env) {
|
|
1840
|
+
return env.CI_PIPELINE_SOURCE === "merge_request_event";
|
|
1841
|
+
},
|
|
1775
1842
|
ciRange(env) {
|
|
1776
1843
|
if (env.CI_PIPELINE_SOURCE !== "merge_request_event") return null;
|
|
1777
1844
|
const base = env.CI_MERGE_REQUEST_DIFF_BASE_SHA;
|
|
@@ -1809,9 +1876,23 @@ function checkCommand(args) {
|
|
|
1809
1876
|
}
|
|
1810
1877
|
const { range, autoHost } = resolveRange(explicit);
|
|
1811
1878
|
const result = evaluateRepoDisclosure(root, { range });
|
|
1879
|
+
const prHost = explicit || autoHost ? null : detectCiPullRequest();
|
|
1880
|
+
const problem = prHost ? `this is a ${prHost} pull-request build, but its commit range couldn't be read from the CI environment. Pass the range explicitly: madeby check <base>..<head>` : historyProblem(root, range);
|
|
1881
|
+
const blocked = problem !== null && result.mode === "required";
|
|
1882
|
+
const pass = result.pass && !blocked;
|
|
1812
1883
|
if (json) {
|
|
1813
|
-
console.log(JSON.stringify({ range: range ?? null, autoHost: autoHost ?? null, ...result }, null, 2));
|
|
1814
|
-
process.exit(
|
|
1884
|
+
console.log(JSON.stringify({ range: range ?? null, autoHost: autoHost ?? null, ...result, pass, ...problem ? { historyProblem: problem } : {} }, null, 2));
|
|
1885
|
+
process.exit(pass ? 0 : 1);
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
if (problem) {
|
|
1889
|
+
const banner = blocked ? "FAILED \u2014 can't check this history" : "WARNING \u2014 this result may be wrong";
|
|
1890
|
+
console.error(`
|
|
1891
|
+
madeby check: ${banner}: ${problem}
|
|
1892
|
+
`);
|
|
1893
|
+
}
|
|
1894
|
+
if (blocked) {
|
|
1895
|
+
process.exit(1);
|
|
1815
1896
|
return;
|
|
1816
1897
|
}
|
|
1817
1898
|
if (result.warning) console.warn(`madeby check: ${result.warning}`);
|
|
@@ -1826,7 +1907,7 @@ Undisclosed commits (${result.undisclosed.length}):`);
|
|
|
1826
1907
|
if (result.undisclosed.length > 20) console.log(` \u2026 and ${result.undisclosed.length - 20} more`);
|
|
1827
1908
|
console.log(
|
|
1828
1909
|
`
|
|
1829
|
-
To disclose:
|
|
1910
|
+
To disclose who wrote it: 'madeby ai <tool>' (an AI wrote it), 'madeby me --with-ai <tool>' (you, with AI), or 'madeby me' (you alone); a Co-Authored-By trailer naming an AI tool also counts. A DCO sign-off or a signature alone doesn't say who wrote the code. (Disclosure, not a ban \u2014 what this project requires is set in .madeby/policy.json.)`
|
|
1830
1911
|
);
|
|
1831
1912
|
}
|
|
1832
1913
|
process.exit(result.pass ? 0 : 1);
|
|
@@ -2250,6 +2331,22 @@ function policyIsRequired(root) {
|
|
|
2250
2331
|
var AGENT_ROWS = 8;
|
|
2251
2332
|
var pct = (n, d) => d > 0 ? Math.round(n / d * 100) : 0;
|
|
2252
2333
|
var bar = (p) => "\u2588".repeat(Math.round(p / 5)) + "\u2591".repeat(20 - Math.round(p / 5));
|
|
2334
|
+
function renderByAuthor(r) {
|
|
2335
|
+
console.log(`authorship coverage by author \u2014 ${r.scope} (estimate \xB7 disclosure, not detection)`);
|
|
2336
|
+
if (r.byAuthor.length === 0) {
|
|
2337
|
+
console.log(" (no commits to score)");
|
|
2338
|
+
return;
|
|
2339
|
+
}
|
|
2340
|
+
const label = (a) => a.name && a.name !== a.email ? `${a.name} <${a.email}>` : a.email;
|
|
2341
|
+
const width = Math.max(...r.byAuthor.map((a) => label(a).length));
|
|
2342
|
+
console.log(` ${"author".padEnd(width)} commits disclosed human with_ai ai`);
|
|
2343
|
+
for (const a of r.byAuthor) {
|
|
2344
|
+
const k = a.byCategory;
|
|
2345
|
+
console.log(
|
|
2346
|
+
` ${label(a).padEnd(width)} ${String(a.commits).padStart(7)} ${`${a.known} \xB7 ${pct(a.known, a.commits)}%`.padStart(9)} ${String(k.human).padStart(6)} ${String(k.with_ai).padStart(7)} ${String(k.ai + k.bot).padStart(5)}`
|
|
2347
|
+
);
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2253
2350
|
function render(r, required) {
|
|
2254
2351
|
const knownPct = pct(r.known, r.total);
|
|
2255
2352
|
const unknownPct = 100 - knownPct;
|
|
@@ -2288,7 +2385,14 @@ function render(r, required) {
|
|
|
2288
2385
|
}
|
|
2289
2386
|
function coverageCommand(args) {
|
|
2290
2387
|
const json = args.includes("--json");
|
|
2291
|
-
const
|
|
2388
|
+
const byAt = args.findIndex((a) => a === "--by" || a.startsWith("--by="));
|
|
2389
|
+
const by = byAt < 0 ? void 0 : args[byAt].includes("=") ? args[byAt].split("=")[1] : args[byAt + 1];
|
|
2390
|
+
if (by !== void 0 && by !== "author") {
|
|
2391
|
+
console.error("madeby coverage: --by takes 'author'.");
|
|
2392
|
+
process.exit(2);
|
|
2393
|
+
return;
|
|
2394
|
+
}
|
|
2395
|
+
const range = args.find((a, i) => !a.startsWith("--") && !(byAt >= 0 && i === byAt + 1 && !args[byAt].includes("=")));
|
|
2292
2396
|
const root = repoRoot();
|
|
2293
2397
|
if (!root) {
|
|
2294
2398
|
console.error("madeby coverage: not a git repository.");
|
|
@@ -2300,12 +2404,18 @@ function coverageCommand(args) {
|
|
|
2300
2404
|
console.log(JSON.stringify(r, null, 2));
|
|
2301
2405
|
return;
|
|
2302
2406
|
}
|
|
2303
|
-
|
|
2407
|
+
if (by === "author") renderByAuthor(r);
|
|
2408
|
+
else render(r, policyIsRequired(root));
|
|
2304
2409
|
}
|
|
2305
2410
|
|
|
2306
2411
|
// src/init-repo.ts
|
|
2307
2412
|
import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync9, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2308
2413
|
import { dirname as dirname2, join as join8 } from "node:path";
|
|
2414
|
+
|
|
2415
|
+
// src/version.ts
|
|
2416
|
+
var VERSION = "0.3.13";
|
|
2417
|
+
|
|
2418
|
+
// src/init-repo.ts
|
|
2309
2419
|
var ACO_VERSION = 2;
|
|
2310
2420
|
var ACO_OPEN = `<!-- madeby:aco v${ACO_VERSION} -->`;
|
|
2311
2421
|
var ACO_CLOSE = `<!-- /madeby:aco -->`;
|
|
@@ -2451,6 +2561,8 @@ function initRepo(root, opts = {}) {
|
|
|
2451
2561
|
ensureAco(join8(root, "CLAUDE.md"), "CLAUDE.md", res, { createIfAbsent: false, ...aco });
|
|
2452
2562
|
if (host === "github") {
|
|
2453
2563
|
writeIfAbsent(join8(root, ".github", "workflows", "disclosure.yml"), ".github/workflows/disclosure.yml", GH_WORKFLOW, created, skipped);
|
|
2564
|
+
} else if (host === "azure-devops") {
|
|
2565
|
+
writeIfAbsent(join8(root, ".madeby", "azure-pipelines.yml"), ".madeby/azure-pipelines.yml", AZURE_PIPELINE, created, skipped);
|
|
2454
2566
|
}
|
|
2455
2567
|
const nextSteps = ["Commit the created files."];
|
|
2456
2568
|
if (raised.length > 0) {
|
|
@@ -2475,19 +2587,21 @@ function initRepo(root, opts = {}) {
|
|
|
2475
2587
|
nextSteps.push(`${outdated.join(" + ")} carries an OUTDATED ACO convention \u2014 run 'madeby init --update' to refresh it in place (only the ACO block changes).`);
|
|
2476
2588
|
}
|
|
2477
2589
|
if (host === "azure-devops") {
|
|
2478
|
-
nextSteps.push("
|
|
2590
|
+
nextSteps.push("In Azure DevOps, create a pipeline from .madeby/azure-pipelines.yml (a PR-only pipeline: no credentials kept, madeby pinned) and add it to the protected branch as a build-validation policy.");
|
|
2479
2591
|
} else if (host !== "github") {
|
|
2480
2592
|
nextSteps.push(`Add a CI step that runs 'npx madeby check' on pull requests for ${host}.`);
|
|
2481
2593
|
}
|
|
2482
2594
|
if (policy.mode !== "required") {
|
|
2483
2595
|
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.`);
|
|
2484
2596
|
}
|
|
2485
|
-
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)
|
|
2597
|
+
nextSteps.push("Contributors disclose origin with a Co-Authored-By / Generated-by trailer (AI), an Authored-by-human trailer ('madeby me'), or an Authored-by-ai trailer ('madeby ai' \u2014 AI authored, committer accountable). A DCO Signed-off-by or a signed commit alone doesn't say who wrote the code, so it counts only if the policy's 'accept' lists it. Evidence upgrade: 'madeby ai --witness' records your own AI session's witnessed spans.");
|
|
2486
2598
|
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.");
|
|
2487
2599
|
return { created, skipped, merged, updated, outdated, raised, modelOn, policyMode: policy.mode, recordModel: policy.recordModel, host, nextSteps };
|
|
2488
2600
|
}
|
|
2489
2601
|
var GH_WORKFLOW = `name: Disclosure
|
|
2490
2602
|
on: pull_request
|
|
2603
|
+
permissions:
|
|
2604
|
+
contents: read
|
|
2491
2605
|
jobs:
|
|
2492
2606
|
madeby:
|
|
2493
2607
|
runs-on: ubuntu-latest
|
|
@@ -2495,7 +2609,27 @@ jobs:
|
|
|
2495
2609
|
- uses: actions/checkout@v4
|
|
2496
2610
|
with:
|
|
2497
2611
|
fetch-depth: 0
|
|
2612
|
+
persist-credentials: false
|
|
2498
2613
|
- uses: MadeByFYI/MadeBy/packages/action@v0.2.0
|
|
2614
|
+
with:
|
|
2615
|
+
version: "${VERSION}"
|
|
2616
|
+
`;
|
|
2617
|
+
var AZURE_PIPELINE = `# MadeBy disclosure check. Run it as a build-validation policy on the protected branch: Azure
|
|
2618
|
+
# DevOps runs it for each pull request. It reads the git history only, so it keeps no credentials.
|
|
2619
|
+
trigger: none
|
|
2620
|
+
|
|
2621
|
+
pool:
|
|
2622
|
+
vmImage: ubuntu-latest
|
|
2623
|
+
|
|
2624
|
+
steps:
|
|
2625
|
+
- checkout: self
|
|
2626
|
+
fetchDepth: 0 # the PR's commits and the target branch, so the range can be read
|
|
2627
|
+
persistCredentials: false
|
|
2628
|
+
- task: NodeTool@0
|
|
2629
|
+
inputs:
|
|
2630
|
+
versionSpec: "20.x"
|
|
2631
|
+
- script: npx --yes madeby@${VERSION} check
|
|
2632
|
+
displayName: MadeBy disclosure check
|
|
2499
2633
|
`;
|
|
2500
2634
|
|
|
2501
2635
|
// src/commands/init.ts
|
|
@@ -2529,7 +2663,7 @@ madeby init \u2014 policy: ${r.policyMode}${r.recordModel ? " + records model" :
|
|
|
2529
2663
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
2530
2664
|
import { createInterface } from "node:readline";
|
|
2531
2665
|
var DEFAULT_PROTOCOL = "2024-11-05";
|
|
2532
|
-
var SERVER_INFO = { name: "madeby", version:
|
|
2666
|
+
var SERVER_INFO = { name: "madeby", version: VERSION };
|
|
2533
2667
|
function rootFor(path) {
|
|
2534
2668
|
const cwd = path && path.length ? path : process.cwd();
|
|
2535
2669
|
return execFileSync6("git", ["-C", cwd, "-c", "core.quotePath=false", "rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
|
|
@@ -2570,7 +2704,7 @@ var TOOLS = [
|
|
|
2570
2704
|
},
|
|
2571
2705
|
{
|
|
2572
2706
|
name: "coverage",
|
|
2573
|
-
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), the asserted-vs-verified trust split,
|
|
2707
|
+
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), the asserted-vs-verified trust split, byAgent: the agents and model versions named in the AI commits, exactly as disclosed (a commit naming a tool and its model counts under both), and byAuthor: the same picture per commit author (by email), to see who discloses and who doesn't. 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.",
|
|
2574
2708
|
inputSchema: repoInput,
|
|
2575
2709
|
handler: (args) => repoCoverage(rootFor(args.path), { range: args.range })
|
|
2576
2710
|
},
|
|
@@ -2640,14 +2774,20 @@ var ALIGN_GUIDE = `# Aligning a repository with MadeBy
|
|
|
2640
2774
|
MadeBy is **disclosure, not detection** \u2014 it records what a commit *states* about its origin, never
|
|
2641
2775
|
guesses whether code is AI. To align a repo:
|
|
2642
2776
|
|
|
2643
|
-
1. **Assess** \u2014 call \`
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2777
|
+
1. **Assess** \u2014 call \`coverage\` for the scoreboard (known vs unknown, the human / with_ai / ai mix, and
|
|
2778
|
+
the agents and model versions named), \`check\` for the gate's verdict, or \`who\` with no path for a
|
|
2779
|
+
repo-wide map.
|
|
2780
|
+
2. **Set up** \u2014 call \`init\`: it writes \`.madeby/policy.json\` (advisory), the AGENTS.md disclosure rule,
|
|
2781
|
+
and the CI disclosure check (\`host: "azure-devops"\` for Azure Pipelines). Idempotent; it never
|
|
2782
|
+
clobbers an existing policy or workflow (\`mode: "required"\` only ever raises an advisory policy to
|
|
2783
|
+
required). Add \`record_model: true\` if the repo should also record which model version did the work
|
|
2784
|
+
(opt-in; a project's choice). Then commit the created files.
|
|
2785
|
+
3. **Disclose going forward** \u2014 contributors disclose origin with an \`Authored-by-ai\` trailer (an AI
|
|
2786
|
+
wrote it; \`madeby ai\` adds it), \`Authored-by-human\` plus \`Assisted-by\` (a human wrote it with AI;
|
|
2787
|
+
\`madeby me --with-ai\`), \`Authored-by-human\` alone (\`madeby me\`), a Co-Authored-By / Generated-by AI
|
|
2788
|
+
trailer, or witnessed spans from an AI session log (\`madeby ai --witness\`). Human and AI disclosure
|
|
2789
|
+
ride the same ladder. A DCO Signed-off-by or a signed commit alone says who is accountable, not who
|
|
2790
|
+
wrote the code, so it satisfies the policy only if the policy's \`accept\` lists it.
|
|
2651
2791
|
4. **Account for existing history honestly** \u2014 do NOT fabricate disclosure for past commits. Read the
|
|
2652
2792
|
\`madeby://guide/backfill\` resource: recognize what's already disclosed, attach recoverable
|
|
2653
2793
|
evidence, and set an adoption boundary \u2014 pre-boundary history is labeled *unknown*, never invented.
|
|
@@ -2685,8 +2825,8 @@ from a PR.
|
|
|
2685
2825
|
## Azure DevOps (build validation branch policy)
|
|
2686
2826
|
|
|
2687
2827
|
Project Settings -> Repositories -> (repo) -> Policies -> Branch (main) -> Build Validation -> Add:
|
|
2688
|
-
point it at the pipeline
|
|
2689
|
-
required. CLI equivalent: az repos policy build create ... --branch main --blocking true.
|
|
2828
|
+
point it at the pipeline created from .madeby/azure-pipelines.yml (written by init with host
|
|
2829
|
+
azure-devops); set trigger automatic and required. CLI equivalent: az repos policy build create ... --branch main --blocking true.
|
|
2690
2830
|
`;
|
|
2691
2831
|
var BACKFILL_GUIDE = `# Honest backfill for an existing repo
|
|
2692
2832
|
|
|
@@ -2695,8 +2835,9 @@ refuses it. But you CAN make an honest, verifiable statement about history. Back
|
|
|
2695
2835
|
never raise a commit's origin above its evidence \u2014 unknown stays unknown, just explicitly so.
|
|
2696
2836
|
|
|
2697
2837
|
1. **Recognize what's already there** (free, pure evidence). Run \`check\` over FULL history:
|
|
2698
|
-
existing Co-Authored-By AI trailers
|
|
2699
|
-
|
|
2838
|
+
existing Co-Authored-By AI trailers and bot authorship are recognized now even though they predate
|
|
2839
|
+
adoption. Much of your history may already be disclosed. (DCO sign-offs and signatures are
|
|
2840
|
+
recognized too, but say who is accountable, not who wrote the code.)
|
|
2700
2841
|
|
|
2701
2842
|
2. **Attach recoverable evidence** (opt-in, evidence-grade):
|
|
2702
2843
|
- Old AI-tool session logs -> \`ai\` attaches structurally-verified spans to their commits (it
|
|
@@ -2727,7 +2868,8 @@ var POLICY_SCHEMA = {
|
|
|
2727
2868
|
properties: {
|
|
2728
2869
|
version: { const: 0 },
|
|
2729
2870
|
mode: { enum: ["advisory", "required"], description: "advisory: report only (the default, and the safe degrade); required: fail on any undisclosed commit" },
|
|
2730
|
-
accept: { type: "array", items: { type: "string" }, description: "restrict what satisfies the policy (e.g. dco-signoff, ai-trailer, commit-signature); omit \u21D2 any
|
|
2871
|
+
accept: { type: "array", items: { type: "string" }, description: "restrict what satisfies the policy (e.g. dco-signoff, ai-trailer, commit-signature); omit \u21D2 any authorship disclosure counts (a DCO sign-off or signature alone counts only if listed)" },
|
|
2872
|
+
recordModel: { type: "boolean", description: "ask AI disclosures to name the model version too (tool:model, e.g. Assisted-by: Cursor:gpt-5.3-codex); madeby reads it from the tool's own session record where it can. Set by init --record-model. A request, not a gate" }
|
|
2731
2873
|
}
|
|
2732
2874
|
};
|
|
2733
2875
|
var ACO_GUIDE = `# ACO \u2014 the Authorship Certificate of Origin
|
|
@@ -2743,12 +2885,22 @@ https://github.com/MadeByFYI/MadeBy/blob/main/ACO.md
|
|
|
2743
2885
|
- **ai** \u2014 an AI agent authored it (a human remains accountable).
|
|
2744
2886
|
|
|
2745
2887
|
## How an agent discloses its OWN work (asserted tier, no keys)
|
|
2746
|
-
-
|
|
2747
|
-
|
|
2888
|
+
- After you commit, call **disclose** with the honest \`category\` and your \`tool\` name (e.g. "Claude Code"):
|
|
2889
|
+
- \`ai\`: you wrote it; the committer stays accountable. Adds \`Authored-by-ai: <tool>\`.
|
|
2890
|
+
- \`with_ai\`: a human wrote it with your help. Adds \`Authored-by-human\` + \`Assisted-by: <tool>\`.
|
|
2891
|
+
- \`human\`: a human wrote it alone. Adds \`Authored-by-human\`.
|
|
2892
|
+
If you wrote the code and a human is only committing it, that is \`ai\`, not \`with_ai\`.
|
|
2893
|
+
- **The model version.** If the repo's \`.madeby/policy.json\` has \`"recordModel": true\`, madeby reads your
|
|
2894
|
+
model from the tool's own record of the session (Cursor, Copilot CLI, Claude Code) and writes
|
|
2895
|
+
\`<tool>:<model>\`. Elsewhere pass \`model\` only if you know your exact model version. Never guess.
|
|
2896
|
+
- **If your platform makes or rebuilds the commit** (a cloud coding agent that pushes through its own
|
|
2897
|
+
tool), a trailer amended in afterwards can be lost. Call disclose with \`print: true\` to get the trailer
|
|
2898
|
+
lines instead, and put them in the commit message's last paragraph.
|
|
2748
2899
|
- Fully AI-authored spans recoverable from your session log -> call **ai** (records structurally-verified
|
|
2749
2900
|
spans, local-only).
|
|
2750
|
-
- Read what's already disclosed -> **who
|
|
2751
|
-
ACO convention so any agent
|
|
2901
|
+
- Read what's already disclosed -> **who**; measure a repo -> **coverage** (also lists the agents and
|
|
2902
|
+
models named). Align a whole repo -> **init** (also scaffolds an AGENTS.md ACO convention so any agent
|
|
2903
|
+
working there discloses).
|
|
2752
2904
|
|
|
2753
2905
|
Disclose per commit, honestly. Undisclosed content stays *unknown*, never guessed.
|
|
2754
2906
|
`;
|
|
@@ -2849,7 +3001,7 @@ Usage:
|
|
|
2849
3001
|
(alias: whom) file's origin, or the whole repo with no path. --lines=A-B to scope.
|
|
2850
3002
|
madeby coverage [<range>] The scoreboard: how much of your repo's authorship is disclosed (known)
|
|
2851
3003
|
vs unknown, the human/with_ai/ai mix, and the asserted/verified split.
|
|
2852
|
-
--json for a machine-readable result.
|
|
3004
|
+
--by author for one row per commit author. --json for a machine-readable result.
|
|
2853
3005
|
madeby ai [<log>] [<ref>] Made by AI: record your AI session's witnessed spans into .madeby/spans.
|
|
2854
3006
|
<log> defaults to the auto-discovered Claude Code transcript.
|
|
2855
3007
|
madeby me Made by me: affirm you authored HEAD yourself (an Authored-by-human
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "madeby",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.13",
|
|
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",
|