madeby 0.3.12 → 0.3.14
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 +42 -12
- package/dist/madeby.mjs +390 -69
- 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
|
|
@@ -138,6 +148,25 @@ of AI disclosure); a sworn declaration is *sworn*. The one asymmetry is evidence
|
|
|
138
148
|
leave a re-checkable artifact (a session log, `madeby ai`); human authorship is your firsthand
|
|
139
149
|
testimony. If you used AI, disclose the AI — don't claim human.
|
|
140
150
|
|
|
151
|
+
### Disclosing earlier commits — `--for`
|
|
152
|
+
|
|
153
|
+
`me` and `ai` amend HEAD. When that isn't possible — the commits are already pushed, or the repo
|
|
154
|
+
forbids amending and rebasing — disclose them with **one new commit** instead:
|
|
155
|
+
|
|
156
|
+
```sh
|
|
157
|
+
madeby me --with-ai "Claude Code" --for origin/dev..HEAD # your commits on this branch
|
|
158
|
+
madeby ai "Claude Code" --for 4f1c2a9e # one commit
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
It makes an empty **disclosure commit** whose `Discloses: <hash>` lines name the commits and whose
|
|
162
|
+
trailers say who wrote them. Nothing is rewritten. Push it as usual: on a pull request the check
|
|
163
|
+
re-runs, and it travels with the branch into every target it's merged into. Only your own commits
|
|
164
|
+
(author email = `git config user.email`) that don't disclose yet are included; the rest are listed
|
|
165
|
+
as skipped. The model is named only if you pass `--model`, since the session you're in now didn't
|
|
166
|
+
write those commits. `check`, `coverage` and `who` read disclosure commits by the rules in
|
|
167
|
+
[ACO §5](https://github.com/MadeByFYI/MadeBy/blob/main/ACO.md#5-carriers-bindings--subsume-dont-compete), and `coverage` counts how many
|
|
168
|
+
disclosures arrived late.
|
|
169
|
+
|
|
141
170
|
**The policy file is optional.** With no `.madeby/policy.json`, `check` defaults to **advisory** —
|
|
142
171
|
it reports coverage and never blocks, so you can run it (or add the CI check) with zero config. Add
|
|
143
172
|
the file only when you want to enforce:
|
|
@@ -151,8 +180,8 @@ Two modes, nothing more:
|
|
|
151
180
|
|
|
152
181
|
- `advisory` — report coverage on each run; never fails. **The default** (and what you get with no file).
|
|
153
182
|
- `required` — fail on any commit that discloses nothing your policy accepts.
|
|
154
|
-
- optional `"accept": ["dco-signoff", "ai-trailer", …]` —
|
|
155
|
-
(omit ⇒ any
|
|
183
|
+
- optional `"accept": ["dco-signoff", "ai-trailer", …]` — set what satisfies the policy
|
|
184
|
+
(omit ⇒ any authorship disclosure counts; a DCO sign-off or signature alone counts only if listed).
|
|
156
185
|
- optional `"recordModel": true` — written by `madeby init --record-model`: the repo asks AI
|
|
157
186
|
disclosures to name the model version as well as the tool. It adds one line to the AGENTS.md rule,
|
|
158
187
|
and `me --with-ai` / `ai` then read the model from the **tool's own record of the session** — Cursor's
|
|
@@ -166,7 +195,8 @@ Two modes, nothing more:
|
|
|
166
195
|
(There's no "off" — that's just not running the check. Advisory is the floor.)
|
|
167
196
|
|
|
168
197
|
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.
|
|
198
|
+
on its own error, and never invents a stricter gate than the maintainer wrote. It never passes a
|
|
199
|
+
`required` check it couldn't actually run, either: missing history fails it (see `check` above).
|
|
170
200
|
|
|
171
201
|
## Use it as a PR check
|
|
172
202
|
|
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 git5 = (...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 (git5("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 (git5("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) {
|
|
@@ -930,6 +971,16 @@ function commitDisclosureKinds(commit) {
|
|
|
930
971
|
if (commit.signed === true) kinds.push("commit-signature");
|
|
931
972
|
return kinds;
|
|
932
973
|
}
|
|
974
|
+
function hasAuthorshipDisclosure(message) {
|
|
975
|
+
return commitDisclosureKinds({ message }).some((k) => DISCLOSURE_KINDS[k].authorship);
|
|
976
|
+
}
|
|
977
|
+
var DISCLOSES_RE = /^[ \t]*Discloses[ \t]*:[ \t]*([0-9a-f]{64}|[0-9a-f]{40})[ \t]*$/gim;
|
|
978
|
+
function disclosedTargets(message) {
|
|
979
|
+
return [...new Set([...message.matchAll(DISCLOSES_RE)].map((m) => m[1].toLowerCase()))];
|
|
980
|
+
}
|
|
981
|
+
function disclosureCommitTrailers(message) {
|
|
982
|
+
return message.split("\n").slice(1).filter((l) => !/^[ \t]*Discloses[ \t]*:/i.test(l)).join("\n").trim();
|
|
983
|
+
}
|
|
933
984
|
|
|
934
985
|
// ../core/src/policy.ts
|
|
935
986
|
var DEFAULT_POLICY = { version: 0, mode: "advisory" };
|
|
@@ -947,18 +998,22 @@ function parseDisclosurePolicy(input) {
|
|
|
947
998
|
...o.recordModel === true ? { recordModel: true } : {}
|
|
948
999
|
};
|
|
949
1000
|
}
|
|
1001
|
+
function acceptsKind(policy, kind) {
|
|
1002
|
+
return policy.accept ? policy.accept.includes(kind) : DISCLOSURE_KINDS[kind]?.authorship === true;
|
|
1003
|
+
}
|
|
950
1004
|
function evaluateDisclosurePolicy(policy, commits) {
|
|
951
1005
|
const accept = policy.accept;
|
|
952
|
-
const isAccepted = (k) => !accept || accept.includes(k);
|
|
953
1006
|
const verdicts = commits.map((c) => {
|
|
954
|
-
const satisfiedBy = c.kinds.filter(
|
|
955
|
-
|
|
1007
|
+
const satisfiedBy = c.kinds.filter((k) => acceptsKind(policy, k));
|
|
1008
|
+
const notAccepted = c.kinds.filter((k) => !acceptsKind(policy, k));
|
|
1009
|
+
return { ref: c.ref, compliant: satisfiedBy.length > 0, satisfiedBy, notAccepted };
|
|
956
1010
|
});
|
|
957
1011
|
const nonCompliant = verdicts.filter((v) => !v.compliant);
|
|
958
1012
|
const compliant = verdicts.length - nonCompliant.length;
|
|
959
1013
|
const pass = policy.mode !== "required" || nonCompliant.length === 0;
|
|
960
|
-
const wanted = accept ? accept.join(", ") : "any
|
|
961
|
-
const
|
|
1014
|
+
const wanted = accept ? accept.join(", ") : "any authorship disclosure";
|
|
1015
|
+
const otherOnly = nonCompliant.filter((v) => v.notAccepted.length > 0).length;
|
|
1016
|
+
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
1017
|
return { mode: policy.mode, pass, total: verdicts.length, compliant, nonCompliant, summary };
|
|
963
1018
|
}
|
|
964
1019
|
|
|
@@ -977,6 +1032,10 @@ function detectCiRange(env = process.env) {
|
|
|
977
1032
|
}
|
|
978
1033
|
return null;
|
|
979
1034
|
}
|
|
1035
|
+
function detectCiPullRequest(env = process.env) {
|
|
1036
|
+
for (const adapter of registry.values()) if (adapter.ciPullRequest?.(env)) return adapter.id;
|
|
1037
|
+
return null;
|
|
1038
|
+
}
|
|
980
1039
|
|
|
981
1040
|
// ../analyzer/src/host/github.ts
|
|
982
1041
|
var NOREPLY = /^(?:(\d+)\+)?([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))@users\.noreply\.github\.com$/i;
|
|
@@ -996,8 +1055,11 @@ var githubAdapter = {
|
|
|
996
1055
|
// On a `pull_request` event GitHub sets GITHUB_EVENT_NAME + GITHUB_EVENT_PATH (a JSON payload with
|
|
997
1056
|
// the PR's base/head SHAs). Reading base..head here replaces the bash range computation the Action
|
|
998
1057
|
// 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
|
-
//
|
|
1058
|
+
// (the quickstart's `fetch-depth: 0`). Any absence/parse failure → null (`check` reports the
|
|
1059
|
+
// unreadable PR build, see ciPullRequest); never throws.
|
|
1060
|
+
ciPullRequest(env) {
|
|
1061
|
+
return env.GITHUB_EVENT_NAME === "pull_request" || env.GITHUB_EVENT_NAME === "pull_request_target";
|
|
1062
|
+
},
|
|
1001
1063
|
ciRange(env) {
|
|
1002
1064
|
const event = env.GITHUB_EVENT_NAME;
|
|
1003
1065
|
if (event !== "pull_request" && event !== "pull_request_target") return null;
|
|
@@ -1018,13 +1080,85 @@ registerHostAdapter(githubAdapter);
|
|
|
1018
1080
|
// ../analyzer/src/read-disclosures.ts
|
|
1019
1081
|
import { existsSync, readFileSync as readFileSync2 } from "node:fs";
|
|
1020
1082
|
import { join } from "node:path";
|
|
1083
|
+
|
|
1084
|
+
// ../analyzer/src/late-disclosure.ts
|
|
1085
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
1086
|
+
var FIELD2 = "";
|
|
1087
|
+
var RECORD2 = "";
|
|
1088
|
+
var git = (root, args) => {
|
|
1089
|
+
try {
|
|
1090
|
+
return execFileSync2("git", ["-C", root, ...args], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024, stdio: ["ignore", "pipe", "ignore"] });
|
|
1091
|
+
} catch {
|
|
1092
|
+
return null;
|
|
1093
|
+
}
|
|
1094
|
+
};
|
|
1095
|
+
function rangeHead(range) {
|
|
1096
|
+
if (!range) return "HEAD";
|
|
1097
|
+
const parts = range.split(/\.{2,3}/);
|
|
1098
|
+
return parts[parts.length - 1] || "HEAD";
|
|
1099
|
+
}
|
|
1100
|
+
function readDisclosureCommits(root, head = "HEAD") {
|
|
1101
|
+
const out = { applied: /* @__PURE__ */ new Map(), disclosureCommits: /* @__PURE__ */ new Set(), rejected: [] };
|
|
1102
|
+
const log = git(root, ["log", "--no-merges", "-E", "--grep=^[ ]*Discloses[ ]*:[ ]*[0-9a-f]{40}", `--format=%H${FIELD2}%ae${FIELD2}%B${RECORD2}`, head]);
|
|
1103
|
+
if (!log) return out;
|
|
1104
|
+
const candidates = log.split(RECORD2).map((r) => r.replace(/^\s+/, "")).filter(Boolean).map((r) => {
|
|
1105
|
+
const [sha = "", email = "", ...msg] = r.split(FIELD2);
|
|
1106
|
+
return { sha, email: email.toLowerCase(), message: msg.join(FIELD2) };
|
|
1107
|
+
});
|
|
1108
|
+
for (const d of candidates) {
|
|
1109
|
+
const targets = disclosedTargets(d.message);
|
|
1110
|
+
if (targets.length === 0) continue;
|
|
1111
|
+
const changed = git(root, ["diff-tree", "--root", "--no-commit-id", "--name-only", "-r", d.sha]);
|
|
1112
|
+
if (changed === null || changed.trim() !== "") continue;
|
|
1113
|
+
out.disclosureCommits.add(d.sha);
|
|
1114
|
+
const trailers = disclosureCommitTrailers(d.message);
|
|
1115
|
+
const emails = /* @__PURE__ */ new Map();
|
|
1116
|
+
const shown = git(root, ["show", "-s", "--format=%H %ae", ...targets]) ?? targets.map((t) => git(root, ["show", "-s", "--format=%H %ae", t]) ?? "").join("\n");
|
|
1117
|
+
for (const line of shown.split("\n")) {
|
|
1118
|
+
const [sha, email] = line.trim().split(" ");
|
|
1119
|
+
if (sha && email !== void 0) emails.set(sha, email.toLowerCase());
|
|
1120
|
+
}
|
|
1121
|
+
for (const t of targets) {
|
|
1122
|
+
if (out.applied.has(t)) continue;
|
|
1123
|
+
const email = emails.get(t);
|
|
1124
|
+
if (email === void 0) out.rejected.push({ sha: t, via: d.sha, reason: "unknown-commit" });
|
|
1125
|
+
else if (email !== d.email) out.rejected.push({ sha: t, via: d.sha, reason: "other-author" });
|
|
1126
|
+
else if (git(root, ["merge-base", "--is-ancestor", t, d.sha]) === null) out.rejected.push({ sha: t, via: d.sha, reason: "not-an-ancestor" });
|
|
1127
|
+
else out.applied.set(t, { via: d.sha, trailers });
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
return out;
|
|
1131
|
+
}
|
|
1132
|
+
function applyLateDisclosures(commits, d) {
|
|
1133
|
+
const out = [];
|
|
1134
|
+
for (const c of commits) {
|
|
1135
|
+
const sha = (c.sha ?? "").toLowerCase();
|
|
1136
|
+
if (d.disclosureCommits.has(sha)) continue;
|
|
1137
|
+
const late = d.applied.get(sha);
|
|
1138
|
+
if (late && !hasAuthorshipDisclosure(c.message)) out.push({ ...c, message: `${c.message}
|
|
1139
|
+
|
|
1140
|
+
${late.trailers}`, late: late.via });
|
|
1141
|
+
else out.push(c);
|
|
1142
|
+
}
|
|
1143
|
+
return out;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// ../analyzer/src/read-disclosures.ts
|
|
1021
1147
|
function readDisclosures(root, opts = {}) {
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1148
|
+
return readDisclosuresWithRejects(root, opts).commits;
|
|
1149
|
+
}
|
|
1150
|
+
function readDisclosuresWithRejects(root, opts) {
|
|
1151
|
+
const late = readDisclosureCommits(root, rangeHead(opts.range));
|
|
1152
|
+
const commits = applyLateDisclosures(readGitLog(root, opts.range ? void 0 : opts.limit ?? 200, opts.range), late);
|
|
1153
|
+
return {
|
|
1154
|
+
commits: commits.map((c) => ({
|
|
1155
|
+
sha: c.sha ?? "",
|
|
1156
|
+
subject: c.message.split("\n")[0] ?? "",
|
|
1157
|
+
disclosures: commitDisclosureKinds({ message: c.message, signed: c.signed }),
|
|
1158
|
+
...c.late ? { late: c.late } : {}
|
|
1159
|
+
})),
|
|
1160
|
+
rejected: late.rejected
|
|
1161
|
+
};
|
|
1028
1162
|
}
|
|
1029
1163
|
function evaluateRepoDisclosure(root, opts = {}) {
|
|
1030
1164
|
let policy = DEFAULT_POLICY;
|
|
@@ -1037,7 +1171,7 @@ function evaluateRepoDisclosure(root, opts = {}) {
|
|
|
1037
1171
|
warning = ".madeby/policy.json is not valid JSON \u2014 degrading to advisory (report-only, never blocks).";
|
|
1038
1172
|
}
|
|
1039
1173
|
}
|
|
1040
|
-
const commits =
|
|
1174
|
+
const { commits, rejected } = readDisclosuresWithRejects(root, opts);
|
|
1041
1175
|
const disclosures = commits.map((c) => ({ ref: (c.sha || "").slice(0, 8) || "(unknown)", kinds: c.disclosures }));
|
|
1042
1176
|
const result = evaluateDisclosurePolicy(policy, disclosures);
|
|
1043
1177
|
const subjectFor = (ref) => commits.find((c) => (c.sha || "").startsWith(ref))?.subject ?? "";
|
|
@@ -1047,12 +1181,13 @@ function evaluateRepoDisclosure(root, opts = {}) {
|
|
|
1047
1181
|
summary: result.summary,
|
|
1048
1182
|
total: disclosures.length,
|
|
1049
1183
|
undisclosed: result.nonCompliant.map((v) => ({ ref: v.ref, subject: subjectFor(v.ref) })),
|
|
1184
|
+
lateRejected: rejected.filter((r) => commits.some((c) => c.sha === r.sha)),
|
|
1050
1185
|
...warning ? { warning } : {}
|
|
1051
1186
|
};
|
|
1052
1187
|
}
|
|
1053
1188
|
|
|
1054
1189
|
// ../analyzer/src/path-provenance.ts
|
|
1055
|
-
import { execFileSync as
|
|
1190
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
1056
1191
|
import { readFileSync as readFileSync4, existsSync as existsSync2 } from "node:fs";
|
|
1057
1192
|
import { join as join3 } from "node:path";
|
|
1058
1193
|
|
|
@@ -1089,13 +1224,13 @@ function readSpanManifestsFromDir(repoDir) {
|
|
|
1089
1224
|
|
|
1090
1225
|
// ../analyzer/src/path-provenance.ts
|
|
1091
1226
|
var overlaps = (aS, aE, bS, bE) => bS === void 0 || bE === void 0 || aS <= bE && aE >= bS;
|
|
1092
|
-
function
|
|
1093
|
-
return
|
|
1227
|
+
function git2(root, args) {
|
|
1228
|
+
return execFileSync3("git", ["-C", root, "-c", "core.quotePath=false", ...args], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024, stdio: ["ignore", "pipe", "ignore"] });
|
|
1094
1229
|
}
|
|
1095
1230
|
var US = "";
|
|
1096
1231
|
function commitMeta(root, sha) {
|
|
1097
1232
|
try {
|
|
1098
|
-
const out =
|
|
1233
|
+
const out = git2(root, ["show", "-s", `--format=%G?${US}%an${US}%ae${US}%B`, sha]);
|
|
1099
1234
|
const parts = out.split(US);
|
|
1100
1235
|
const sig = parts[0] ?? "N";
|
|
1101
1236
|
return { signed: sig !== "N" && sig !== "", name: parts[1] ?? "", email: parts[2] ?? "", message: (parts.slice(3).join(US) ?? "").trim() };
|
|
@@ -1165,12 +1300,17 @@ function provenanceOf(root, path, opts = {}) {
|
|
|
1165
1300
|
if (opts.useBlame !== false) {
|
|
1166
1301
|
try {
|
|
1167
1302
|
const range = opts.startLine !== void 0 ? [`-L`, `${opts.startLine},${opts.endLine ?? opts.startLine}`] : [];
|
|
1168
|
-
const porcelain =
|
|
1303
|
+
const porcelain = git2(root, ["blame", "--porcelain", ...range, "--", path]);
|
|
1169
1304
|
const metaCache = /* @__PURE__ */ new Map();
|
|
1305
|
+
const late = readDisclosureCommits(root);
|
|
1170
1306
|
for (const g of blameGroups(porcelain)) {
|
|
1171
1307
|
let meta = metaCache.get(g.sha);
|
|
1172
1308
|
if (meta === void 0) {
|
|
1173
1309
|
meta = commitMeta(root, g.sha);
|
|
1310
|
+
const filled = meta && late.applied.get(g.sha.toLowerCase());
|
|
1311
|
+
if (meta && filled && !hasAuthorshipDisclosure(meta.message)) meta = { ...meta, message: `${meta.message}
|
|
1312
|
+
|
|
1313
|
+
${filled.trailers}` };
|
|
1174
1314
|
metaCache.set(g.sha, meta);
|
|
1175
1315
|
}
|
|
1176
1316
|
if (!meta) continue;
|
|
@@ -1221,7 +1361,7 @@ function provenanceMap(root, opts = {}) {
|
|
|
1221
1361
|
const witnessed = [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
1222
1362
|
let trackedFiles = 0;
|
|
1223
1363
|
try {
|
|
1224
|
-
trackedFiles =
|
|
1364
|
+
trackedFiles = git2(root, ["ls-files", ...prefix ? ["--", prefix] : []]).split("\n").filter(Boolean).length;
|
|
1225
1365
|
} catch {
|
|
1226
1366
|
}
|
|
1227
1367
|
const commits = readDisclosures(root, { limit: opts.commitLimit ?? 500 });
|
|
@@ -1261,17 +1401,26 @@ function repoCoverage(root, opts = {}) {
|
|
|
1261
1401
|
const oldest = authored.at(-1)?.sha;
|
|
1262
1402
|
const since = limit && oldest && authored.length >= limit ? commitTime(root, oldest) : void 0;
|
|
1263
1403
|
const merges = authored.length > 0 && (!limit || authored.length < limit || since !== void 0) ? readAuthoredMerges(root, { range: opts.range, since }) : [];
|
|
1264
|
-
const commits = [...authored, ...merges];
|
|
1404
|
+
const commits = applyLateDisclosures([...authored, ...merges], readDisclosureCommits(root, rangeHead(opts.range)));
|
|
1265
1405
|
const byCategory = { human: 0, with_ai: 0, ai: 0, bot: 0 };
|
|
1266
1406
|
const byTier = { asserted: 0, verified: 0 };
|
|
1267
1407
|
let known = 0;
|
|
1408
|
+
let late = 0;
|
|
1268
1409
|
const agents = /* @__PURE__ */ new Map();
|
|
1410
|
+
const authors = /* @__PURE__ */ new Map();
|
|
1269
1411
|
for (const c of commits) {
|
|
1270
1412
|
const kinds = commitDisclosureKinds({ message: c.message, signed: c.signed });
|
|
1271
1413
|
const { class: cls, aiContributors } = classifyCommit({ message: c.message, authorName: c.authorName, authorEmail: c.authorEmail, signed: c.signed, sha: c.sha });
|
|
1272
1414
|
const category = cls === "human" && !kinds.includes("human-attestation") ? "unknown" : cls;
|
|
1415
|
+
const email = (c.authorEmail ?? "").toLowerCase();
|
|
1416
|
+
const author = authors.get(email) ?? { name: c.authorName ?? email, email, commits: 0, known: 0, byCategory: { human: 0, with_ai: 0, ai: 0, bot: 0 } };
|
|
1417
|
+
author.commits += 1;
|
|
1418
|
+
authors.set(email, author);
|
|
1273
1419
|
if (category === "unknown") continue;
|
|
1420
|
+
author.known += 1;
|
|
1421
|
+
author.byCategory[category] += 1;
|
|
1274
1422
|
known += 1;
|
|
1423
|
+
if (c.late) late += 1;
|
|
1275
1424
|
byCategory[category] += 1;
|
|
1276
1425
|
if (c.signed) byTier.verified += 1;
|
|
1277
1426
|
else byTier.asserted += 1;
|
|
@@ -1291,19 +1440,21 @@ function repoCoverage(root, opts = {}) {
|
|
|
1291
1440
|
}
|
|
1292
1441
|
return {
|
|
1293
1442
|
isEstimate: true,
|
|
1294
|
-
methodology: "commit-level authorship disclosure (estimate); undisclosed = unknown, never inferred; merges scored only when they change content of their own",
|
|
1443
|
+
methodology: "commit-level authorship disclosure (estimate); undisclosed = unknown, never inferred; merges scored only when they change content of their own; a disclosure commit (Discloses:) fills in commits disclosed afterwards and isn't scored itself",
|
|
1295
1444
|
scope: (opts.range ? `range ${opts.range}` : `last ${authored.length} commits`) + (merges.length ? ` + ${merges.length} merges with changes of their own` : ""),
|
|
1296
1445
|
total: commits.length,
|
|
1297
1446
|
known,
|
|
1298
1447
|
unknown: commits.length - known,
|
|
1448
|
+
late,
|
|
1299
1449
|
byCategory,
|
|
1300
1450
|
byTier,
|
|
1301
|
-
byAgent: [...agents.values()].sort((a, b) => b.commits - a.commits || a.name.localeCompare(b.name))
|
|
1451
|
+
byAgent: [...agents.values()].sort((a, b) => b.commits - a.commits || a.name.localeCompare(b.name)),
|
|
1452
|
+
byAuthor: [...authors.values()].sort((a, b) => b.commits - a.commits || a.email.localeCompare(b.email))
|
|
1302
1453
|
};
|
|
1303
1454
|
}
|
|
1304
1455
|
|
|
1305
1456
|
// ../analyzer/src/record-spans.ts
|
|
1306
|
-
import { execFileSync as
|
|
1457
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
1307
1458
|
import { existsSync as existsSync3, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync, writeFileSync } from "node:fs";
|
|
1308
1459
|
import { homedir } from "node:os";
|
|
1309
1460
|
import { dirname, join as join4 } from "node:path";
|
|
@@ -1636,8 +1787,8 @@ ${body}
|
|
|
1636
1787
|
}
|
|
1637
1788
|
|
|
1638
1789
|
// ../analyzer/src/record-spans.ts
|
|
1639
|
-
function
|
|
1640
|
-
return
|
|
1790
|
+
function git3(root, ...a) {
|
|
1791
|
+
return execFileSync4("git", ["-C", root, "-c", "core.quotePath=false", ...a], { encoding: "utf8" }).trim();
|
|
1641
1792
|
}
|
|
1642
1793
|
function autodiscover(root) {
|
|
1643
1794
|
const enc3 = root.replace(/[/_]/g, "-");
|
|
@@ -1653,7 +1804,7 @@ function recordAiSpans(root, opts = {}) {
|
|
|
1653
1804
|
if (!transcriptPath || !existsSync3(transcriptPath)) {
|
|
1654
1805
|
return { written: false, matched: 0, discarded: 0, message: "transcript not found \u2014 pass a transcript path", error: "transcript-not-found" };
|
|
1655
1806
|
}
|
|
1656
|
-
const commit =
|
|
1807
|
+
const commit = git3(root, "rev-parse", opts.ref ?? "HEAD");
|
|
1657
1808
|
const transcriptText = readFileSync5(transcriptPath, "utf8");
|
|
1658
1809
|
const parser = detectParser(transcriptText);
|
|
1659
1810
|
if (!parser) {
|
|
@@ -1671,7 +1822,7 @@ function recordAiSpans(root, opts = {}) {
|
|
|
1671
1822
|
}
|
|
1672
1823
|
},
|
|
1673
1824
|
commit,
|
|
1674
|
-
operatorId:
|
|
1825
|
+
operatorId: git3(root, "show", "-s", "--format=%ae", commit),
|
|
1675
1826
|
now: opts.now ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
1676
1827
|
});
|
|
1677
1828
|
if (result.manifest.attestations.length === 0) {
|
|
@@ -1745,7 +1896,11 @@ var azureAdapter = {
|
|
|
1745
1896
|
// these optional precisely so an adapter can omit what its host doesn't offer.
|
|
1746
1897
|
// Azure Pipelines sets these predefined variables on a PR-triggered build. The target branch is
|
|
1747
1898
|
// 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
|
|
1899
|
+
// fetchDepth: 0 so that ref + the PR history are present. Any absence → null; `check` then reports
|
|
1900
|
+
// the PR build whose range it couldn't read (ciPullRequest) instead of passing on recent history.
|
|
1901
|
+
ciPullRequest(env) {
|
|
1902
|
+
return env.BUILD_REASON === "PullRequest";
|
|
1903
|
+
},
|
|
1749
1904
|
ciRange(env) {
|
|
1750
1905
|
if (env.BUILD_REASON !== "PullRequest") return null;
|
|
1751
1906
|
const targetRef = env.SYSTEM_PULLREQUEST_TARGETBRANCH;
|
|
@@ -1771,7 +1926,10 @@ var gitlabAdapter = {
|
|
|
1771
1926
|
},
|
|
1772
1927
|
// GitLab CI sets these predefined variables on a merge-request pipeline. CI_MERGE_REQUEST_DIFF_BASE_SHA
|
|
1773
1928
|
// 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 (
|
|
1929
|
+
// (Needs the MR pipeline / a deep-enough fetch for those SHAs.) Any absence → null (reported by `check`).
|
|
1930
|
+
ciPullRequest(env) {
|
|
1931
|
+
return env.CI_PIPELINE_SOURCE === "merge_request_event";
|
|
1932
|
+
},
|
|
1775
1933
|
ciRange(env) {
|
|
1776
1934
|
if (env.CI_PIPELINE_SOURCE !== "merge_request_event") return null;
|
|
1777
1935
|
const base = env.CI_MERGE_REQUEST_DIFF_BASE_SHA;
|
|
@@ -1783,10 +1941,10 @@ var gitlabAdapter = {
|
|
|
1783
1941
|
registerHostAdapter(gitlabAdapter);
|
|
1784
1942
|
|
|
1785
1943
|
// src/scope.ts
|
|
1786
|
-
import { execFileSync as
|
|
1944
|
+
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
1787
1945
|
function repoRoot() {
|
|
1788
1946
|
try {
|
|
1789
|
-
return
|
|
1947
|
+
return execFileSync5("git", ["-c", "core.quotePath=false", "rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
|
|
1790
1948
|
} catch {
|
|
1791
1949
|
return null;
|
|
1792
1950
|
}
|
|
@@ -1809,9 +1967,23 @@ function checkCommand(args) {
|
|
|
1809
1967
|
}
|
|
1810
1968
|
const { range, autoHost } = resolveRange(explicit);
|
|
1811
1969
|
const result = evaluateRepoDisclosure(root, { range });
|
|
1970
|
+
const prHost = explicit || autoHost ? null : detectCiPullRequest();
|
|
1971
|
+
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);
|
|
1972
|
+
const blocked = problem !== null && result.mode === "required";
|
|
1973
|
+
const pass = result.pass && !blocked;
|
|
1812
1974
|
if (json) {
|
|
1813
|
-
console.log(JSON.stringify({ range: range ?? null, autoHost: autoHost ?? null, ...result }, null, 2));
|
|
1814
|
-
process.exit(
|
|
1975
|
+
console.log(JSON.stringify({ range: range ?? null, autoHost: autoHost ?? null, ...result, pass, ...problem ? { historyProblem: problem } : {} }, null, 2));
|
|
1976
|
+
process.exit(pass ? 0 : 1);
|
|
1977
|
+
return;
|
|
1978
|
+
}
|
|
1979
|
+
if (problem) {
|
|
1980
|
+
const banner = blocked ? "FAILED \u2014 can't check this history" : "WARNING \u2014 this result may be wrong";
|
|
1981
|
+
console.error(`
|
|
1982
|
+
madeby check: ${banner}: ${problem}
|
|
1983
|
+
`);
|
|
1984
|
+
}
|
|
1985
|
+
if (blocked) {
|
|
1986
|
+
process.exit(1);
|
|
1815
1987
|
return;
|
|
1816
1988
|
}
|
|
1817
1989
|
if (result.warning) console.warn(`madeby check: ${result.warning}`);
|
|
@@ -1824,16 +1996,20 @@ Undisclosed commits (${result.undisclosed.length}):`);
|
|
|
1824
1996
|
console.log(` \u2717 ${v.ref} ${v.subject.slice(0, 60)}`);
|
|
1825
1997
|
}
|
|
1826
1998
|
if (result.undisclosed.length > 20) console.log(` \u2026 and ${result.undisclosed.length - 20} more`);
|
|
1999
|
+
const why = { "other-author": "it names another author's commit", "not-an-ancestor": "it names a commit that isn't before it", "unknown-commit": "it names a commit that isn't here" };
|
|
2000
|
+
for (const r of result.lateRejected.slice(0, 20)) {
|
|
2001
|
+
console.log(` ! ${r.sha.slice(0, 8)} is named by disclosure commit ${r.via.slice(0, 8)}, which can't disclose it: ${why[r.reason]}.`);
|
|
2002
|
+
}
|
|
1827
2003
|
console.log(
|
|
1828
2004
|
`
|
|
1829
|
-
To disclose:
|
|
2005
|
+
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. Already pushed, or no amending allowed? Add --for <range> (e.g. madeby me --with-ai <tool> --for origin/main..HEAD): one new commit discloses the earlier ones, nothing rewritten. (Disclosure, not a ban \u2014 what this project requires is set in .madeby/policy.json.)`
|
|
1830
2006
|
);
|
|
1831
2007
|
}
|
|
1832
2008
|
process.exit(result.pass ? 0 : 1);
|
|
1833
2009
|
}
|
|
1834
2010
|
|
|
1835
2011
|
// src/commands/me.ts
|
|
1836
|
-
import { execFileSync as
|
|
2012
|
+
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
1837
2013
|
import { existsSync as existsSync5, readFileSync as readFileSync7 } from "node:fs";
|
|
1838
2014
|
import { homedir as homedir3 } from "node:os";
|
|
1839
2015
|
import { join as join6 } from "node:path";
|
|
@@ -1952,8 +2128,8 @@ function parseWithAi(args) {
|
|
|
1952
2128
|
const next = args[i + 1];
|
|
1953
2129
|
return { withAi: true, tool: next && !next.startsWith("-") ? next : "AI" };
|
|
1954
2130
|
}
|
|
1955
|
-
function
|
|
1956
|
-
return
|
|
2131
|
+
function git4(root, args) {
|
|
2132
|
+
return execFileSync6("git", ["-C", root, ...args], { encoding: "utf8" }).trim();
|
|
1957
2133
|
}
|
|
1958
2134
|
var HUMAN_ATTEST2 = /^[ \t]*(?:Authored-by-human|Human-authored(?:-by)?)[ \t]*:/im;
|
|
1959
2135
|
var ASSIST = /^[ \t]*Assisted-by[ \t]*:/im;
|
|
@@ -1979,15 +2155,15 @@ function attestAiAuthorship(root, opts = {}) {
|
|
|
1979
2155
|
const tool = ai.value;
|
|
1980
2156
|
const base = { ok: false, changed: false, withAi: false, tool, model: ai.model, modelSource: ai.modelSource, added: [], category: "ai" };
|
|
1981
2157
|
try {
|
|
1982
|
-
|
|
2158
|
+
execFileSync6("git", ["-C", root, "diff", "--cached", "--quiet"]);
|
|
1983
2159
|
} catch {
|
|
1984
2160
|
return { ...base, reason: "staged-changes" };
|
|
1985
2161
|
}
|
|
1986
2162
|
let subject = "";
|
|
1987
2163
|
let message = "";
|
|
1988
2164
|
try {
|
|
1989
|
-
subject =
|
|
1990
|
-
message =
|
|
2165
|
+
subject = git4(root, ["show", "-s", "--format=%s", "HEAD"]);
|
|
2166
|
+
message = git4(root, ["show", "-s", "--format=%B", "HEAD"]);
|
|
1991
2167
|
} catch {
|
|
1992
2168
|
return { ...base, reason: "no-commit" };
|
|
1993
2169
|
}
|
|
@@ -1998,7 +2174,7 @@ function attestAiAuthorship(root, opts = {}) {
|
|
|
1998
2174
|
added.push("Authored-by-ai");
|
|
1999
2175
|
}
|
|
2000
2176
|
if (trailers.length > 0) {
|
|
2001
|
-
|
|
2177
|
+
execFileSync6("git", ["-C", root, "commit", "--amend", "--no-edit", "--allow-empty", ...trailers]);
|
|
2002
2178
|
}
|
|
2003
2179
|
return { ...base, ok: true, changed: trailers.length > 0, subject, added };
|
|
2004
2180
|
}
|
|
@@ -2010,8 +2186,8 @@ function disclosureTrailers(root, opts) {
|
|
|
2010
2186
|
let name = "";
|
|
2011
2187
|
let email = "";
|
|
2012
2188
|
try {
|
|
2013
|
-
name =
|
|
2014
|
-
email =
|
|
2189
|
+
name = git4(root, ["config", "user.name"]);
|
|
2190
|
+
email = git4(root, ["config", "user.email"]);
|
|
2015
2191
|
} catch {
|
|
2016
2192
|
}
|
|
2017
2193
|
if (!name || !email) return { ok: false, reason: "no-identity", lines: [] };
|
|
@@ -2028,21 +2204,21 @@ function affirmAuthorship(root, opts = {}) {
|
|
|
2028
2204
|
let name = "";
|
|
2029
2205
|
let email = "";
|
|
2030
2206
|
try {
|
|
2031
|
-
name =
|
|
2032
|
-
email =
|
|
2207
|
+
name = git4(root, ["config", "user.name"]);
|
|
2208
|
+
email = git4(root, ["config", "user.email"]);
|
|
2033
2209
|
} catch {
|
|
2034
2210
|
}
|
|
2035
2211
|
if (!name || !email) return { ...base, reason: "no-identity" };
|
|
2036
2212
|
try {
|
|
2037
|
-
|
|
2213
|
+
execFileSync6("git", ["-C", root, "diff", "--cached", "--quiet"]);
|
|
2038
2214
|
} catch {
|
|
2039
2215
|
return { ...base, reason: "staged-changes", name, email };
|
|
2040
2216
|
}
|
|
2041
2217
|
let subject = "";
|
|
2042
2218
|
let message = "";
|
|
2043
2219
|
try {
|
|
2044
|
-
subject =
|
|
2045
|
-
message =
|
|
2220
|
+
subject = git4(root, ["show", "-s", "--format=%s", "HEAD"]);
|
|
2221
|
+
message = git4(root, ["show", "-s", "--format=%B", "HEAD"]);
|
|
2046
2222
|
} catch {
|
|
2047
2223
|
return { ...base, reason: "no-commit" };
|
|
2048
2224
|
}
|
|
@@ -2059,11 +2235,84 @@ function affirmAuthorship(root, opts = {}) {
|
|
|
2059
2235
|
added.push("Assisted-by");
|
|
2060
2236
|
}
|
|
2061
2237
|
if (trailers.length > 0) {
|
|
2062
|
-
|
|
2238
|
+
execFileSync6("git", ["-C", root, "commit", "--amend", "--no-edit", "--allow-empty", ...trailers]);
|
|
2063
2239
|
}
|
|
2064
2240
|
const category = hasAssist || added.includes("Assisted-by") ? "with_ai" : "human";
|
|
2065
2241
|
return { ...base, ok: true, changed: trailers.length > 0, name, email, subject, added, category };
|
|
2066
2242
|
}
|
|
2243
|
+
function parseFor(args) {
|
|
2244
|
+
const i = args.findIndex((a2) => a2 === "--for" || a2.startsWith("--for="));
|
|
2245
|
+
if (i === -1) return void 0;
|
|
2246
|
+
const a = args[i];
|
|
2247
|
+
const v = a.startsWith("--for=") ? a.slice("--for=".length) : args[i + 1];
|
|
2248
|
+
return v && !v.startsWith("-") ? v : void 0;
|
|
2249
|
+
}
|
|
2250
|
+
function discloseFor(root, target, opts) {
|
|
2251
|
+
const none = { disclosed: [], skipped: [], lines: [] };
|
|
2252
|
+
let email = "";
|
|
2253
|
+
try {
|
|
2254
|
+
email = git4(root, ["config", "user.email"]).toLowerCase();
|
|
2255
|
+
} catch {
|
|
2256
|
+
}
|
|
2257
|
+
if (!email) return { ok: false, reason: "no-identity", ...none };
|
|
2258
|
+
try {
|
|
2259
|
+
execFileSync6("git", ["-C", root, "diff", "--cached", "--quiet"]);
|
|
2260
|
+
} catch {
|
|
2261
|
+
return { ok: false, reason: "staged-changes", ...none };
|
|
2262
|
+
}
|
|
2263
|
+
let commits;
|
|
2264
|
+
if (target.includes("..")) {
|
|
2265
|
+
commits = [...readGitLog(root, void 0, target), ...readAuthoredMerges(root, { range: target })];
|
|
2266
|
+
} else {
|
|
2267
|
+
try {
|
|
2268
|
+
const sha = git4(root, ["rev-parse", "--verify", "--quiet", `${target}^{commit}`]);
|
|
2269
|
+
commits = readGitLog(root, 1, sha);
|
|
2270
|
+
if (commits[0]?.sha !== sha) commits = readAuthoredMerges(root, { range: `${sha}^!` });
|
|
2271
|
+
} catch {
|
|
2272
|
+
return { ok: false, reason: "bad-target", ...none };
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
if (commits.length === 0) return { ok: false, reason: "bad-target", ...none };
|
|
2276
|
+
const later = readDisclosureCommits(root);
|
|
2277
|
+
const disclosed = [];
|
|
2278
|
+
const skipped = [];
|
|
2279
|
+
for (const c of commits) {
|
|
2280
|
+
const sha = (c.sha ?? "").toLowerCase();
|
|
2281
|
+
const subject2 = c.message.split("\n")[0] ?? "";
|
|
2282
|
+
if (later.disclosureCommits.has(sha)) continue;
|
|
2283
|
+
if ((c.authorEmail ?? "").toLowerCase() !== email) skipped.push({ sha, subject: subject2, reason: "not-yours" });
|
|
2284
|
+
else if (hasAuthorshipDisclosure(c.message)) skipped.push({ sha, subject: subject2, reason: "already-discloses" });
|
|
2285
|
+
else if (later.applied.has(sha)) skipped.push({ sha, subject: subject2, reason: "already-disclosed-later" });
|
|
2286
|
+
else disclosed.push({ sha, subject: subject2 });
|
|
2287
|
+
}
|
|
2288
|
+
const t = disclosureTrailers(root, { category: opts.category, tool: opts.tool, model: opts.model, env: {}, home: "/nonexistent" });
|
|
2289
|
+
if (!t.ok) return { ok: false, reason: "no-identity", ...none };
|
|
2290
|
+
if (disclosed.length === 0) return { ok: true, disclosed, skipped, lines: t.lines };
|
|
2291
|
+
const n = disclosed.length;
|
|
2292
|
+
const subject = `Disclose authorship of ${n} earlier commit${n === 1 ? "" : "s"}`;
|
|
2293
|
+
const body = [...disclosed.map((d) => `Discloses: ${d.sha}`), ...t.lines].join("\n");
|
|
2294
|
+
execFileSync6("git", ["-C", root, "commit", "--allow-empty", "-m", subject, "-m", body], { stdio: ["ignore", "ignore", "pipe"] });
|
|
2295
|
+
return { ok: true, commit: git4(root, ["rev-parse", "HEAD"]), disclosed, skipped, lines: t.lines };
|
|
2296
|
+
}
|
|
2297
|
+
function renderDiscloseFor(cmd2, target, r) {
|
|
2298
|
+
if (!r.ok) {
|
|
2299
|
+
if (r.reason === "no-identity") console.error(`madeby ${cmd2}: set your git identity first (git config user.name / user.email).`);
|
|
2300
|
+
else if (r.reason === "staged-changes") console.error(`madeby ${cmd2}: you have staged changes. A disclosure commit carries no content \u2014 commit or unstage them first.`);
|
|
2301
|
+
else console.error(`madeby ${cmd2}: no commits found for ${target}. Give a commit or a range, e.g. --for origin/dev..HEAD`);
|
|
2302
|
+
process.exit(1);
|
|
2303
|
+
return;
|
|
2304
|
+
}
|
|
2305
|
+
const why = { "not-yours": "another author's \u2014 they disclose their own", "already-discloses": "its message already discloses", "already-disclosed-later": "a disclosure commit already covers it" };
|
|
2306
|
+
for (const s of r.skipped) console.log(` skipped ${s.sha.slice(0, 8)} ${s.subject.slice(0, 50)} (${why[s.reason]})`);
|
|
2307
|
+
if (!r.commit) {
|
|
2308
|
+
console.log(`madeby ${cmd2}: nothing to disclose in ${target}.`);
|
|
2309
|
+
return;
|
|
2310
|
+
}
|
|
2311
|
+
console.log(
|
|
2312
|
+
`madeby ${cmd2}: made disclosure commit ${r.commit.slice(0, 8)} for ${r.disclosed.length} earlier commit${r.disclosed.length === 1 ? "" : "s"} (${r.lines.join("; ")}).
|
|
2313
|
+
Nothing was rewritten. Push it as usual; on a pull request the check re-runs.`
|
|
2314
|
+
);
|
|
2315
|
+
}
|
|
2067
2316
|
function meCommand(args) {
|
|
2068
2317
|
const { withAi, tool } = parseWithAi(args);
|
|
2069
2318
|
const model = parseModel(args);
|
|
@@ -2078,6 +2327,11 @@ function meCommand(args) {
|
|
|
2078
2327
|
process.exit(2);
|
|
2079
2328
|
return;
|
|
2080
2329
|
}
|
|
2330
|
+
const forTarget = parseFor(args);
|
|
2331
|
+
if (forTarget) {
|
|
2332
|
+
renderDiscloseFor("me", forTarget, discloseFor(root, forTarget, { category: withAi ? "with_ai" : "human", tool, model }));
|
|
2333
|
+
return;
|
|
2334
|
+
}
|
|
2081
2335
|
if (args.includes("--print")) {
|
|
2082
2336
|
const t = disclosureTrailers(root, { category: withAi ? "with_ai" : "human", tool, model });
|
|
2083
2337
|
if (!t.ok) {
|
|
@@ -2120,7 +2374,7 @@ function aiCommand(args) {
|
|
|
2120
2374
|
const makeSample = args.includes("--make-parser-sample");
|
|
2121
2375
|
const witness = args.includes("--witness");
|
|
2122
2376
|
const model = parseModel(args);
|
|
2123
|
-
const positional = args.filter((a, i) => !a.startsWith("--") &&
|
|
2377
|
+
const positional = args.filter((a, i) => !a.startsWith("--") && args[i - 1] !== "--model" && args[i - 1] !== "--for");
|
|
2124
2378
|
if (makeSample) {
|
|
2125
2379
|
const s = writeParserSample(root, { transcriptPath: positional[0] });
|
|
2126
2380
|
if (s.error === "transcript-not-found") {
|
|
@@ -2162,6 +2416,11 @@ function aiCommand(args) {
|
|
|
2162
2416
|
return;
|
|
2163
2417
|
}
|
|
2164
2418
|
const tool = positional[0] || "AI";
|
|
2419
|
+
const forTarget = parseFor(args);
|
|
2420
|
+
if (forTarget) {
|
|
2421
|
+
renderDiscloseFor("ai", forTarget, discloseFor(root, forTarget, { category: "ai", tool, model }));
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2165
2424
|
if (args.includes("--print")) {
|
|
2166
2425
|
console.log(disclosureTrailers(root, { category: "ai", tool, model }).lines.join("\n"));
|
|
2167
2426
|
return;
|
|
@@ -2250,6 +2509,22 @@ function policyIsRequired(root) {
|
|
|
2250
2509
|
var AGENT_ROWS = 8;
|
|
2251
2510
|
var pct = (n, d) => d > 0 ? Math.round(n / d * 100) : 0;
|
|
2252
2511
|
var bar = (p) => "\u2588".repeat(Math.round(p / 5)) + "\u2591".repeat(20 - Math.round(p / 5));
|
|
2512
|
+
function renderByAuthor(r) {
|
|
2513
|
+
console.log(`authorship coverage by author \u2014 ${r.scope} (estimate \xB7 disclosure, not detection)`);
|
|
2514
|
+
if (r.byAuthor.length === 0) {
|
|
2515
|
+
console.log(" (no commits to score)");
|
|
2516
|
+
return;
|
|
2517
|
+
}
|
|
2518
|
+
const label = (a) => a.name && a.name !== a.email ? `${a.name} <${a.email}>` : a.email;
|
|
2519
|
+
const width = Math.max(...r.byAuthor.map((a) => label(a).length));
|
|
2520
|
+
console.log(` ${"author".padEnd(width)} commits disclosed human with_ai ai`);
|
|
2521
|
+
for (const a of r.byAuthor) {
|
|
2522
|
+
const k = a.byCategory;
|
|
2523
|
+
console.log(
|
|
2524
|
+
` ${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)}`
|
|
2525
|
+
);
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2253
2528
|
function render(r, required) {
|
|
2254
2529
|
const knownPct = pct(r.known, r.total);
|
|
2255
2530
|
const unknownPct = 100 - knownPct;
|
|
@@ -2288,7 +2563,14 @@ function render(r, required) {
|
|
|
2288
2563
|
}
|
|
2289
2564
|
function coverageCommand(args) {
|
|
2290
2565
|
const json = args.includes("--json");
|
|
2291
|
-
const
|
|
2566
|
+
const byAt = args.findIndex((a) => a === "--by" || a.startsWith("--by="));
|
|
2567
|
+
const by = byAt < 0 ? void 0 : args[byAt].includes("=") ? args[byAt].split("=")[1] : args[byAt + 1];
|
|
2568
|
+
if (by !== void 0 && by !== "author") {
|
|
2569
|
+
console.error("madeby coverage: --by takes 'author'.");
|
|
2570
|
+
process.exit(2);
|
|
2571
|
+
return;
|
|
2572
|
+
}
|
|
2573
|
+
const range = args.find((a, i) => !a.startsWith("--") && !(byAt >= 0 && i === byAt + 1 && !args[byAt].includes("=")));
|
|
2292
2574
|
const root = repoRoot();
|
|
2293
2575
|
if (!root) {
|
|
2294
2576
|
console.error("madeby coverage: not a git repository.");
|
|
@@ -2300,12 +2582,18 @@ function coverageCommand(args) {
|
|
|
2300
2582
|
console.log(JSON.stringify(r, null, 2));
|
|
2301
2583
|
return;
|
|
2302
2584
|
}
|
|
2303
|
-
|
|
2585
|
+
if (by === "author") renderByAuthor(r);
|
|
2586
|
+
else render(r, policyIsRequired(root));
|
|
2304
2587
|
}
|
|
2305
2588
|
|
|
2306
2589
|
// src/init-repo.ts
|
|
2307
2590
|
import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync9, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2308
2591
|
import { dirname as dirname2, join as join8 } from "node:path";
|
|
2592
|
+
|
|
2593
|
+
// src/version.ts
|
|
2594
|
+
var VERSION = "0.3.14";
|
|
2595
|
+
|
|
2596
|
+
// src/init-repo.ts
|
|
2309
2597
|
var ACO_VERSION = 2;
|
|
2310
2598
|
var ACO_OPEN = `<!-- madeby:aco v${ACO_VERSION} -->`;
|
|
2311
2599
|
var ACO_CLOSE = `<!-- /madeby:aco -->`;
|
|
@@ -2451,6 +2739,8 @@ function initRepo(root, opts = {}) {
|
|
|
2451
2739
|
ensureAco(join8(root, "CLAUDE.md"), "CLAUDE.md", res, { createIfAbsent: false, ...aco });
|
|
2452
2740
|
if (host === "github") {
|
|
2453
2741
|
writeIfAbsent(join8(root, ".github", "workflows", "disclosure.yml"), ".github/workflows/disclosure.yml", GH_WORKFLOW, created, skipped);
|
|
2742
|
+
} else if (host === "azure-devops") {
|
|
2743
|
+
writeIfAbsent(join8(root, ".madeby", "azure-pipelines.yml"), ".madeby/azure-pipelines.yml", AZURE_PIPELINE, created, skipped);
|
|
2454
2744
|
}
|
|
2455
2745
|
const nextSteps = ["Commit the created files."];
|
|
2456
2746
|
if (raised.length > 0) {
|
|
@@ -2475,19 +2765,21 @@ function initRepo(root, opts = {}) {
|
|
|
2475
2765
|
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
2766
|
}
|
|
2477
2767
|
if (host === "azure-devops") {
|
|
2478
|
-
nextSteps.push("
|
|
2768
|
+
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
2769
|
} else if (host !== "github") {
|
|
2480
2770
|
nextSteps.push(`Add a CI step that runs 'npx madeby check' on pull requests for ${host}.`);
|
|
2481
2771
|
}
|
|
2482
2772
|
if (policy.mode !== "required") {
|
|
2483
2773
|
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
2774
|
}
|
|
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)
|
|
2775
|
+
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
2776
|
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
2777
|
return { created, skipped, merged, updated, outdated, raised, modelOn, policyMode: policy.mode, recordModel: policy.recordModel, host, nextSteps };
|
|
2488
2778
|
}
|
|
2489
2779
|
var GH_WORKFLOW = `name: Disclosure
|
|
2490
2780
|
on: pull_request
|
|
2781
|
+
permissions:
|
|
2782
|
+
contents: read
|
|
2491
2783
|
jobs:
|
|
2492
2784
|
madeby:
|
|
2493
2785
|
runs-on: ubuntu-latest
|
|
@@ -2495,7 +2787,27 @@ jobs:
|
|
|
2495
2787
|
- uses: actions/checkout@v4
|
|
2496
2788
|
with:
|
|
2497
2789
|
fetch-depth: 0
|
|
2790
|
+
persist-credentials: false
|
|
2498
2791
|
- uses: MadeByFYI/MadeBy/packages/action@v0.2.0
|
|
2792
|
+
with:
|
|
2793
|
+
version: "${VERSION}"
|
|
2794
|
+
`;
|
|
2795
|
+
var AZURE_PIPELINE = `# MadeBy disclosure check. Run it as a build-validation policy on the protected branch: Azure
|
|
2796
|
+
# DevOps runs it for each pull request. It reads the git history only, so it keeps no credentials.
|
|
2797
|
+
trigger: none
|
|
2798
|
+
|
|
2799
|
+
pool:
|
|
2800
|
+
vmImage: ubuntu-latest
|
|
2801
|
+
|
|
2802
|
+
steps:
|
|
2803
|
+
- checkout: self
|
|
2804
|
+
fetchDepth: 0 # the PR's commits and the target branch, so the range can be read
|
|
2805
|
+
persistCredentials: false
|
|
2806
|
+
- task: NodeTool@0
|
|
2807
|
+
inputs:
|
|
2808
|
+
versionSpec: "20.x"
|
|
2809
|
+
- script: npx --yes madeby@${VERSION} check
|
|
2810
|
+
displayName: MadeBy disclosure check
|
|
2499
2811
|
`;
|
|
2500
2812
|
|
|
2501
2813
|
// src/commands/init.ts
|
|
@@ -2526,13 +2838,13 @@ madeby init \u2014 policy: ${r.policyMode}${r.recordModel ? " + records model" :
|
|
|
2526
2838
|
}
|
|
2527
2839
|
|
|
2528
2840
|
// src/mcp.ts
|
|
2529
|
-
import { execFileSync as
|
|
2841
|
+
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
2530
2842
|
import { createInterface } from "node:readline";
|
|
2531
2843
|
var DEFAULT_PROTOCOL = "2024-11-05";
|
|
2532
|
-
var SERVER_INFO = { name: "madeby", version:
|
|
2844
|
+
var SERVER_INFO = { name: "madeby", version: VERSION };
|
|
2533
2845
|
function rootFor(path) {
|
|
2534
2846
|
const cwd = path && path.length ? path : process.cwd();
|
|
2535
|
-
return
|
|
2847
|
+
return execFileSync7("git", ["-C", cwd, "-c", "core.quotePath=false", "rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
|
|
2536
2848
|
}
|
|
2537
2849
|
var repoInput = {
|
|
2538
2850
|
type: "object",
|
|
@@ -2570,7 +2882,7 @@ var TOOLS = [
|
|
|
2570
2882
|
},
|
|
2571
2883
|
{
|
|
2572
2884
|
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,
|
|
2885
|
+
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
2886
|
inputSchema: repoInput,
|
|
2575
2887
|
handler: (args) => repoCoverage(rootFor(args.path), { range: args.range })
|
|
2576
2888
|
},
|
|
@@ -2595,7 +2907,7 @@ var TOOLS = [
|
|
|
2595
2907
|
},
|
|
2596
2908
|
{
|
|
2597
2909
|
name: "disclose",
|
|
2598
|
-
description: "Disclose authorship on HEAD per ACO \u2014 the agent-native disclosure of work you just did. Pick the honest category: 'ai' (an AI authored it; you, the committer, are accountable) adds Authored-by-ai; 'with_ai' (a human authored it WITH AI assistance) adds Authored-by-human + Assisted-by; 'human' adds Authored-by-human. IMPORTANT: if the AI wrote the code and a human is merely committing it, that is 'ai' \u2014 do NOT claim 'with_ai'/human authorship the human didn't do. Amends HEAD's MESSAGE only (a testimony, never a content change) and refuses if there are staged changes. Evidence upgrade for the ai category (witnessed spans from a session log) is the CLI `madeby ai --witness`. Read the standard: madeby://guide/aco.",
|
|
2910
|
+
description: "Disclose authorship on HEAD per ACO \u2014 the agent-native disclosure of work you just did. Pick the honest category: 'ai' (an AI authored it; you, the committer, are accountable) adds Authored-by-ai; 'with_ai' (a human authored it WITH AI assistance) adds Authored-by-human + Assisted-by; 'human' adds Authored-by-human. IMPORTANT: if the AI wrote the code and a human is merely committing it, that is 'ai' \u2014 do NOT claim 'with_ai'/human authorship the human didn't do. Amends HEAD's MESSAGE only (a testimony, never a content change) and refuses if there are staged changes. Evidence upgrade for the ai category (witnessed spans from a session log) is the CLI `madeby ai --witness`. To disclose EARLIER commits without rewriting them (a repo that forbids amend/rebase), pass `for` (a commit or range): it makes one empty disclosure commit naming them (Discloses: <hash>), only for the git user's own undisclosed commits; push it and the PR check re-runs. Read the standard: madeby://guide/aco.",
|
|
2599
2911
|
inputSchema: {
|
|
2600
2912
|
type: "object",
|
|
2601
2913
|
properties: {
|
|
@@ -2604,7 +2916,8 @@ var TOOLS = [
|
|
|
2604
2916
|
with_ai: { type: "boolean", description: "legacy shorthand: true \u21D2 category with_ai, false \u21D2 human. Prefer `category`." },
|
|
2605
2917
|
tool: { type: "string", description: "the AI tool to name (Assisted-by for with_ai, Authored-by-ai for ai). Default: AI." },
|
|
2606
2918
|
print: { type: "boolean", description: "return the disclosure trailer lines instead of amending HEAD \u2014 for platforms that make the commit themselves; put them in the commit message's last paragraph" },
|
|
2607
|
-
model: { type: "string", description: "the model version you are running as, e.g. claude-sonnet-5 \u2014 written as tool:model. Give it when the repo's AGENTS.md asks; omit it rather than guess." }
|
|
2919
|
+
model: { type: "string", description: "the model version you are running as, e.g. claude-sonnet-5 \u2014 written as tool:model. Give it when the repo's AGENTS.md asks; omit it rather than guess." },
|
|
2920
|
+
for: { type: "string", description: "disclose earlier commits instead of HEAD: a commit or a range (e.g. origin/dev..HEAD). Makes an empty disclosure commit; nothing is rewritten. The model is named only if you pass `model` for those commits." }
|
|
2608
2921
|
}
|
|
2609
2922
|
},
|
|
2610
2923
|
handler: (args) => {
|
|
@@ -2612,6 +2925,10 @@ var TOOLS = [
|
|
|
2612
2925
|
const tool = args.tool;
|
|
2613
2926
|
const model = args.model;
|
|
2614
2927
|
const category = args.category;
|
|
2928
|
+
if (typeof args.for === "string" && args.for) {
|
|
2929
|
+
const c = category === "ai" ? "ai" : category === "with_ai" || args.with_ai === true ? "with_ai" : "human";
|
|
2930
|
+
return discloseFor(root, args.for, { category: c, tool, model: c === "human" ? void 0 : model });
|
|
2931
|
+
}
|
|
2615
2932
|
if (args.print === true) {
|
|
2616
2933
|
const c = category === "ai" ? "ai" : category === "with_ai" || args.with_ai === true ? "with_ai" : "human";
|
|
2617
2934
|
return disclosureTrailers(root, { category: c, tool, model });
|
|
@@ -2651,8 +2968,9 @@ guesses whether code is AI. To align a repo:
|
|
|
2651
2968
|
3. **Disclose going forward** \u2014 contributors disclose origin with an \`Authored-by-ai\` trailer (an AI
|
|
2652
2969
|
wrote it; \`madeby ai\` adds it), \`Authored-by-human\` plus \`Assisted-by\` (a human wrote it with AI;
|
|
2653
2970
|
\`madeby me --with-ai\`), \`Authored-by-human\` alone (\`madeby me\`), a Co-Authored-By / Generated-by AI
|
|
2654
|
-
trailer,
|
|
2655
|
-
|
|
2971
|
+
trailer, or witnessed spans from an AI session log (\`madeby ai --witness\`). Human and AI disclosure
|
|
2972
|
+
ride the same ladder. A DCO Signed-off-by or a signed commit alone says who is accountable, not who
|
|
2973
|
+
wrote the code, so it satisfies the policy only if the policy's \`accept\` lists it.
|
|
2656
2974
|
4. **Account for existing history honestly** \u2014 do NOT fabricate disclosure for past commits. Read the
|
|
2657
2975
|
\`madeby://guide/backfill\` resource: recognize what's already disclosed, attach recoverable
|
|
2658
2976
|
evidence, and set an adoption boundary \u2014 pre-boundary history is labeled *unknown*, never invented.
|
|
@@ -2690,8 +3008,8 @@ from a PR.
|
|
|
2690
3008
|
## Azure DevOps (build validation branch policy)
|
|
2691
3009
|
|
|
2692
3010
|
Project Settings -> Repositories -> (repo) -> Policies -> Branch (main) -> Build Validation -> Add:
|
|
2693
|
-
point it at the pipeline
|
|
2694
|
-
required. CLI equivalent: az repos policy build create ... --branch main --blocking true.
|
|
3011
|
+
point it at the pipeline created from .madeby/azure-pipelines.yml (written by init with host
|
|
3012
|
+
azure-devops); set trigger automatic and required. CLI equivalent: az repos policy build create ... --branch main --blocking true.
|
|
2695
3013
|
`;
|
|
2696
3014
|
var BACKFILL_GUIDE = `# Honest backfill for an existing repo
|
|
2697
3015
|
|
|
@@ -2700,8 +3018,9 @@ refuses it. But you CAN make an honest, verifiable statement about history. Back
|
|
|
2700
3018
|
never raise a commit's origin above its evidence \u2014 unknown stays unknown, just explicitly so.
|
|
2701
3019
|
|
|
2702
3020
|
1. **Recognize what's already there** (free, pure evidence). Run \`check\` over FULL history:
|
|
2703
|
-
existing Co-Authored-By AI trailers
|
|
2704
|
-
|
|
3021
|
+
existing Co-Authored-By AI trailers and bot authorship are recognized now even though they predate
|
|
3022
|
+
adoption. Much of your history may already be disclosed. (DCO sign-offs and signatures are
|
|
3023
|
+
recognized too, but say who is accountable, not who wrote the code.)
|
|
2705
3024
|
|
|
2706
3025
|
2. **Attach recoverable evidence** (opt-in, evidence-grade):
|
|
2707
3026
|
- Old AI-tool session logs -> \`ai\` attaches structurally-verified spans to their commits (it
|
|
@@ -2732,7 +3051,7 @@ var POLICY_SCHEMA = {
|
|
|
2732
3051
|
properties: {
|
|
2733
3052
|
version: { const: 0 },
|
|
2734
3053
|
mode: { enum: ["advisory", "required"], description: "advisory: report only (the default, and the safe degrade); required: fail on any undisclosed commit" },
|
|
2735
|
-
accept: { type: "array", items: { type: "string" }, description: "restrict what satisfies the policy (e.g. dco-signoff, ai-trailer, commit-signature); omit \u21D2 any
|
|
3054
|
+
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)" },
|
|
2736
3055
|
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" }
|
|
2737
3056
|
}
|
|
2738
3057
|
};
|
|
@@ -2865,7 +3184,7 @@ Usage:
|
|
|
2865
3184
|
(alias: whom) file's origin, or the whole repo with no path. --lines=A-B to scope.
|
|
2866
3185
|
madeby coverage [<range>] The scoreboard: how much of your repo's authorship is disclosed (known)
|
|
2867
3186
|
vs unknown, the human/with_ai/ai mix, and the asserted/verified split.
|
|
2868
|
-
--json for a machine-readable result.
|
|
3187
|
+
--by author for one row per commit author. --json for a machine-readable result.
|
|
2869
3188
|
madeby ai [<log>] [<ref>] Made by AI: record your AI session's witnessed spans into .madeby/spans.
|
|
2870
3189
|
<log> defaults to the auto-discovered Claude Code transcript.
|
|
2871
3190
|
madeby me Made by me: affirm you authored HEAD yourself (an Authored-by-human
|
|
@@ -2875,6 +3194,8 @@ Usage:
|
|
|
2875
3194
|
--model <version> names the model too (Assisted-by: <tool>:<version>).
|
|
2876
3195
|
--print (me / ai) prints the trailer lines instead of amending HEAD,
|
|
2877
3196
|
for the commit message itself: git commit -m <msg> -m "$(madeby ai <tool> --print)".
|
|
3197
|
+
--for <commit|range> (me / ai) discloses EARLIER commits instead, with one
|
|
3198
|
+
new empty commit naming them (Discloses: <hash>) \u2014 nothing rewritten.
|
|
2878
3199
|
madeby mcp Run the MCP server (stdio) \u2014 the tools, agent-callable.
|
|
2879
3200
|
madeby help Show this help.
|
|
2880
3201
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "madeby",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.14",
|
|
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",
|