madeby 0.1.0
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 +182 -0
- package/dist/madeby.mjs +2095 -0
- package/package.json +53 -0
package/dist/madeby.mjs
ADDED
|
@@ -0,0 +1,2095 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// ../analyzer/src/git.ts
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
var FIELD = "";
|
|
6
|
+
var RECORD = "";
|
|
7
|
+
function readGitLog(repoPath, limit, range) {
|
|
8
|
+
try {
|
|
9
|
+
const args = [
|
|
10
|
+
"-C",
|
|
11
|
+
repoPath,
|
|
12
|
+
"log",
|
|
13
|
+
"--no-merges",
|
|
14
|
+
// merge commits aren't authored content — exclude them from attribution
|
|
15
|
+
...limit ? ["-n", String(limit)] : [],
|
|
16
|
+
// %G? is a signature-PRESENCE probe for the Disclosure Score: 'N' = unsigned; any other code
|
|
17
|
+
// (G/U/E/…) = a signature is present (E = present but unverifiable here — no keyring). We only
|
|
18
|
+
// read presence, never validity (that's the verified tier). If gpg is absent git returns 'N',
|
|
19
|
+
// which under-counts disclosure — the fail-safe direction (never a false "disclosed").
|
|
20
|
+
`--format=%H${FIELD}%an${FIELD}%ae${FIELD}%G?${FIELD}%B${RECORD}`,
|
|
21
|
+
// optional revision range, e.g. "origin/main..HEAD" — the PR's own commits for the gate
|
|
22
|
+
...range ? [range] : []
|
|
23
|
+
];
|
|
24
|
+
const out = execFileSync("git", args, { encoding: "utf8", maxBuffer: 256 * 1024 * 1024, stdio: ["ignore", "pipe", "ignore"] });
|
|
25
|
+
return out.split(RECORD).map((r) => r.replace(/^\s+/, "")).filter((r) => r.length > 0).map((rec) => {
|
|
26
|
+
const parts = rec.split(FIELD);
|
|
27
|
+
const sha = parts[0] ?? "";
|
|
28
|
+
const authorName = parts[1] ?? "";
|
|
29
|
+
const authorEmail = parts[2] ?? "";
|
|
30
|
+
const sig = parts[3] ?? "N";
|
|
31
|
+
const message = parts.slice(4).join(FIELD).trimEnd();
|
|
32
|
+
return { sha, authorName, authorEmail, signed: sig !== "N" && sig !== "", message };
|
|
33
|
+
});
|
|
34
|
+
} catch {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ../classify/src/classify.ts
|
|
40
|
+
var KNOWN_BOTS = /\b(?:dependabot|renovate(?:-bot)?|greenkeeper|snyk-bot|github-actions|semantic-release-bot|stainless-app|allcontributors|mergify|codecov-commenter|pre-commit-ci|imgbot|whitesource-bot|copybara)\b/i;
|
|
41
|
+
function isBotIdentity(name, email) {
|
|
42
|
+
const n = (name ?? "").trim();
|
|
43
|
+
const e = (email ?? "").trim();
|
|
44
|
+
if (/\[bot\]$/i.test(n) || /\[bot\]@/i.test(e)) return true;
|
|
45
|
+
if (e === "actions@github.com") return true;
|
|
46
|
+
return KNOWN_BOTS.test(n) || KNOWN_BOTS.test(e);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ../classify/src/benchmark/benchmark-v0.json
|
|
50
|
+
var benchmark_v0_default = {
|
|
51
|
+
version: "0",
|
|
52
|
+
cases: [
|
|
53
|
+
{ message: "Fix off-by-one in pagination", authorName: "Mac", authorEmail: "mac@ex.com", expected: "human" },
|
|
54
|
+
{ message: "Bump dependency versions", authorName: "Sam", authorEmail: "sam@ex.com", expected: "human" },
|
|
55
|
+
{ message: "Update README with setup steps", authorName: "Lee", authorEmail: "lee@ex.com", expected: "human" },
|
|
56
|
+
{ message: "Add unit tests for the parser", authorName: "Jo", authorEmail: "jo@ex.com", expected: "human" },
|
|
57
|
+
{ message: "Refactor config loader for clarity", authorName: "Mac", authorEmail: "mac@ex.com", expected: "human" },
|
|
58
|
+
{ message: "Add AI-provider enum to the schema", authorName: "Mac", authorEmail: "mac@ex.com", expected: "human", note: "prose mentions 'AI' but no co-author \u2014 must NOT false-positive" },
|
|
59
|
+
{ message: "Document how Claude Code integrates", authorName: "Lee", authorEmail: "lee@ex.com", expected: "human", note: "prose mentions 'Claude' in the subject, not a trailer \u2014 must NOT false-positive" },
|
|
60
|
+
{ message: "Implement billing webhook\n\nCo-authored-by: Sam Dev <sam@ex.com>", authorName: "Mac", authorEmail: "mac@ex.com", expected: "human", note: "human co-author, not AI" },
|
|
61
|
+
{ message: "Refactor registration form\n\nCo-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>", authorName: "Mac", authorEmail: "mac@ex.com", expected: "with_ai" },
|
|
62
|
+
{ message: "Add tests\n\nCo-authored-by: GitHub Copilot <copilot@github.com>", authorName: "Dev", authorEmail: "dev@ex.com", expected: "with_ai" },
|
|
63
|
+
{ message: "Generate boilerplate\n\nCo-authored-by: Cursor <cursor@cursor.com>", authorName: "Mac", authorEmail: "mac@ex.com", expected: "with_ai" },
|
|
64
|
+
{ message: "Draft API docs\n\nCo-authored-by: Gemini <gemini@google.com>", authorName: "Lee", authorEmail: "lee@ex.com", expected: "with_ai" },
|
|
65
|
+
{ message: "Implement the parser\n\nCo-authored-by: ChatGPT <assistant@openai.com>", authorName: "Jo", authorEmail: "jo@ex.com", expected: "with_ai" },
|
|
66
|
+
{ message: "Scaffold the module\n\nCo-Authored-By: Claude <noreply@anthropic.com>", authorName: "Mac", authorEmail: "mac@ex.com", expected: "with_ai" },
|
|
67
|
+
{ message: "Add export endpoint\n\nGenerated-by: aider", authorName: "Sam", authorEmail: "sam@ex.com", expected: "with_ai", note: "#78: Generated-by trailer + named AI tool \u2014 a miss before evidence-grade signals" },
|
|
68
|
+
{ message: "Refine the dashboard\n\nAssisted-by: Codeium", authorName: "Lee", authorEmail: "lee@ex.com", expected: "with_ai", note: "#78: Assisted-by trailer + Codeium" },
|
|
69
|
+
{ message: "Automated dependency bump", authorName: "Claude", authorEmail: "noreply@anthropic.com", expected: "ai" },
|
|
70
|
+
{ message: "Auto-generated migration", authorName: "Copilot", authorEmail: "copilot@github.com", expected: "ai" },
|
|
71
|
+
{ message: "Regenerate types", authorName: "Cursor Agent", authorEmail: "agent@cursor.com", expected: "ai" },
|
|
72
|
+
{ message: "Automated changelog", authorName: "Claude", authorEmail: "noreply@anthropic.com", expected: "ai" },
|
|
73
|
+
{ message: "Implement caching layer", authorName: "Mac", authorEmail: "mac@ex.com", expected: "with_ai", note: "AI-assisted in reality, trailer omitted \u2192 EXPECTED MISS (untrailered AI; addressed by span-level evidence #2/#10)" },
|
|
74
|
+
{ message: "Rewrite the serializer", authorName: "Mac", authorEmail: "mac@ex.com", expected: "with_ai", note: "untrailered AI assist \u2192 EXPECTED MISS" },
|
|
75
|
+
{ message: "Add retry logic to the client", authorName: "Lee", authorEmail: "lee@ex.com", expected: "with_ai", note: "untrailered AI assist \u2192 EXPECTED MISS" }
|
|
76
|
+
]
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// ../classify/src/eval.ts
|
|
80
|
+
var BENCHMARK_VERSION = benchmark_v0_default.version;
|
|
81
|
+
var CASES = benchmark_v0_default.cases;
|
|
82
|
+
|
|
83
|
+
// ../classify/src/benchmark/ground-truth-v0.json
|
|
84
|
+
var ground_truth_v0_default = {
|
|
85
|
+
version: "0",
|
|
86
|
+
note: "INDEPENDENT ground truth (TESTING.md \xA76, review 2026-06-29; corpus grown 2026-08-13 with REAL cited public commits). Unlike the synthetic benchmark and the dogfood canary, each label here is established by provenance OTHER than the AI trailer the classifier reads: the commit's ERA (predates LLM coding assistants), an autonomous-agent ACCOUNT, or a build the developer PUBLICLY documented as AI-generated. `untrailered: true` marks the real-world blind spot \u2014 AI the classifier fails to flag as AI-involved despite no trailer. This is now purely the no-signal class: inline autocomplete / stripped trailers / AI committed under a human's name \u2014 irrecoverable without session-log evidence (#68). (A second sub-cause once lived here \u2014 an AI-agent identity DISCARDED because its GitHub noreply email embeds '[bot]', so bot-precedence beat the AI-identity match, e.g. real Devin commits \u2014 but that was RECOVERED on 2026-08-13 by a classify.ts precedence refinement, so those cases are now `ai`/detected, not blind.) Real cited cases (repo@sha) are preferred over representative stand-ins and are replacing them over time; the label basis (never the trailer) is documented per case. The corpus grows via captured corrections (#69) and real labeled repos.",
|
|
87
|
+
cases: [
|
|
88
|
+
{
|
|
89
|
+
message: 'Initial revision of "git", the information manager from hell',
|
|
90
|
+
authorName: "Linus Torvalds",
|
|
91
|
+
authorEmail: "torvalds@ppc970.osdl.org",
|
|
92
|
+
expected: "human",
|
|
93
|
+
provenanceBasis: "the 2005 initial commit of git \u2014 predates LLM coding assistants by ~15 years"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
message: "Fix memory leak in request handler",
|
|
97
|
+
authorName: "Jane Cooper",
|
|
98
|
+
authorEmail: "jane@example.org",
|
|
99
|
+
expected: "human",
|
|
100
|
+
provenanceBasis: "representative pre-2021 OSS commit; predates mainstream AI coding tools"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
message: "Bump version to 2.4.1 and update changelog",
|
|
104
|
+
authorName: "Marco Rossi",
|
|
105
|
+
authorEmail: "marco@example.com",
|
|
106
|
+
expected: "human",
|
|
107
|
+
provenanceBasis: "representative pre-2021 release commit; no AI tooling existed"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
message: "Refactor auth middleware for clarity",
|
|
111
|
+
authorName: "Priya Nair",
|
|
112
|
+
authorEmail: "priya@example.net",
|
|
113
|
+
expected: "human",
|
|
114
|
+
provenanceBasis: "representative human OSS contribution, hand-written (no AI assistance)"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
message: "Add integration tests for the payments flow",
|
|
118
|
+
authorName: "Tom Becker",
|
|
119
|
+
authorEmail: "tom@example.com",
|
|
120
|
+
expected: "human",
|
|
121
|
+
provenanceBasis: "representative human-authored commit, no AI tooling"
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
message: "Correct typo in README installation steps",
|
|
125
|
+
authorName: "Aiko Tanaka",
|
|
126
|
+
authorEmail: "aiko@example.jp",
|
|
127
|
+
expected: "human",
|
|
128
|
+
provenanceBasis: "representative human-authored docs fix"
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
message: "Add an AI-provider field to the schema",
|
|
132
|
+
authorName: "Sam Reed",
|
|
133
|
+
authorEmail: "sam@example.com",
|
|
134
|
+
expected: "human",
|
|
135
|
+
provenanceBasis: "human-authored; message MENTIONS 'AI' but there is no AI co-author \u2014 must not false-positive"
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
message: "Document how Claude Code integrates with our CI",
|
|
139
|
+
authorName: "Dana Liu",
|
|
140
|
+
authorEmail: "dana@example.com",
|
|
141
|
+
expected: "human",
|
|
142
|
+
provenanceBasis: "human-authored docs; 'Claude' appears in prose only \u2014 must not false-positive"
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
message: "Implement webhook retries\n\nCo-authored-by: Pat Singh <pat@example.com>",
|
|
146
|
+
authorName: "Sam Reed",
|
|
147
|
+
authorEmail: "sam@example.com",
|
|
148
|
+
expected: "human",
|
|
149
|
+
provenanceBasis: "human co-author (not an AI) \u2014 must not false-positive"
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
message: "Refactor login form\n\nCo-Authored-By: Claude <noreply@anthropic.com>",
|
|
153
|
+
authorName: "Mac",
|
|
154
|
+
authorEmail: "mac@example.com",
|
|
155
|
+
expected: "with_ai",
|
|
156
|
+
provenanceBasis: "carries an explicit AI co-author trailer \u2014 the trailered case the classifier should catch",
|
|
157
|
+
untrailered: false
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
message: "Generate API client\n\nCo-authored-by: GitHub Copilot <copilot@github.com>",
|
|
161
|
+
authorName: "Dev",
|
|
162
|
+
authorEmail: "dev@example.com",
|
|
163
|
+
expected: "with_ai",
|
|
164
|
+
provenanceBasis: "explicit Copilot trailer \u2014 trailered case",
|
|
165
|
+
untrailered: false
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
message: "Automated dependency upgrade",
|
|
169
|
+
authorName: "Devin",
|
|
170
|
+
authorEmail: "devin@cognition.ai",
|
|
171
|
+
expected: "ai",
|
|
172
|
+
provenanceBasis: "authored by the Devin autonomous-agent account (no human co-author) \u2014 agent-authored",
|
|
173
|
+
untrailered: false
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
message: "Regenerate API client and types",
|
|
177
|
+
authorName: "Cursor Agent",
|
|
178
|
+
authorEmail: "agent@cursor.com",
|
|
179
|
+
expected: "ai",
|
|
180
|
+
provenanceBasis: "authored by the Cursor agent account \u2014 agent-authored (evidence-grade identity, #78)",
|
|
181
|
+
untrailered: false
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
message: "Add pagination to the list endpoint\n\nGenerated-by: aider",
|
|
185
|
+
authorName: "Devon Park",
|
|
186
|
+
authorEmail: "devon@example.com",
|
|
187
|
+
expected: "with_ai",
|
|
188
|
+
provenanceBasis: "explicit Generated-by trailer naming the aider AI tool (#78) \u2014 identifiable, not untrailered",
|
|
189
|
+
untrailered: false
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
message: "Add dark mode toggle to settings",
|
|
193
|
+
authorName: "Alex Kim",
|
|
194
|
+
authorEmail: "alex@example.com",
|
|
195
|
+
expected: "with_ai",
|
|
196
|
+
provenanceBasis: "developer publicly documented this feature as generated by an AI assistant; commit carries NO trailer (the real-world blind spot)",
|
|
197
|
+
untrailered: true
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
message: "Implement CSV export endpoint",
|
|
201
|
+
authorName: "Jordan Vega",
|
|
202
|
+
authorEmail: "jordan@example.com",
|
|
203
|
+
expected: "with_ai",
|
|
204
|
+
provenanceBasis: "AI-generated via inline autocomplete (accepted + committed under the human's name); no trailer emitted",
|
|
205
|
+
untrailered: true
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
message: "Add rate limiting to the public API",
|
|
209
|
+
authorName: "Chris Obi",
|
|
210
|
+
authorEmail: "chris@example.com",
|
|
211
|
+
expected: "with_ai",
|
|
212
|
+
provenanceBasis: "built with an AI coding agent the developer credited in a blog post; commit has no trailer",
|
|
213
|
+
untrailered: true
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
message: "Wire up Stripe checkout",
|
|
217
|
+
authorName: "Lena Vogt",
|
|
218
|
+
authorEmail: "lena@example.de",
|
|
219
|
+
expected: "with_ai",
|
|
220
|
+
provenanceBasis: "vibe-coded with an AI IDE assistant; trailer stripped on squash-merge",
|
|
221
|
+
untrailered: true
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
message: "Scaffold the admin dashboard",
|
|
225
|
+
authorName: "Omar Haddad",
|
|
226
|
+
authorEmail: "omar@example.com",
|
|
227
|
+
expected: "with_ai",
|
|
228
|
+
provenanceBasis: "scaffold generated by an AI tool, documented in the PR description; no co-author trailer",
|
|
229
|
+
untrailered: true
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
message: "Add unit tests for the parser",
|
|
233
|
+
authorName: "Sofia Marin",
|
|
234
|
+
authorEmail: "sofia@example.com",
|
|
235
|
+
expected: "with_ai",
|
|
236
|
+
provenanceBasis: "tests generated by an AI assistant (tab-completion); committed under the human's name, no trailer",
|
|
237
|
+
untrailered: true
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
message: "zmalloc fix, return NULL or real malloc failure",
|
|
241
|
+
authorName: "antirez",
|
|
242
|
+
authorEmail: "antirez@gmail.com",
|
|
243
|
+
expected: "human",
|
|
244
|
+
provenanceBasis: "REAL cited commit: redis@8d196ebac255 (2009-04-30, Salvatore Sanfilippo) \u2014 predates LLM coding assistants; verifiable public history"
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
message: "fix scaffolding template paths (Pathname does weird things)",
|
|
248
|
+
authorName: "Leon Breedt",
|
|
249
|
+
authorEmail: "bitserf@gmail.com",
|
|
250
|
+
expected: "human",
|
|
251
|
+
provenanceBasis: "REAL cited commit: rails/rails@46e68538c08d (2005-05-31) \u2014 pre-LLM era; verifiable public history"
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
message: "Fixed #6820: flush no longer fails under PostgreSQL 8.3",
|
|
255
|
+
authorName: "Jacob Kaplan-Moss",
|
|
256
|
+
authorEmail: "jacob@jacobian.org",
|
|
257
|
+
expected: "human",
|
|
258
|
+
provenanceBasis: "REAL cited commit: django/django@3de70a10c066 (2008-05-31) \u2014 pre-LLM era; verifiable public history"
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
message: "not needed",
|
|
262
|
+
authorName: "Blake Mizerany",
|
|
263
|
+
authorEmail: "blakemizerany@blake-mizeranys-computer.local",
|
|
264
|
+
expected: "human",
|
|
265
|
+
provenanceBasis: "REAL cited commit: sinatra/sinatra@95287a1b5514 (2007-12-03) \u2014 pre-LLM era; verifiable public history"
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
message: "Fix magic-link sign-in: token_hash verify and confirm step\n\nCo-authored-by: ksg-spidy <ksg-spidy@users.noreply.github.com>",
|
|
269
|
+
authorName: "Cursor Agent",
|
|
270
|
+
authorEmail: "cursoragent@cursor.com",
|
|
271
|
+
expected: "with_ai",
|
|
272
|
+
provenanceBasis: "REAL cited commit: ksg-spidy/Bookmyslot@7a140859 \u2014 authored by the Cursor agent account with a human co-author trailer; agent-authored (identity is evidence-grade, #78). DETECTED via author identity.",
|
|
273
|
+
untrailered: false
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
message: "Add live DSC/HD95 metrics panel for full-3d-mask guidance",
|
|
277
|
+
authorName: "Copilot",
|
|
278
|
+
authorEmail: "198982749+Copilot@users.noreply.github.com",
|
|
279
|
+
expected: "ai",
|
|
280
|
+
provenanceBasis: "REAL cited commit: LMUK-RADONC-PHYS-RES/beacon@492668ba \u2014 authored by GitHub's Copilot coding agent, NO trailer of any kind. DETECTED anyway because the agent is the git AUTHOR (its identity is the signal) \u2014 proof that untrailered AI is not always invisible when the agent authors directly.",
|
|
281
|
+
untrailered: false
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
message: "feat(phase0): scaffold backend, dashboard, nginx, docker compose",
|
|
285
|
+
authorName: "Devin AI",
|
|
286
|
+
authorEmail: "158243242+devin-ai-integration[bot]@users.noreply.github.com",
|
|
287
|
+
expected: "ai",
|
|
288
|
+
provenanceBasis: "REAL cited commit: Moksha89/trade@627b2b5d \u2014 authored by the Devin autonomous-agent account, NO trailer; the GitHub noreply email embeds '[bot]'. DETECTED via the disclosed agent identity ('devin' is a known AI pattern): a [bot] identity that is itself a named AI agent classifies 'ai', not generic 'bot'. Previously lost to bot-precedence \u2014 recovered by the classify.ts precedence refinement (2026-08-13). The old representative Devin case (devin@cognition.ai) never exercised the [bot]-email path this real commit does.",
|
|
289
|
+
untrailered: false
|
|
290
|
+
}
|
|
291
|
+
]
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// ../classify/src/ground-truth.ts
|
|
295
|
+
var GROUND_TRUTH_VERSION = ground_truth_v0_default.version;
|
|
296
|
+
var CASES2 = ground_truth_v0_default.cases;
|
|
297
|
+
|
|
298
|
+
// ../core/src/canonicalize.ts
|
|
299
|
+
function compact(obj) {
|
|
300
|
+
const out = {};
|
|
301
|
+
for (const [k, val] of Object.entries(obj)) {
|
|
302
|
+
if (val !== void 0) out[k] = val;
|
|
303
|
+
}
|
|
304
|
+
return out;
|
|
305
|
+
}
|
|
306
|
+
function claimPayload(claim) {
|
|
307
|
+
return compact({
|
|
308
|
+
subject: {
|
|
309
|
+
algorithm: claim.subject.algorithm,
|
|
310
|
+
target: claim.subject.target,
|
|
311
|
+
value: claim.subject.value
|
|
312
|
+
},
|
|
313
|
+
attribution: compact({
|
|
314
|
+
identityId: claim.attribution.identityId,
|
|
315
|
+
role: claim.attribution.role,
|
|
316
|
+
aiProvider: claim.attribution.aiProvider,
|
|
317
|
+
aiModel: claim.attribution.aiModel,
|
|
318
|
+
operatorId: claim.attribution.operatorId
|
|
319
|
+
}),
|
|
320
|
+
assertedTier: claim.assertedTier,
|
|
321
|
+
sworn: claim.sworn ? {
|
|
322
|
+
representationCode: claim.sworn.representationCode,
|
|
323
|
+
signatureName: claim.sworn.signatureName,
|
|
324
|
+
signedAt: claim.sworn.signedAt
|
|
325
|
+
} : void 0,
|
|
326
|
+
createdAt: claim.createdAt
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ../core/src/carriers.ts
|
|
331
|
+
function sigToJson(sig) {
|
|
332
|
+
return sig ? { signerKeyId: sig.signerKeyId, value: sig.value, carrierId: sig.carrierId, signedAt: sig.signedAt } : void 0;
|
|
333
|
+
}
|
|
334
|
+
function sigFromJson(v) {
|
|
335
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) return void 0;
|
|
336
|
+
const s = v;
|
|
337
|
+
return { signerKeyId: s.signerKeyId, value: s.value, carrierId: s.carrierId, signedAt: s.signedAt };
|
|
338
|
+
}
|
|
339
|
+
var PREDICATE_TYPE = "https://madeby.fyi/attestation/v0";
|
|
340
|
+
var inToto = {
|
|
341
|
+
id: "in-toto",
|
|
342
|
+
verificationCapable: true,
|
|
343
|
+
serialize(claim, signature) {
|
|
344
|
+
const payload = claimPayload(claim);
|
|
345
|
+
return {
|
|
346
|
+
carrier: "in-toto",
|
|
347
|
+
_type: "https://in-toto.io/Statement/v1",
|
|
348
|
+
predicateType: PREDICATE_TYPE,
|
|
349
|
+
subject: [{ digest: { [claim.subject.algorithm]: claim.subject.value } }],
|
|
350
|
+
predicate: payload,
|
|
351
|
+
...sigToJson(signature) ? { signatures: [sigToJson(signature)] } : {}
|
|
352
|
+
};
|
|
353
|
+
},
|
|
354
|
+
parse(env) {
|
|
355
|
+
const sigs = env.signatures;
|
|
356
|
+
return { payload: env.predicate, signature: sigFromJson(sigs?.[0]) };
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
var gitNotes = {
|
|
360
|
+
id: "git-notes",
|
|
361
|
+
verificationCapable: true,
|
|
362
|
+
serialize(claim, signature) {
|
|
363
|
+
return {
|
|
364
|
+
carrier: "git-notes",
|
|
365
|
+
madeby: "v0",
|
|
366
|
+
claim: claimPayload(claim),
|
|
367
|
+
...sigToJson(signature) ? { sig: sigToJson(signature) } : {}
|
|
368
|
+
};
|
|
369
|
+
},
|
|
370
|
+
parse(env) {
|
|
371
|
+
return { payload: env.claim, signature: sigFromJson(env.sig) };
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
var spdx = {
|
|
375
|
+
id: "spdx",
|
|
376
|
+
verificationCapable: true,
|
|
377
|
+
serialize(claim, signature) {
|
|
378
|
+
return {
|
|
379
|
+
carrier: "spdx",
|
|
380
|
+
spdxVersion: "SPDX-3.0",
|
|
381
|
+
profile: "ai",
|
|
382
|
+
madebyPayload: claimPayload(claim),
|
|
383
|
+
...sigToJson(signature) ? { madebySignature: sigToJson(signature) } : {}
|
|
384
|
+
};
|
|
385
|
+
},
|
|
386
|
+
parse(env) {
|
|
387
|
+
return { payload: env.madebyPayload, signature: sigFromJson(env.madebySignature) };
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
var CARRIER_REGISTRY = /* @__PURE__ */ new Map([
|
|
391
|
+
[inToto.id, inToto],
|
|
392
|
+
[gitNotes.id, gitNotes],
|
|
393
|
+
[spdx.id, spdx]
|
|
394
|
+
]);
|
|
395
|
+
|
|
396
|
+
// ../core/src/edges.ts
|
|
397
|
+
var EDGE_TYPES = new Map(
|
|
398
|
+
[
|
|
399
|
+
{ id: "PART_OF", category: "composition", description: "A is contained in B (span\u2282file\u2282commit\u2282release); attribution rolls up." },
|
|
400
|
+
{ id: "DERIVED_FROM", category: "derivation", description: "A is derived from B (fork/refactor/snippet/AI-generated-by-model)." },
|
|
401
|
+
{ id: "DISPUTES", category: "contradiction", description: "A contests B's claim (\xA711) \u2014 a counter-claim, ranked like any claim." },
|
|
402
|
+
{ id: "IMPLEMENTS", category: "relation", description: "Code A implements artifact B (ticket/spec)." },
|
|
403
|
+
{ id: "MOTIVATED_BY", category: "relation", description: "A exists because of artifact B (the why)." },
|
|
404
|
+
{ id: "DECIDED_BY", category: "relation", description: "A is governed by decision record B (ADR/RFC)." },
|
|
405
|
+
{ id: "DISCUSSED_IN", category: "relation", description: "A was discussed in B (issue/thread)." }
|
|
406
|
+
].map((d) => [d.id, d])
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
// ../core/src/manifest.ts
|
|
410
|
+
var SPAN_MANIFEST_VERSION = "0";
|
|
411
|
+
var MADEBY_SPANS_TAG = `spans/${SPAN_MANIFEST_VERSION}`;
|
|
412
|
+
var fingerprintToString = (fp) => `${fp.algorithm}:${fp.target}:${fp.value}`;
|
|
413
|
+
function fingerprintFromString(s) {
|
|
414
|
+
const i1 = s.indexOf(":");
|
|
415
|
+
const i2 = s.indexOf(":", i1 + 1);
|
|
416
|
+
if (i1 < 1 || i2 <= i1) return null;
|
|
417
|
+
const value = s.slice(i2 + 1);
|
|
418
|
+
if (!value) return null;
|
|
419
|
+
return { algorithm: s.slice(0, i1), target: s.slice(i1 + 1, i2), value };
|
|
420
|
+
}
|
|
421
|
+
function linesToString(hint) {
|
|
422
|
+
if (!hint || hint.startLine === void 0) return void 0;
|
|
423
|
+
return hint.endLine !== void 0 && hint.endLine !== hint.startLine ? `${hint.startLine}-${hint.endLine}` : `${hint.startLine}`;
|
|
424
|
+
}
|
|
425
|
+
function linesFromString(v) {
|
|
426
|
+
if (typeof v !== "string") return {};
|
|
427
|
+
const m = /^(\d+)(?:-(\d+))?$/.exec(v);
|
|
428
|
+
if (!m) return {};
|
|
429
|
+
const startLine = Number(m[1]);
|
|
430
|
+
return m[2] ? { startLine, endLine: Number(m[2]) } : { startLine };
|
|
431
|
+
}
|
|
432
|
+
function spanToFlat(a) {
|
|
433
|
+
const o = {};
|
|
434
|
+
if (a.anchor.hint?.path) o.file = a.anchor.hint.path;
|
|
435
|
+
const lines = linesToString(a.anchor.hint);
|
|
436
|
+
if (lines) o.lines = lines;
|
|
437
|
+
o.model = a.attribution.model;
|
|
438
|
+
o.provider = a.attribution.provider;
|
|
439
|
+
if (a.attribution.modelVersion !== void 0) o.modelVersion = a.attribution.modelVersion;
|
|
440
|
+
o.source = a.attribution.source;
|
|
441
|
+
o.operator = a.attribution.operatorId;
|
|
442
|
+
o.fingerprint = fingerprintToString(a.anchor.fingerprint);
|
|
443
|
+
return o;
|
|
444
|
+
}
|
|
445
|
+
function serializeSpanManifest(m) {
|
|
446
|
+
const flat = {
|
|
447
|
+
madeby: MADEBY_SPANS_TAG,
|
|
448
|
+
commit: m.commit,
|
|
449
|
+
recorded: m.generatedAt,
|
|
450
|
+
spans: m.attestations.map(spanToFlat)
|
|
451
|
+
};
|
|
452
|
+
if (m.signature) flat.signature = m.signature;
|
|
453
|
+
return JSON.stringify(flat, null, 2) + "\n";
|
|
454
|
+
}
|
|
455
|
+
function parseSpanManifest(text) {
|
|
456
|
+
const raw = JSON.parse(text);
|
|
457
|
+
if (typeof raw.madeby !== "string" || !raw.madeby.startsWith("spans/")) {
|
|
458
|
+
throw new Error("span manifest: not a madeby spans file (expected 'madeby': 'spans/N')");
|
|
459
|
+
}
|
|
460
|
+
const version = raw.madeby.slice("spans/".length);
|
|
461
|
+
if (typeof raw.commit !== "string" || raw.commit.length === 0) throw new Error("span manifest: missing commit");
|
|
462
|
+
if (typeof raw.recorded !== "string") throw new Error("span manifest: missing recorded");
|
|
463
|
+
if (!Array.isArray(raw.spans)) throw new Error("span manifest: spans must be an array");
|
|
464
|
+
const attestations = raw.spans.map((s, i) => {
|
|
465
|
+
const o = s;
|
|
466
|
+
const fp = typeof o.fingerprint === "string" ? fingerprintFromString(o.fingerprint) : null;
|
|
467
|
+
if (!fp) throw new Error(`span manifest: span ${i} has a malformed fingerprint`);
|
|
468
|
+
if (typeof o.model !== "string" || typeof o.provider !== "string" || typeof o.operator !== "string" || typeof o.source !== "string") {
|
|
469
|
+
throw new Error(`span manifest: span ${i} has a malformed attribution`);
|
|
470
|
+
}
|
|
471
|
+
const anchor = {
|
|
472
|
+
fingerprint: fp,
|
|
473
|
+
...typeof o.file === "string" ? { hint: { path: o.file, ...linesFromString(o.lines) } } : {}
|
|
474
|
+
};
|
|
475
|
+
const attribution = {
|
|
476
|
+
provider: o.provider,
|
|
477
|
+
model: o.model,
|
|
478
|
+
operatorId: o.operator,
|
|
479
|
+
source: o.source,
|
|
480
|
+
...typeof o.modelVersion === "string" ? { modelVersion: o.modelVersion } : {}
|
|
481
|
+
};
|
|
482
|
+
return { anchor, attribution };
|
|
483
|
+
});
|
|
484
|
+
return {
|
|
485
|
+
version,
|
|
486
|
+
commit: raw.commit,
|
|
487
|
+
generatedAt: raw.recorded,
|
|
488
|
+
attestations,
|
|
489
|
+
...raw.signature ? { signature: raw.signature } : {}
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ../core/src/fingerprint.ts
|
|
494
|
+
var enc = new TextEncoder();
|
|
495
|
+
|
|
496
|
+
// ../core/src/private-fingerprint.ts
|
|
497
|
+
var enc2 = new TextEncoder();
|
|
498
|
+
|
|
499
|
+
// ../core/src/structural.ts
|
|
500
|
+
var DIM = 256;
|
|
501
|
+
var K = 5;
|
|
502
|
+
var WINDOW = 4;
|
|
503
|
+
function normalizeSource(src) {
|
|
504
|
+
return src.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\n]*/g, " ").replace(/#[^\n]*/g, " ").replace(/\s+/g, " ").trim();
|
|
505
|
+
}
|
|
506
|
+
function tokenize(s) {
|
|
507
|
+
return s.match(/[A-Za-z_]\w*|\d+|[^\sA-Za-z0-9_]/g) ?? [];
|
|
508
|
+
}
|
|
509
|
+
function canonicalizeTokens(tokens) {
|
|
510
|
+
return tokens.map((t) => /^[A-Za-z_]\w*$/.test(t) ? "ID" : /^\d+$/.test(t) ? "NUM" : t);
|
|
511
|
+
}
|
|
512
|
+
function fnv1a(str) {
|
|
513
|
+
let h = 2166136261;
|
|
514
|
+
for (let i = 0; i < str.length; i++) {
|
|
515
|
+
h ^= str.charCodeAt(i);
|
|
516
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
517
|
+
}
|
|
518
|
+
return h >>> 0;
|
|
519
|
+
}
|
|
520
|
+
function kgramHashes(tokens) {
|
|
521
|
+
if (tokens.length < K) return tokens.length ? [fnv1a(tokens.join("\0"))] : [];
|
|
522
|
+
const out = [];
|
|
523
|
+
for (let i = 0; i + K <= tokens.length; i++) out.push(fnv1a(tokens.slice(i, i + K).join("\0")));
|
|
524
|
+
return out;
|
|
525
|
+
}
|
|
526
|
+
function winnow(hashes, w = WINDOW) {
|
|
527
|
+
if (hashes.length <= w) return [...new Set(hashes)];
|
|
528
|
+
const selected = /* @__PURE__ */ new Set();
|
|
529
|
+
for (let i = 0; i + w <= hashes.length; i++) {
|
|
530
|
+
let min = hashes[i];
|
|
531
|
+
let idx = i;
|
|
532
|
+
for (let j = i; j < i + w; j++) {
|
|
533
|
+
if (hashes[j] <= min) {
|
|
534
|
+
min = hashes[j];
|
|
535
|
+
idx = j;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
selected.add(hashes[idx]);
|
|
539
|
+
}
|
|
540
|
+
return [...selected];
|
|
541
|
+
}
|
|
542
|
+
function structuralFingerprint(source, target = "FILE") {
|
|
543
|
+
const tokens = canonicalizeTokens(tokenize(normalizeSource(source)));
|
|
544
|
+
const fps = winnow(kgramHashes(tokens));
|
|
545
|
+
const embedding = new Array(DIM).fill(0);
|
|
546
|
+
for (const fp of fps) embedding[fp % DIM] += 1;
|
|
547
|
+
const norm = Math.sqrt(embedding.reduce((s, x) => s + x * x, 0)) || 1;
|
|
548
|
+
for (let i = 0; i < DIM; i++) embedding[i] = embedding[i] / norm;
|
|
549
|
+
const value = fnv1a([...fps].sort((a, b) => a - b).join(",")).toString(16).padStart(8, "0");
|
|
550
|
+
return { fingerprint: { algorithm: "structural-v1", target, value }, embedding };
|
|
551
|
+
}
|
|
552
|
+
function cosineSimilarity(a, b) {
|
|
553
|
+
let dot = 0;
|
|
554
|
+
const n = Math.min(a.length, b.length);
|
|
555
|
+
for (let i = 0; i < n; i++) dot += a[i] * b[i];
|
|
556
|
+
return dot;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// ../core/src/vectors/conformance-v0.json
|
|
560
|
+
var conformance_v0_default = {
|
|
561
|
+
version: "0",
|
|
562
|
+
jcs: [
|
|
563
|
+
{ name: "sorts object keys", input: { b: 1, a: 2 }, expected: '{"a":2,"b":1}' },
|
|
564
|
+
{
|
|
565
|
+
name: "nested objects + arrays, order-independent",
|
|
566
|
+
input: { a: [1, true, null], b: { d: 4, c: 3 } },
|
|
567
|
+
expected: '{"a":[1,true,null],"b":{"c":3,"d":4}}'
|
|
568
|
+
},
|
|
569
|
+
{ name: "ADVERSARIAL: non-integer number must error", input: 1.5, expectError: true }
|
|
570
|
+
],
|
|
571
|
+
canonicalize: [
|
|
572
|
+
{
|
|
573
|
+
name: "claim payload excludes id and signature",
|
|
574
|
+
claim: {
|
|
575
|
+
id: "ignored",
|
|
576
|
+
subject: { algorithm: "sha256", target: "FILE", value: "abc" },
|
|
577
|
+
attribution: { identityId: "alice", role: "creator" },
|
|
578
|
+
assertedTier: "asserted",
|
|
579
|
+
signature: { signerKeyId: "k", value: "v", carrierId: "in-toto", signedAt: "2026-01-01T00:00:00Z" },
|
|
580
|
+
createdAt: "2026-01-01T00:00:00Z"
|
|
581
|
+
},
|
|
582
|
+
expected: '{"assertedTier":"asserted","attribution":{"identityId":"alice","role":"creator"},"createdAt":"2026-01-01T00:00:00Z","subject":{"algorithm":"sha256","target":"FILE","value":"abc"}}'
|
|
583
|
+
}
|
|
584
|
+
],
|
|
585
|
+
fingerprint: [
|
|
586
|
+
{
|
|
587
|
+
name: "git-blob-sha1 of 'hello\\n' (== git hash-object)",
|
|
588
|
+
algorithm: "git-blob-sha1",
|
|
589
|
+
content: "hello\n",
|
|
590
|
+
expected: "ce013625030ba8dba906f756967f9e9ca394464a"
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
name: "sha256 of 'abc'",
|
|
594
|
+
algorithm: "sha256",
|
|
595
|
+
content: "abc",
|
|
596
|
+
expected: "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
|
|
597
|
+
}
|
|
598
|
+
],
|
|
599
|
+
tierResolution: [
|
|
600
|
+
{
|
|
601
|
+
name: "asserted is the floor",
|
|
602
|
+
claim: { id: "c", subject: { algorithm: "sha256", target: "FILE", value: "x" }, attribution: { identityId: "a", role: "creator" }, assertedTier: "asserted", createdAt: "2026-01-01T00:00:00Z" },
|
|
603
|
+
env: { knownCarriers: ["in-toto"], verifySignature: true, signerVerified: true },
|
|
604
|
+
expected: "asserted"
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
name: "verified: valid sig in known carrier + verified signer",
|
|
608
|
+
claim: { id: "c", subject: { algorithm: "sha256", target: "FILE", value: "x" }, attribution: { identityId: "a", role: "creator" }, assertedTier: "verified", signature: { signerKeyId: "k", value: "v", carrierId: "in-toto", signedAt: "2026-01-01T00:00:00Z" }, createdAt: "2026-01-01T00:00:00Z" },
|
|
609
|
+
env: { knownCarriers: ["in-toto"], verifySignature: true, signerVerified: true },
|
|
610
|
+
expected: "verified"
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
name: "bound: verified + exact fingerprint",
|
|
614
|
+
claim: { id: "c", subject: { algorithm: "sha256", target: "FILE", value: "x" }, attribution: { identityId: "a", role: "creator" }, assertedTier: "bound", signature: { signerKeyId: "k", value: "v", carrierId: "in-toto", signedAt: "2026-01-01T00:00:00Z" }, createdAt: "2026-01-01T00:00:00Z" },
|
|
615
|
+
env: { knownCarriers: ["in-toto"], verifySignature: true, signerVerified: true },
|
|
616
|
+
expected: "bound"
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
name: "ADVERSARIAL: unknown carrier caps at asserted (even for bound)",
|
|
620
|
+
claim: { id: "c", subject: { algorithm: "sha256", target: "FILE", value: "x" }, attribution: { identityId: "a", role: "creator" }, assertedTier: "bound", signature: { signerKeyId: "k", value: "v", carrierId: "mystery", signedAt: "2026-01-01T00:00:00Z" }, createdAt: "2026-01-01T00:00:00Z" },
|
|
621
|
+
env: { knownCarriers: ["in-toto"], verifySignature: true, signerVerified: true },
|
|
622
|
+
expected: "asserted"
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
name: "ADVERSARIAL: forged/invalid signature caps at asserted",
|
|
626
|
+
claim: { id: "c", subject: { algorithm: "sha256", target: "FILE", value: "x" }, attribution: { identityId: "a", role: "creator" }, assertedTier: "bound", signature: { signerKeyId: "k", value: "v", carrierId: "in-toto", signedAt: "2026-01-01T00:00:00Z" }, createdAt: "2026-01-01T00:00:00Z" },
|
|
627
|
+
env: { knownCarriers: ["in-toto"], verifySignature: false, signerVerified: true },
|
|
628
|
+
expected: "asserted"
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
name: "ADVERSARIAL: anonymous (unverified) signer caps at asserted",
|
|
632
|
+
claim: { id: "c", subject: { algorithm: "sha256", target: "FILE", value: "x" }, attribution: { identityId: "a", role: "creator" }, assertedTier: "verified", signature: { signerKeyId: "k", value: "v", carrierId: "in-toto", signedAt: "2026-01-01T00:00:00Z" }, createdAt: "2026-01-01T00:00:00Z" },
|
|
633
|
+
env: { knownCarriers: ["in-toto"], verifySignature: true, signerVerified: false },
|
|
634
|
+
expected: "asserted"
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
name: "bound over a fuzzy fingerprint degrades to verified",
|
|
638
|
+
claim: { id: "c", subject: { algorithm: "structural-v1", target: "FILE", value: "x" }, attribution: { identityId: "a", role: "creator" }, assertedTier: "bound", signature: { signerKeyId: "k", value: "v", carrierId: "in-toto", signedAt: "2026-01-01T00:00:00Z" }, createdAt: "2026-01-01T00:00:00Z" },
|
|
639
|
+
env: { knownCarriers: ["in-toto"], verifySignature: true, signerVerified: true },
|
|
640
|
+
expected: "verified"
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
name: "sworn without an overlay degrades to asserted",
|
|
644
|
+
claim: { id: "c", subject: { algorithm: "sha256", target: "FILE", value: "x" }, attribution: { identityId: "a", role: "creator" }, assertedTier: "sworn", createdAt: "2026-01-01T00:00:00Z" },
|
|
645
|
+
env: { knownCarriers: ["in-toto"], verifySignature: true, signerVerified: true },
|
|
646
|
+
expected: "asserted"
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
name: "ADVERSARIAL: verified/bound with no signature caps at asserted",
|
|
650
|
+
claim: { id: "c", subject: { algorithm: "sha256", target: "FILE", value: "x" }, attribution: { identityId: "a", role: "creator" }, assertedTier: "bound", createdAt: "2026-01-01T00:00:00Z" },
|
|
651
|
+
env: { knownCarriers: ["in-toto"], verifySignature: true, signerVerified: true },
|
|
652
|
+
expected: "asserted"
|
|
653
|
+
}
|
|
654
|
+
],
|
|
655
|
+
edgeTierResolution: [
|
|
656
|
+
{
|
|
657
|
+
name: "declared + valid sig + verified signer + known type \u2192 verified (the cap)",
|
|
658
|
+
edge: { type: "DERIVED_FROM", fromClaimId: "a", toClaimId: "b", method: "declared", signature: { signerKeyId: "k", value: "v", carrierId: "in-toto", signedAt: "2026-01-01T00:00:00Z" } },
|
|
659
|
+
env: { knownEdgeTypes: ["PART_OF", "DERIVED_FROM", "DISPUTES", "IMPLEMENTS", "MOTIVATED_BY", "DECIDED_BY", "DISCUSSED_IN"], verifySignature: true, signerVerified: true },
|
|
660
|
+
expected: "verified"
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
name: "inferred edge caps at asserted (even signed + known)",
|
|
664
|
+
edge: { type: "IMPLEMENTS", fromClaimId: "a", toClaimId: "b", method: "inferred", signature: { signerKeyId: "k", value: "v", carrierId: "in-toto", signedAt: "2026-01-01T00:00:00Z" } },
|
|
665
|
+
env: { knownEdgeTypes: ["IMPLEMENTS"], verifySignature: true, signerVerified: true },
|
|
666
|
+
expected: "asserted"
|
|
667
|
+
},
|
|
668
|
+
{
|
|
669
|
+
name: "ADVERSARIAL: unrecognized edge type caps at asserted",
|
|
670
|
+
edge: { type: "TOTALLY_MADE_UP", fromClaimId: "a", toClaimId: "b", method: "declared", signature: { signerKeyId: "k", value: "v", carrierId: "in-toto", signedAt: "2026-01-01T00:00:00Z" } },
|
|
671
|
+
env: { knownEdgeTypes: ["PART_OF", "DERIVED_FROM"], verifySignature: true, signerVerified: true },
|
|
672
|
+
expected: "asserted"
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
name: "ADVERSARIAL: declared but unsigned edge caps at asserted",
|
|
676
|
+
edge: { type: "PART_OF", fromClaimId: "a", toClaimId: "b", method: "declared" },
|
|
677
|
+
env: { knownEdgeTypes: ["PART_OF"], verifySignature: true, signerVerified: true },
|
|
678
|
+
expected: "asserted"
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
name: "ADVERSARIAL: declared + forged signature caps at asserted",
|
|
682
|
+
edge: { type: "PART_OF", fromClaimId: "a", toClaimId: "b", method: "declared", signature: { signerKeyId: "k", value: "v", carrierId: "in-toto", signedAt: "2026-01-01T00:00:00Z" } },
|
|
683
|
+
env: { knownEdgeTypes: ["PART_OF"], verifySignature: false, signerVerified: true },
|
|
684
|
+
expected: "asserted"
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
name: "ADVERSARIAL: declared + anonymous signer caps at asserted",
|
|
688
|
+
edge: { type: "PART_OF", fromClaimId: "a", toClaimId: "b", method: "declared", signature: { signerKeyId: "k", value: "v", carrierId: "in-toto", signedAt: "2026-01-01T00:00:00Z" } },
|
|
689
|
+
env: { knownEdgeTypes: ["PART_OF"], verifySignature: true, signerVerified: false },
|
|
690
|
+
expected: "asserted"
|
|
691
|
+
}
|
|
692
|
+
],
|
|
693
|
+
contention: [
|
|
694
|
+
{
|
|
695
|
+
name: "single claim is the uncontested origin",
|
|
696
|
+
claims: [
|
|
697
|
+
{ id: "c", subject: { algorithm: "sha256", target: "FILE", value: "x" }, attribution: { identityId: "a", role: "creator" }, assertedTier: "asserted", createdAt: "2026-01-01T00:00:00Z" }
|
|
698
|
+
],
|
|
699
|
+
env: { knownCarriers: ["in-toto"], verifySignature: true, signerVerified: true },
|
|
700
|
+
expected: { open: false, originIndex: 0, divergence: false }
|
|
701
|
+
},
|
|
702
|
+
{
|
|
703
|
+
name: "verified owner outranks an asserted squatter (index 1 wins)",
|
|
704
|
+
claims: [
|
|
705
|
+
{ id: "sq", subject: { algorithm: "sha256", target: "FILE", value: "x" }, attribution: { identityId: "sq", role: "creator" }, assertedTier: "asserted", createdAt: "2026-01-01T00:00:00Z" },
|
|
706
|
+
{ id: "own", subject: { algorithm: "sha256", target: "FILE", value: "x" }, attribution: { identityId: "own", role: "creator" }, assertedTier: "verified", signature: { signerKeyId: "k", value: "v", carrierId: "in-toto", signedAt: "2026-01-01T00:00:00Z" }, createdAt: "2026-02-01T00:00:00Z" }
|
|
707
|
+
],
|
|
708
|
+
env: { knownCarriers: ["in-toto"], verifySignature: true, signerVerified: true },
|
|
709
|
+
expected: { open: false, originIndex: 1, divergence: false }
|
|
710
|
+
},
|
|
711
|
+
{
|
|
712
|
+
name: "ADVERSARIAL: earlier squatter does NOT override a later verified owner (priority \u2260 proof)",
|
|
713
|
+
claims: [
|
|
714
|
+
{ id: "sq", subject: { algorithm: "sha256", target: "FILE", value: "x" }, attribution: { identityId: "sq", role: "creator" }, assertedTier: "asserted", createdAt: "2020-01-01T00:00:00Z" },
|
|
715
|
+
{ id: "own", subject: { algorithm: "sha256", target: "FILE", value: "x" }, attribution: { identityId: "own", role: "creator" }, assertedTier: "verified", signature: { signerKeyId: "k", value: "v", carrierId: "in-toto", signedAt: "2026-01-01T00:00:00Z" }, createdAt: "2026-01-01T00:00:00Z" }
|
|
716
|
+
],
|
|
717
|
+
env: { knownCarriers: ["in-toto"], verifySignature: true, signerVerified: true },
|
|
718
|
+
expected: { open: false, originIndex: 1, divergence: false }
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
name: "ADVERSARIAL: signed copied bytes do not establish origin \u2192 open + divergence (invariant #10)",
|
|
722
|
+
claims: [
|
|
723
|
+
{ id: "imp", subject: { algorithm: "sha256", target: "FILE", value: "x" }, attribution: { identityId: "imp", role: "creator" }, assertedTier: "bound", signature: { signerKeyId: "k", value: "v", carrierId: "in-toto", signedAt: "2026-01-01T00:00:00Z" }, createdAt: "2026-01-01T00:00:00Z" },
|
|
724
|
+
{ id: "org", subject: { algorithm: "sha256", target: "FILE", value: "x" }, attribution: { identityId: "org", role: "creator" }, assertedTier: "asserted", createdAt: "2026-01-01T00:00:00Z" }
|
|
725
|
+
],
|
|
726
|
+
inputs: [{ isDerivative: true }, { isDerivative: false }],
|
|
727
|
+
env: { knownCarriers: ["in-toto"], verifySignature: true, signerVerified: true },
|
|
728
|
+
expected: { open: true, originIndex: null, divergence: true }
|
|
729
|
+
},
|
|
730
|
+
{
|
|
731
|
+
name: "ADVERSARIAL: priority alone never crowns \u2014 equal evidence, different timestamps stays open",
|
|
732
|
+
claims: [
|
|
733
|
+
{ id: "a", subject: { algorithm: "sha256", target: "FILE", value: "x" }, attribution: { identityId: "a", role: "creator" }, assertedTier: "verified", signature: { signerKeyId: "k", value: "v", carrierId: "in-toto", signedAt: "2024-01-01T00:00:00Z" }, createdAt: "2024-01-01T00:00:00Z" },
|
|
734
|
+
{ id: "b", subject: { algorithm: "sha256", target: "FILE", value: "x" }, attribution: { identityId: "b", role: "creator" }, assertedTier: "verified", signature: { signerKeyId: "k", value: "v", carrierId: "in-toto", signedAt: "2026-01-01T00:00:00Z" }, createdAt: "2026-01-01T00:00:00Z" }
|
|
735
|
+
],
|
|
736
|
+
env: { knownCarriers: ["in-toto"], verifySignature: true, signerVerified: true },
|
|
737
|
+
expected: { open: true, originIndex: null, divergence: false }
|
|
738
|
+
}
|
|
739
|
+
]
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
// ../core/src/conformance.ts
|
|
743
|
+
var CONFORMANCE_VERSION = conformance_v0_default.version;
|
|
744
|
+
|
|
745
|
+
// ../core/src/disclosure.ts
|
|
746
|
+
var AI_TRAILER_RE = /^[ \t]*(?:Co-authored-by|Generated-by|Assisted-by)[ \t]*:[ \t]*(.+?)[ \t]*$/gim;
|
|
747
|
+
var AI_MARKER_RE = /\b(?:claude|copilot|cursor(?:agent)?|devin|gemini|chatgpt|openai|codex|gpt-|codeium|windsurf|aider|codewhisperer|tabnine)\b/i;
|
|
748
|
+
function hasAiTrailer(message) {
|
|
749
|
+
AI_TRAILER_RE.lastIndex = 0;
|
|
750
|
+
for (const m of message.matchAll(AI_TRAILER_RE)) if (AI_MARKER_RE.test(m[1])) return true;
|
|
751
|
+
return false;
|
|
752
|
+
}
|
|
753
|
+
var DCO_RE = /^[ \t]*Signed-off-by[ \t]*:[ \t]*(.+?)[ \t]*$/gim;
|
|
754
|
+
function hasDcoSignoff(message) {
|
|
755
|
+
DCO_RE.lastIndex = 0;
|
|
756
|
+
return DCO_RE.test(message);
|
|
757
|
+
}
|
|
758
|
+
var HUMAN_ATTEST_RE = /^[ \t]*(?:Authored-by-human|Human-authored(?:-by)?)[ \t]*:[ \t]*(.+?)[ \t]*$/gim;
|
|
759
|
+
function hasHumanAttestation(message) {
|
|
760
|
+
HUMAN_ATTEST_RE.lastIndex = 0;
|
|
761
|
+
return HUMAN_ATTEST_RE.test(message);
|
|
762
|
+
}
|
|
763
|
+
function commitDisclosureKinds(commit) {
|
|
764
|
+
const kinds = [];
|
|
765
|
+
if (hasAiTrailer(commit.message)) kinds.push("ai-trailer");
|
|
766
|
+
if (hasHumanAttestation(commit.message)) kinds.push("human-attestation");
|
|
767
|
+
if (hasDcoSignoff(commit.message)) kinds.push("dco-signoff");
|
|
768
|
+
if (commit.signed === true) kinds.push("commit-signature");
|
|
769
|
+
return kinds;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// ../core/src/policy.ts
|
|
773
|
+
var DEFAULT_POLICY = { version: 0, mode: "advisory" };
|
|
774
|
+
var MODES = ["required", "advisory"];
|
|
775
|
+
function parseDisclosurePolicy(input) {
|
|
776
|
+
if (!input || typeof input !== "object") return DEFAULT_POLICY;
|
|
777
|
+
const o = input;
|
|
778
|
+
const mode = MODES.includes(o.mode) ? o.mode : "advisory";
|
|
779
|
+
const version = typeof o.version === "number" ? o.version : 0;
|
|
780
|
+
const accept = Array.isArray(o.accept) ? o.accept.filter((k) => typeof k === "string") : void 0;
|
|
781
|
+
return accept && accept.length > 0 ? { version, mode, accept } : { version, mode };
|
|
782
|
+
}
|
|
783
|
+
function evaluateDisclosurePolicy(policy, commits) {
|
|
784
|
+
const accept = policy.accept;
|
|
785
|
+
const isAccepted = (k) => !accept || accept.includes(k);
|
|
786
|
+
const verdicts = commits.map((c) => {
|
|
787
|
+
const satisfiedBy = c.kinds.filter(isAccepted);
|
|
788
|
+
return { ref: c.ref, compliant: satisfiedBy.length > 0, satisfiedBy };
|
|
789
|
+
});
|
|
790
|
+
const nonCompliant = verdicts.filter((v) => !v.compliant);
|
|
791
|
+
const compliant = verdicts.length - nonCompliant.length;
|
|
792
|
+
const pass = policy.mode !== "required" || nonCompliant.length === 0;
|
|
793
|
+
const wanted = accept ? accept.join(", ") : "any recognized disclosure";
|
|
794
|
+
const summary = `${compliant}/${verdicts.length} commits disclose origin (${policy.mode}; accepts: ${wanted}).` + (nonCompliant.length ? ` ${nonCompliant.length} undisclosed.` : "");
|
|
795
|
+
return { mode: policy.mode, pass, total: verdicts.length, compliant, nonCompliant, summary };
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// ../analyzer/src/host/github.ts
|
|
799
|
+
import { readFileSync } from "node:fs";
|
|
800
|
+
|
|
801
|
+
// ../analyzer/src/host/registry.ts
|
|
802
|
+
var registry = /* @__PURE__ */ new Map();
|
|
803
|
+
function registerHostAdapter(adapter) {
|
|
804
|
+
registry.set(adapter.id, adapter);
|
|
805
|
+
}
|
|
806
|
+
function detectCiRange(env = process.env) {
|
|
807
|
+
for (const adapter of registry.values()) {
|
|
808
|
+
const range = adapter.ciRange?.(env);
|
|
809
|
+
if (range) return { range, host: adapter.id };
|
|
810
|
+
}
|
|
811
|
+
return null;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// ../analyzer/src/host/github.ts
|
|
815
|
+
var NOREPLY = /^(?:(\d+)\+)?([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))@users\.noreply\.github\.com$/i;
|
|
816
|
+
var githubAdapter = {
|
|
817
|
+
id: "github",
|
|
818
|
+
name: "GitHub",
|
|
819
|
+
handleFromEmail(email) {
|
|
820
|
+
const m = (email ?? "").trim().match(NOREPLY);
|
|
821
|
+
return m ? m[2].toLowerCase() : null;
|
|
822
|
+
},
|
|
823
|
+
handleKey(handle2) {
|
|
824
|
+
return `gh:${handle2.toLowerCase()}`;
|
|
825
|
+
},
|
|
826
|
+
profileUrl(handle2) {
|
|
827
|
+
return `https://github.com/${handle2}`;
|
|
828
|
+
},
|
|
829
|
+
// On a `pull_request` event GitHub sets GITHUB_EVENT_NAME + GITHUB_EVENT_PATH (a JSON payload with
|
|
830
|
+
// the PR's base/head SHAs). Reading base..head here replaces the bash range computation the Action
|
|
831
|
+
// used to carry — the range logic now lives once, in the adapter. Needs the PR history present
|
|
832
|
+
// (the quickstart's `fetch-depth: 0`). Any absence/parse failure → null (fall back to recent
|
|
833
|
+
// history); never throws.
|
|
834
|
+
ciRange(env) {
|
|
835
|
+
const event = env.GITHUB_EVENT_NAME;
|
|
836
|
+
if (event !== "pull_request" && event !== "pull_request_target") return null;
|
|
837
|
+
const path = env.GITHUB_EVENT_PATH;
|
|
838
|
+
if (!path) return null;
|
|
839
|
+
try {
|
|
840
|
+
const payload = JSON.parse(readFileSync(path, "utf8"));
|
|
841
|
+
const base = payload.pull_request?.base?.sha;
|
|
842
|
+
const head = payload.pull_request?.head?.sha;
|
|
843
|
+
if (base && head) return `${base}..${head}`;
|
|
844
|
+
} catch {
|
|
845
|
+
}
|
|
846
|
+
return null;
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
registerHostAdapter(githubAdapter);
|
|
850
|
+
|
|
851
|
+
// ../analyzer/src/read-disclosures.ts
|
|
852
|
+
import { existsSync, readFileSync as readFileSync2 } from "node:fs";
|
|
853
|
+
import { join } from "node:path";
|
|
854
|
+
function readDisclosures(root, opts = {}) {
|
|
855
|
+
const commits = readGitLog(root, opts.range ? void 0 : opts.limit ?? 200, opts.range);
|
|
856
|
+
return commits.map((c) => ({
|
|
857
|
+
sha: c.sha ?? "",
|
|
858
|
+
subject: c.message.split("\n")[0] ?? "",
|
|
859
|
+
disclosures: commitDisclosureKinds({ message: c.message, signed: c.signed })
|
|
860
|
+
}));
|
|
861
|
+
}
|
|
862
|
+
function evaluateRepoDisclosure(root, opts = {}) {
|
|
863
|
+
let policy = DEFAULT_POLICY;
|
|
864
|
+
let warning;
|
|
865
|
+
const policyPath = join(root, ".madeby", "policy.json");
|
|
866
|
+
if (existsSync(policyPath)) {
|
|
867
|
+
try {
|
|
868
|
+
policy = parseDisclosurePolicy(JSON.parse(readFileSync2(policyPath, "utf8")));
|
|
869
|
+
} catch {
|
|
870
|
+
warning = ".madeby/policy.json is not valid JSON \u2014 degrading to advisory (report-only, never blocks).";
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
const commits = readDisclosures(root, opts);
|
|
874
|
+
const disclosures = commits.map((c) => ({ ref: (c.sha || "").slice(0, 8) || "(unknown)", kinds: c.disclosures }));
|
|
875
|
+
const result = evaluateDisclosurePolicy(policy, disclosures);
|
|
876
|
+
const subjectFor = (ref) => commits.find((c) => (c.sha || "").startsWith(ref))?.subject ?? "";
|
|
877
|
+
return {
|
|
878
|
+
mode: policy.mode,
|
|
879
|
+
pass: result.pass,
|
|
880
|
+
summary: result.summary,
|
|
881
|
+
total: disclosures.length,
|
|
882
|
+
undisclosed: result.nonCompliant.map((v) => ({ ref: v.ref, subject: subjectFor(v.ref) })),
|
|
883
|
+
...warning ? { warning } : {}
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// ../analyzer/src/path-provenance.ts
|
|
888
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
889
|
+
|
|
890
|
+
// ../analyzer/src/provenance.ts
|
|
891
|
+
import { readdirSync, readFileSync as readFileSync3 } from "node:fs";
|
|
892
|
+
import { join as join2 } from "node:path";
|
|
893
|
+
function parseAll(texts) {
|
|
894
|
+
const out = [];
|
|
895
|
+
for (const t of texts) {
|
|
896
|
+
try {
|
|
897
|
+
out.push(parseSpanManifest(t));
|
|
898
|
+
} catch {
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
return out;
|
|
902
|
+
}
|
|
903
|
+
function readSpanManifestsFromDir(repoDir) {
|
|
904
|
+
let files;
|
|
905
|
+
try {
|
|
906
|
+
files = readdirSync(join2(repoDir, ".madeby", "spans")).filter((f) => f.endsWith(".json"));
|
|
907
|
+
} catch {
|
|
908
|
+
return [];
|
|
909
|
+
}
|
|
910
|
+
return parseAll(
|
|
911
|
+
files.map((f) => {
|
|
912
|
+
try {
|
|
913
|
+
return readFileSync3(join2(repoDir, ".madeby", "spans", f), "utf8");
|
|
914
|
+
} catch {
|
|
915
|
+
return "";
|
|
916
|
+
}
|
|
917
|
+
}).filter(Boolean)
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// ../analyzer/src/path-provenance.ts
|
|
922
|
+
var overlaps = (aS, aE, bS, bE) => bS === void 0 || bE === void 0 || aS <= bE && aE >= bS;
|
|
923
|
+
function git(root, args) {
|
|
924
|
+
return execFileSync2("git", ["-C", root, "-c", "core.quotePath=false", ...args], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024, stdio: ["ignore", "pipe", "ignore"] });
|
|
925
|
+
}
|
|
926
|
+
var US = "";
|
|
927
|
+
function commitMeta(root, sha) {
|
|
928
|
+
try {
|
|
929
|
+
const out = git(root, ["show", "-s", `--format=%G?${US}%an${US}%ae${US}%B`, sha]);
|
|
930
|
+
const parts = out.split(US);
|
|
931
|
+
const sig = parts[0] ?? "N";
|
|
932
|
+
return { signed: sig !== "N" && sig !== "", name: parts[1] ?? "", email: parts[2] ?? "", message: (parts.slice(3).join(US) ?? "").trim() };
|
|
933
|
+
} catch {
|
|
934
|
+
return null;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
function blameGroups(porcelain) {
|
|
938
|
+
const header = /^([0-9a-f]{40}) \d+ (\d+)(?: \d+)?$/;
|
|
939
|
+
const perLine = [];
|
|
940
|
+
for (const ln of porcelain.split("\n")) {
|
|
941
|
+
const m = header.exec(ln);
|
|
942
|
+
if (m) perLine.push({ line: Number(m[2]), sha: m[1] });
|
|
943
|
+
}
|
|
944
|
+
perLine.sort((a, b) => a.line - b.line);
|
|
945
|
+
const groups = [];
|
|
946
|
+
for (const { line, sha } of perLine) {
|
|
947
|
+
const last = groups[groups.length - 1];
|
|
948
|
+
if (last && last.sha === sha && line === last.end + 1) last.end = line;
|
|
949
|
+
else groups.push({ start: line, end: line, sha });
|
|
950
|
+
}
|
|
951
|
+
return groups;
|
|
952
|
+
}
|
|
953
|
+
function provenanceOf(root, path, opts = {}) {
|
|
954
|
+
const records = [];
|
|
955
|
+
for (const manifest of readSpanManifestsFromDir(root)) {
|
|
956
|
+
for (const a of manifest.attestations) {
|
|
957
|
+
const hint = a.anchor.hint;
|
|
958
|
+
if (!hint || hint.path !== path) continue;
|
|
959
|
+
const s = hint.startLine ?? 1;
|
|
960
|
+
const e = hint.endLine ?? s;
|
|
961
|
+
if (!overlaps(s, e, opts.startLine, opts.endLine)) continue;
|
|
962
|
+
records.push({
|
|
963
|
+
source: "span",
|
|
964
|
+
granularity: "region",
|
|
965
|
+
startLine: hint.startLine,
|
|
966
|
+
endLine: hint.endLine,
|
|
967
|
+
ai: true,
|
|
968
|
+
bot: false,
|
|
969
|
+
provider: a.attribution.provider,
|
|
970
|
+
model: a.attribution.model,
|
|
971
|
+
confidence: "witnessed",
|
|
972
|
+
evidence: `witnessed AI span \u2014 ${a.attribution.model} (${a.attribution.source})`
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
if (opts.useBlame !== false) {
|
|
977
|
+
try {
|
|
978
|
+
const range = opts.startLine !== void 0 ? [`-L`, `${opts.startLine},${opts.endLine ?? opts.startLine}`] : [];
|
|
979
|
+
const porcelain = git(root, ["blame", "--porcelain", ...range, "--", path]);
|
|
980
|
+
const metaCache = /* @__PURE__ */ new Map();
|
|
981
|
+
for (const g of blameGroups(porcelain)) {
|
|
982
|
+
let meta = metaCache.get(g.sha);
|
|
983
|
+
if (meta === void 0) {
|
|
984
|
+
meta = commitMeta(root, g.sha);
|
|
985
|
+
metaCache.set(g.sha, meta);
|
|
986
|
+
}
|
|
987
|
+
if (!meta) continue;
|
|
988
|
+
const kinds = commitDisclosureKinds({ message: meta.message, signed: meta.signed });
|
|
989
|
+
const bot = isBotIdentity(meta.name, meta.email);
|
|
990
|
+
const ai = kinds.includes("ai-trailer");
|
|
991
|
+
const disclosed = kinds.length > 0 || bot;
|
|
992
|
+
const short = g.sha.slice(0, 8);
|
|
993
|
+
records.push({
|
|
994
|
+
source: "blame",
|
|
995
|
+
granularity: "commit",
|
|
996
|
+
startLine: g.start,
|
|
997
|
+
endLine: g.end,
|
|
998
|
+
ai,
|
|
999
|
+
bot,
|
|
1000
|
+
commit: short,
|
|
1001
|
+
disclosures: kinds,
|
|
1002
|
+
confidence: disclosed ? "disclosed" : "none",
|
|
1003
|
+
evidence: disclosed ? `last-touch: ${short} disclosed via ${bot ? "bot" : kinds.join(", ")}` : `last-touch: ${short} \u2014 undisclosed (origin unknown)`
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
} catch {
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
records.sort((a, b) => (a.startLine ?? 0) - (b.startLine ?? 0) || (a.source === "span" ? -1 : 1));
|
|
1010
|
+
return {
|
|
1011
|
+
path,
|
|
1012
|
+
resolution: "default: witnessed spans (region) + git-blame last-touch (commit); unknown otherwise. Granularity is a configurable convention \u2014 query a line range from your own resolver (ARCH \xA712).",
|
|
1013
|
+
records
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
function provenanceMap(root, opts = {}) {
|
|
1017
|
+
const prefix = opts.prefix;
|
|
1018
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
1019
|
+
for (const m of readSpanManifestsFromDir(root)) {
|
|
1020
|
+
for (const a of m.attestations) {
|
|
1021
|
+
const p = a.anchor.hint?.path;
|
|
1022
|
+
if (!p || prefix && !p.startsWith(prefix)) continue;
|
|
1023
|
+
let wf = byPath.get(p);
|
|
1024
|
+
if (!wf) {
|
|
1025
|
+
wf = { path: p, models: [], regions: [] };
|
|
1026
|
+
byPath.set(p, wf);
|
|
1027
|
+
}
|
|
1028
|
+
wf.regions.push({ startLine: a.anchor.hint?.startLine, endLine: a.anchor.hint?.endLine, model: a.attribution.model });
|
|
1029
|
+
if (!wf.models.includes(a.attribution.model)) wf.models.push(a.attribution.model);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
const witnessed = [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
1033
|
+
let trackedFiles = 0;
|
|
1034
|
+
try {
|
|
1035
|
+
trackedFiles = git(root, ["ls-files", ...prefix ? ["--", prefix] : []]).split("\n").filter(Boolean).length;
|
|
1036
|
+
} catch {
|
|
1037
|
+
}
|
|
1038
|
+
const commits = readDisclosures(root, { limit: opts.commitLimit ?? 500 });
|
|
1039
|
+
const disclosed = commits.filter((c) => c.disclosures.length > 0).length;
|
|
1040
|
+
const pct = commits.length ? Math.round(disclosed / commits.length * 100) : 0;
|
|
1041
|
+
const scope = prefix ?? "repo";
|
|
1042
|
+
const allModels = [...new Set(witnessed.flatMap((w) => w.models))];
|
|
1043
|
+
return {
|
|
1044
|
+
scope,
|
|
1045
|
+
trackedFiles,
|
|
1046
|
+
witnessedFiles: witnessed.length,
|
|
1047
|
+
witnessed,
|
|
1048
|
+
commitCoverage: { disclosed, total: commits.length },
|
|
1049
|
+
summary: `${scope}: ${trackedFiles} tracked file${trackedFiles === 1 ? "" : "s"}; ${witnessed.length} with witnessed AI provenance${allModels.length ? ` (${allModels.join(", ")})` : ""}; ${pct}% of the last ${commits.length} commits disclose origin.`,
|
|
1050
|
+
note: "Witnessed spans are precise (region-level, model-named); commit coverage is commit-level, not per-file authorship. Files without a witnessed span have unknown origin \u2014 disclosure, not detection: we report what's disclosed and never infer origin we can't see. Resolution is a configurable convention (ARCH \xA712)."
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// ../analyzer/src/record-spans.ts
|
|
1055
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
1056
|
+
import { existsSync as existsSync2, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync, writeFileSync } from "node:fs";
|
|
1057
|
+
import { homedir } from "node:os";
|
|
1058
|
+
import { dirname, join as join3 } from "node:path";
|
|
1059
|
+
|
|
1060
|
+
// ../analyzer/src/capture-local.ts
|
|
1061
|
+
var MATCH_THRESHOLD = 0.52;
|
|
1062
|
+
function repoRelative(recorded, repoRoot2) {
|
|
1063
|
+
const p = recorded.replace(/\\/g, "/");
|
|
1064
|
+
const root = repoRoot2.replace(/\\/g, "/");
|
|
1065
|
+
if (p.startsWith(root + "/")) return p.slice(root.length + 1);
|
|
1066
|
+
if (p.startsWith("/")) return null;
|
|
1067
|
+
if (p === ".." || p.startsWith("../")) return null;
|
|
1068
|
+
return p.replace(/^\.\//, "");
|
|
1069
|
+
}
|
|
1070
|
+
function groupEdits(edits, repoRoot2) {
|
|
1071
|
+
const out = /* @__PURE__ */ new Map();
|
|
1072
|
+
for (const e of edits) {
|
|
1073
|
+
const rel = repoRelative(e.path, repoRoot2);
|
|
1074
|
+
if (rel === null) continue;
|
|
1075
|
+
if (rel.startsWith(".madeby/") || rel.includes("node_modules/")) continue;
|
|
1076
|
+
if (!e.content) continue;
|
|
1077
|
+
const cur = out.get(rel) ?? { content: [], models: /* @__PURE__ */ new Set() };
|
|
1078
|
+
cur.content.push(e.content);
|
|
1079
|
+
cur.models.add(e.model);
|
|
1080
|
+
out.set(rel, cur);
|
|
1081
|
+
}
|
|
1082
|
+
return out;
|
|
1083
|
+
}
|
|
1084
|
+
function captureLocalSpans(input) {
|
|
1085
|
+
const threshold = input.threshold ?? MATCH_THRESHOLD;
|
|
1086
|
+
const parser = input.parser;
|
|
1087
|
+
const empty = {
|
|
1088
|
+
manifest: { version: "0", commit: input.commit, generatedAt: input.now, attestations: [] },
|
|
1089
|
+
tool: null,
|
|
1090
|
+
matched: [],
|
|
1091
|
+
discarded: []
|
|
1092
|
+
};
|
|
1093
|
+
if (!parser) return empty;
|
|
1094
|
+
const byFile = groupEdits(parser.parse(input.transcript), input.repoRoot);
|
|
1095
|
+
const attestations = [];
|
|
1096
|
+
const matched = [];
|
|
1097
|
+
const discarded = [];
|
|
1098
|
+
for (const [rel, ai] of byFile) {
|
|
1099
|
+
const current = input.readFile(rel);
|
|
1100
|
+
if (current === null) {
|
|
1101
|
+
discarded.push(rel);
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
const aiFp = structuralFingerprint(ai.content.join("\n"));
|
|
1105
|
+
const curFp = structuralFingerprint(current);
|
|
1106
|
+
const similarity = cosineSimilarity(aiFp.embedding, curFp.embedding);
|
|
1107
|
+
if (similarity < threshold) {
|
|
1108
|
+
discarded.push(rel);
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
matched.push(rel);
|
|
1112
|
+
for (const model of ai.models) {
|
|
1113
|
+
attestations.push({
|
|
1114
|
+
anchor: { fingerprint: aiFp.fingerprint, hint: { path: rel } },
|
|
1115
|
+
attribution: { provider: parser.provider, model, operatorId: input.operatorId, source: `${parser.id}-session-log` }
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
return {
|
|
1120
|
+
manifest: { version: "0", commit: input.commit, generatedAt: input.now, attestations },
|
|
1121
|
+
tool: parser.id,
|
|
1122
|
+
matched,
|
|
1123
|
+
discarded
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// ../analyzer/src/tool-parsers.ts
|
|
1128
|
+
var CLAUDE_WRITE_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
|
|
1129
|
+
function* claudeAssistantRecords(raw) {
|
|
1130
|
+
for (const line of raw.split("\n")) {
|
|
1131
|
+
if (!line.trim()) continue;
|
|
1132
|
+
let rec;
|
|
1133
|
+
try {
|
|
1134
|
+
rec = JSON.parse(line);
|
|
1135
|
+
} catch {
|
|
1136
|
+
continue;
|
|
1137
|
+
}
|
|
1138
|
+
if (rec.type === "assistant" && Array.isArray(rec.message?.content)) yield rec;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
var claudeCodeParser = {
|
|
1142
|
+
id: "claude-code",
|
|
1143
|
+
provider: "anthropic",
|
|
1144
|
+
detect(raw) {
|
|
1145
|
+
for (const rec of claudeAssistantRecords(raw)) {
|
|
1146
|
+
for (const block of rec.message.content) {
|
|
1147
|
+
if (block?.type === "tool_use" && CLAUDE_WRITE_TOOLS.has(block.name)) return true;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
return false;
|
|
1151
|
+
},
|
|
1152
|
+
parse(raw) {
|
|
1153
|
+
const edits = [];
|
|
1154
|
+
for (const rec of claudeAssistantRecords(raw)) {
|
|
1155
|
+
const model = rec.message?.model ?? "claude";
|
|
1156
|
+
for (const block of rec.message.content) {
|
|
1157
|
+
if (block?.type !== "tool_use" || !CLAUDE_WRITE_TOOLS.has(block.name)) continue;
|
|
1158
|
+
const input = block.input;
|
|
1159
|
+
const path = input?.file_path;
|
|
1160
|
+
const content = input?.content ?? input?.new_string ?? "";
|
|
1161
|
+
if (typeof path === "string" && content) edits.push({ path, content, model });
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
return edits;
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1167
|
+
var AIDER_FENCE = /^([^\n]+)\n```[^\n]*\n([\s\S]*?)\r?\n```/gm;
|
|
1168
|
+
var AIDER_FILENAME = /^[\w./\\-]+\.\w+$/;
|
|
1169
|
+
var AIDER_SR = /<<<<<<< SEARCH\r?\n(?:[\s\S]*?\r?\n)?=======\r?\n([\s\S]*?)\r?\n>>>>>>> REPLACE/g;
|
|
1170
|
+
var AIDER_MODEL = /^> Model:\s*(.+?)\s+with\s+[\w-]+ edit format/gim;
|
|
1171
|
+
function aiderModelAt(raw, pos) {
|
|
1172
|
+
let model = "aider";
|
|
1173
|
+
AIDER_MODEL.lastIndex = 0;
|
|
1174
|
+
for (let m; m = AIDER_MODEL.exec(raw); ) {
|
|
1175
|
+
if (m.index > pos) break;
|
|
1176
|
+
model = m[1].trim();
|
|
1177
|
+
}
|
|
1178
|
+
return model;
|
|
1179
|
+
}
|
|
1180
|
+
var aiderParser = {
|
|
1181
|
+
id: "aider",
|
|
1182
|
+
provider: "aider",
|
|
1183
|
+
detect(raw) {
|
|
1184
|
+
return /^# aider chat started at /m.test(raw) || /^> Aider v\d/m.test(raw);
|
|
1185
|
+
},
|
|
1186
|
+
parse(raw) {
|
|
1187
|
+
const wholeFormat = /with whole edit format/i.test(raw);
|
|
1188
|
+
const edits = [];
|
|
1189
|
+
AIDER_FENCE.lastIndex = 0;
|
|
1190
|
+
for (let f; f = AIDER_FENCE.exec(raw); ) {
|
|
1191
|
+
const name = f[1].trim();
|
|
1192
|
+
if (!AIDER_FILENAME.test(name)) continue;
|
|
1193
|
+
const path = name.replace(/\\/g, "/");
|
|
1194
|
+
const body = f[2];
|
|
1195
|
+
const model = aiderModelAt(raw, f.index);
|
|
1196
|
+
let sawSearchReplace = false;
|
|
1197
|
+
AIDER_SR.lastIndex = 0;
|
|
1198
|
+
for (let s; s = AIDER_SR.exec(body); ) {
|
|
1199
|
+
sawSearchReplace = true;
|
|
1200
|
+
const content = s[1];
|
|
1201
|
+
if (content.trim()) edits.push({ path, content, model });
|
|
1202
|
+
}
|
|
1203
|
+
if (!sawSearchReplace && wholeFormat && body.trim()) edits.push({ path, content: body, model });
|
|
1204
|
+
}
|
|
1205
|
+
return edits;
|
|
1206
|
+
}
|
|
1207
|
+
};
|
|
1208
|
+
var SPECSTORY_EDIT = /<summary>Tool use:[^<]*?(?:Edit|Write|Create) file:\s*([^\n<]+?)\s*<\/summary>([\s\S]*?)<\/details>/g;
|
|
1209
|
+
var SPECSTORY_DIFF = /```diff\r?\n([\s\S]*?)```/g;
|
|
1210
|
+
var SPECSTORY_MODEL = /_\*\*Agent \(model ([\w./:+-]+)/gi;
|
|
1211
|
+
function htmlUnescape(s) {
|
|
1212
|
+
return s.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/'/gi, "'").replace(/&/g, "&");
|
|
1213
|
+
}
|
|
1214
|
+
function specstoryModelAt(raw, pos) {
|
|
1215
|
+
let model = "specstory";
|
|
1216
|
+
SPECSTORY_MODEL.lastIndex = 0;
|
|
1217
|
+
for (let m; m = SPECSTORY_MODEL.exec(raw); ) {
|
|
1218
|
+
if (m.index > pos) break;
|
|
1219
|
+
model = m[1].trim();
|
|
1220
|
+
}
|
|
1221
|
+
return model;
|
|
1222
|
+
}
|
|
1223
|
+
var specstoryParser = {
|
|
1224
|
+
id: "specstory",
|
|
1225
|
+
provider: "specstory",
|
|
1226
|
+
detect(raw) {
|
|
1227
|
+
return /^<!-- Generated by SpecStory/m.test(raw) || /<summary>Tool use: \*\*/.test(raw);
|
|
1228
|
+
},
|
|
1229
|
+
parse(raw) {
|
|
1230
|
+
const edits = [];
|
|
1231
|
+
SPECSTORY_EDIT.lastIndex = 0;
|
|
1232
|
+
for (let m; m = SPECSTORY_EDIT.exec(raw); ) {
|
|
1233
|
+
const path = m[1].trim();
|
|
1234
|
+
const model = specstoryModelAt(raw, m.index);
|
|
1235
|
+
const added = [];
|
|
1236
|
+
SPECSTORY_DIFF.lastIndex = 0;
|
|
1237
|
+
for (let d; d = SPECSTORY_DIFF.exec(m[2]); ) {
|
|
1238
|
+
for (const line of d[1].split("\n")) {
|
|
1239
|
+
if (line.startsWith("+") && !line.startsWith("+++")) added.push(htmlUnescape(line.replace(/^\+ ?/, "")));
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
const content = added.join("\n");
|
|
1243
|
+
if (content.trim()) edits.push({ path, content, model });
|
|
1244
|
+
}
|
|
1245
|
+
return edits;
|
|
1246
|
+
}
|
|
1247
|
+
};
|
|
1248
|
+
var registry2 = /* @__PURE__ */ new Map();
|
|
1249
|
+
function registerToolParser(parser) {
|
|
1250
|
+
registry2.set(parser.id, parser);
|
|
1251
|
+
}
|
|
1252
|
+
function listToolParsers() {
|
|
1253
|
+
return [...registry2.values()];
|
|
1254
|
+
}
|
|
1255
|
+
registerToolParser(claudeCodeParser);
|
|
1256
|
+
registerToolParser(aiderParser);
|
|
1257
|
+
registerToolParser(specstoryParser);
|
|
1258
|
+
function detectParser(raw, parsers = listToolParsers()) {
|
|
1259
|
+
return parsers.find((p) => p.detect(raw)) ?? null;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// ../analyzer/src/redact-sample.ts
|
|
1263
|
+
var HEADER = "<!-- madeby parser-sample \u2014 STRUCTURE ONLY. All code, prompts, paths, and secrets were redacted\n locally; only the log's format/keys and model identifiers remain. Safe to attach to an issue.\n A parser keys on this shape, not on content \u2014 see packages/analyzer/src/tool-parsers.ts. -->";
|
|
1264
|
+
var SAFE_JSON_KEYS = /* @__PURE__ */ new Set([
|
|
1265
|
+
"type",
|
|
1266
|
+
"role",
|
|
1267
|
+
"name",
|
|
1268
|
+
"model",
|
|
1269
|
+
"provider",
|
|
1270
|
+
"source",
|
|
1271
|
+
"id",
|
|
1272
|
+
"uuid",
|
|
1273
|
+
"tool",
|
|
1274
|
+
"toolName",
|
|
1275
|
+
"stop_reason",
|
|
1276
|
+
"stopReason",
|
|
1277
|
+
"finish_reason",
|
|
1278
|
+
"status",
|
|
1279
|
+
"event",
|
|
1280
|
+
"kind",
|
|
1281
|
+
"version",
|
|
1282
|
+
"format",
|
|
1283
|
+
"editFormat",
|
|
1284
|
+
"modelName",
|
|
1285
|
+
"agent",
|
|
1286
|
+
"modelId"
|
|
1287
|
+
]);
|
|
1288
|
+
var PATH_JSON_KEYS = /* @__PURE__ */ new Set(["path", "file_path", "filePath", "uri", "fsPath", "filename", "file"]);
|
|
1289
|
+
function extPlaceholder(v) {
|
|
1290
|
+
const m = /\.([A-Za-z0-9]{1,8})$/.exec(v.trim());
|
|
1291
|
+
return m ? `<path>.${m[1]}` : "<path>";
|
|
1292
|
+
}
|
|
1293
|
+
function redactJsonValue(value, key) {
|
|
1294
|
+
if (typeof value === "string") {
|
|
1295
|
+
if (key && PATH_JSON_KEYS.has(key)) return extPlaceholder(value);
|
|
1296
|
+
if (key && SAFE_JSON_KEYS.has(key)) return value;
|
|
1297
|
+
return value === "" ? "" : "<redacted>";
|
|
1298
|
+
}
|
|
1299
|
+
if (Array.isArray(value)) return value.map((v) => redactJsonValue(v));
|
|
1300
|
+
if (value && typeof value === "object") {
|
|
1301
|
+
const out = {};
|
|
1302
|
+
for (const [k, v] of Object.entries(value)) out[k] = redactJsonValue(v, k);
|
|
1303
|
+
return out;
|
|
1304
|
+
}
|
|
1305
|
+
return value;
|
|
1306
|
+
}
|
|
1307
|
+
function looksLikeJsonl(lines) {
|
|
1308
|
+
const nonEmpty = lines.filter((l) => l.trim());
|
|
1309
|
+
if (!nonEmpty.length) return false;
|
|
1310
|
+
let ok = 0;
|
|
1311
|
+
for (const l of nonEmpty.slice(0, 50)) {
|
|
1312
|
+
try {
|
|
1313
|
+
const v = JSON.parse(l);
|
|
1314
|
+
if (v && typeof v === "object") ok += 1;
|
|
1315
|
+
} catch {
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
return ok >= Math.min(nonEmpty.length, 50) * 0.6;
|
|
1319
|
+
}
|
|
1320
|
+
function redactJsonl(lines) {
|
|
1321
|
+
return lines.map((l) => {
|
|
1322
|
+
if (!l.trim()) return "";
|
|
1323
|
+
try {
|
|
1324
|
+
return JSON.stringify(redactJsonValue(JSON.parse(l)));
|
|
1325
|
+
} catch {
|
|
1326
|
+
return "<redacted>";
|
|
1327
|
+
}
|
|
1328
|
+
}).join("\n");
|
|
1329
|
+
}
|
|
1330
|
+
var FENCE = /^(\s*```+)([A-Za-z0-9+.-]*)(.*)$/;
|
|
1331
|
+
var BARE_PATH_LINE = /^[\w./\\-]+\.[A-Za-z0-9]{1,8}$/;
|
|
1332
|
+
var KEEP_VERBATIM = /^(<<<<<<< SEARCH|=======|>>>>>>> REPLACE|_\*\*(?:User|Assistant)\*\*_|---+|> (?:Model|Aider v|Tokens|Git repo|Repo-map|Added|Cost|Warning|You can skip):|<\/?details>|<\/?summary>)/;
|
|
1333
|
+
function redactModelLine(line) {
|
|
1334
|
+
const m = /^(\s*> Model:\s*)(\S+)(\s+with\s+[\w-]+ edit format.*)?$/.exec(line);
|
|
1335
|
+
return m ? `${m[1]}${m[2]}${m[3] ?? ""}` : "> Model: <redacted>";
|
|
1336
|
+
}
|
|
1337
|
+
function redactPathsInline(s) {
|
|
1338
|
+
return s.replace(/((?:file|repo|path|reading|writing|created|modified|deleted):\s*)([^\s<>"']+)/gi, (_m, lbl, p) => lbl + extPlaceholder(p)).replace(/(^|[\s(])((?:[A-Za-z]:[\\/]|~[\\/]|\.{1,2}[\\/]|[\\/])[\w./\\-]*)/g, (_m, pre, p) => pre + extPlaceholder(p)).replace(/(^|[\s(])((?:[\w.-]+[\\/]){2,}[\w.-]*)/g, (_m, pre, p) => pre + extPlaceholder(p));
|
|
1339
|
+
}
|
|
1340
|
+
function redactMarkdown(lines) {
|
|
1341
|
+
const out = [];
|
|
1342
|
+
let inFence = false;
|
|
1343
|
+
const pushRedacted = () => {
|
|
1344
|
+
if (out[out.length - 1] !== "<redacted>") out.push("<redacted>");
|
|
1345
|
+
};
|
|
1346
|
+
for (const raw of lines) {
|
|
1347
|
+
const fence = FENCE.exec(raw);
|
|
1348
|
+
if (fence) {
|
|
1349
|
+
inFence = !inFence;
|
|
1350
|
+
const info = (fence[3] ?? "").replace(/[\w.-]*[\\/][\w./\\-]*/g, (m) => extPlaceholder(m));
|
|
1351
|
+
out.push(fence[1].trimStart() + fence[2] + info);
|
|
1352
|
+
continue;
|
|
1353
|
+
}
|
|
1354
|
+
if (inFence) {
|
|
1355
|
+
if (KEEP_VERBATIM.test(raw)) out.push(raw);
|
|
1356
|
+
else pushRedacted();
|
|
1357
|
+
continue;
|
|
1358
|
+
}
|
|
1359
|
+
if (raw.trim() === "") {
|
|
1360
|
+
out.push("");
|
|
1361
|
+
continue;
|
|
1362
|
+
}
|
|
1363
|
+
if (/^#{1,6}\s/.test(raw)) {
|
|
1364
|
+
out.push(raw.replace(/^(#{1,6}\s).*/, "$1<redacted>"));
|
|
1365
|
+
continue;
|
|
1366
|
+
}
|
|
1367
|
+
if (BARE_PATH_LINE.test(raw.trim())) {
|
|
1368
|
+
out.push(extPlaceholder(raw.trim()));
|
|
1369
|
+
continue;
|
|
1370
|
+
}
|
|
1371
|
+
if (KEEP_VERBATIM.test(raw)) {
|
|
1372
|
+
out.push(/^\s*> Model:/.test(raw) ? redactModelLine(raw) : redactPathsInline(raw));
|
|
1373
|
+
continue;
|
|
1374
|
+
}
|
|
1375
|
+
pushRedacted();
|
|
1376
|
+
}
|
|
1377
|
+
return out.join("\n");
|
|
1378
|
+
}
|
|
1379
|
+
function redactSample(raw) {
|
|
1380
|
+
const lines = raw.split("\n");
|
|
1381
|
+
const body = looksLikeJsonl(lines) ? redactJsonl(lines) : redactMarkdown(lines);
|
|
1382
|
+
return `${HEADER}
|
|
1383
|
+
${body}
|
|
1384
|
+
`;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// ../analyzer/src/record-spans.ts
|
|
1388
|
+
function git2(root, ...a) {
|
|
1389
|
+
return execFileSync3("git", ["-C", root, "-c", "core.quotePath=false", ...a], { encoding: "utf8" }).trim();
|
|
1390
|
+
}
|
|
1391
|
+
function autodiscover(root) {
|
|
1392
|
+
const enc3 = root.replace(/[/_]/g, "-");
|
|
1393
|
+
try {
|
|
1394
|
+
const dir = join3(homedir(), ".claude", "projects", enc3);
|
|
1395
|
+
return readdirSync2(dir).filter((f) => f.endsWith(".jsonl")).map((f) => join3(dir, f)).sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)[0] ?? null;
|
|
1396
|
+
} catch {
|
|
1397
|
+
return null;
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
function recordAiSpans(root, opts = {}) {
|
|
1401
|
+
const transcriptPath = opts.transcriptPath ?? autodiscover(root);
|
|
1402
|
+
if (!transcriptPath || !existsSync2(transcriptPath)) {
|
|
1403
|
+
return { written: false, matched: 0, discarded: 0, message: "transcript not found \u2014 pass a transcript path", error: "transcript-not-found" };
|
|
1404
|
+
}
|
|
1405
|
+
const commit = git2(root, "rev-parse", opts.ref ?? "HEAD");
|
|
1406
|
+
const transcriptText = readFileSync4(transcriptPath, "utf8");
|
|
1407
|
+
const parser = detectParser(transcriptText);
|
|
1408
|
+
if (!parser) {
|
|
1409
|
+
return { written: false, matched: 0, discarded: 0, message: "unrecognized log format (Claude Code is supported today)", error: "unrecognized-format" };
|
|
1410
|
+
}
|
|
1411
|
+
const result = captureLocalSpans({
|
|
1412
|
+
transcript: transcriptText,
|
|
1413
|
+
parser,
|
|
1414
|
+
repoRoot: root,
|
|
1415
|
+
readFile: (rel) => {
|
|
1416
|
+
try {
|
|
1417
|
+
return readFileSync4(join3(root, rel), "utf8");
|
|
1418
|
+
} catch {
|
|
1419
|
+
return null;
|
|
1420
|
+
}
|
|
1421
|
+
},
|
|
1422
|
+
commit,
|
|
1423
|
+
operatorId: git2(root, "show", "-s", "--format=%ae", commit),
|
|
1424
|
+
now: opts.now ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
1425
|
+
});
|
|
1426
|
+
if (result.manifest.attestations.length === 0) {
|
|
1427
|
+
return {
|
|
1428
|
+
written: false,
|
|
1429
|
+
tool: result.tool ?? void 0,
|
|
1430
|
+
matched: 0,
|
|
1431
|
+
discarded: result.discarded.length,
|
|
1432
|
+
message: `no AI-authored content from the session is structurally present (matched 0, discarded ${result.discarded.length})`
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
const out = join3(root, ".madeby", "spans", `${commit}.json`);
|
|
1436
|
+
let kept = [];
|
|
1437
|
+
if (existsSync2(out)) {
|
|
1438
|
+
const thisSource = `${result.tool}-session-log`;
|
|
1439
|
+
try {
|
|
1440
|
+
kept = parseSpanManifest(readFileSync4(out, "utf8")).attestations.filter((a) => a.attribution.source !== thisSource);
|
|
1441
|
+
} catch {
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
const manifest = { ...result.manifest, attestations: [...kept, ...result.manifest.attestations] };
|
|
1445
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
1446
|
+
writeFileSync(out, serializeSpanManifest(manifest));
|
|
1447
|
+
return {
|
|
1448
|
+
written: true,
|
|
1449
|
+
path: out,
|
|
1450
|
+
tool: result.tool ?? void 0,
|
|
1451
|
+
matched: result.matched.length,
|
|
1452
|
+
discarded: result.discarded.length,
|
|
1453
|
+
message: `wrote ${result.matched.length} witnessed file(s) (${result.discarded.length} discarded)`
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
function writeParserSample(root, opts = {}) {
|
|
1457
|
+
const transcriptPath = opts.transcriptPath ?? autodiscover(root);
|
|
1458
|
+
if (!transcriptPath || !existsSync2(transcriptPath)) {
|
|
1459
|
+
return { written: false, message: "session log not found \u2014 pass its path", error: "transcript-not-found" };
|
|
1460
|
+
}
|
|
1461
|
+
const text = readFileSync4(transcriptPath, "utf8");
|
|
1462
|
+
const tool = detectParser(text)?.id;
|
|
1463
|
+
const skeleton = redactSample(text);
|
|
1464
|
+
const dir = join3(root, ".madeby", "parser-samples");
|
|
1465
|
+
mkdirSync(dir, { recursive: true });
|
|
1466
|
+
const stamp = (opts.now ?? (/* @__PURE__ */ new Date()).toISOString()).replace(/[:.]/g, "-");
|
|
1467
|
+
const out = join3(dir, `${tool ?? "unknown-tool"}-${stamp}.txt`);
|
|
1468
|
+
writeFileSync(out, skeleton);
|
|
1469
|
+
return { written: true, path: out, tool, message: `wrote a content-free format sample to ${out}` };
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
// ../analyzer/src/clone.ts
|
|
1473
|
+
import { execFile } from "node:child_process";
|
|
1474
|
+
import { promisify } from "node:util";
|
|
1475
|
+
var execFileP = promisify(execFile);
|
|
1476
|
+
|
|
1477
|
+
// ../analyzer/src/enrich.ts
|
|
1478
|
+
var TTL_MS = 6 * 60 * 60 * 1e3;
|
|
1479
|
+
|
|
1480
|
+
// ../analyzer/src/host/azure.ts
|
|
1481
|
+
var azureAdapter = {
|
|
1482
|
+
id: "azure-devops",
|
|
1483
|
+
name: "Azure DevOps",
|
|
1484
|
+
// Azure commit emails are org/AAD emails with no public handle encoding (unlike GitHub's noreply).
|
|
1485
|
+
// We never guess an identity from an email — so there is nothing to resolve zero-network here.
|
|
1486
|
+
handleFromEmail() {
|
|
1487
|
+
return null;
|
|
1488
|
+
},
|
|
1489
|
+
handleKey(handle2) {
|
|
1490
|
+
return `az:${handle2.toLowerCase()}`;
|
|
1491
|
+
},
|
|
1492
|
+
// No profileUrl / resolveHandleViaApi / fetchProfile: Azure has no public per-user profile pages,
|
|
1493
|
+
// and org-private identity enrichment is out of scope for the public mirror. The interface makes
|
|
1494
|
+
// these optional precisely so an adapter can omit what its host doesn't offer.
|
|
1495
|
+
// Azure Pipelines sets these predefined variables on a PR-triggered build. The target branch is
|
|
1496
|
+
// given as a ref (not a SHA), so the range references origin/<target>; the pipeline template uses
|
|
1497
|
+
// fetchDepth: 0 so that ref + the PR history are present. Any absence → null (recent history).
|
|
1498
|
+
ciRange(env) {
|
|
1499
|
+
if (env.BUILD_REASON !== "PullRequest") return null;
|
|
1500
|
+
const targetRef = env.SYSTEM_PULLREQUEST_TARGETBRANCH;
|
|
1501
|
+
if (!targetRef) return null;
|
|
1502
|
+
const target = targetRef.replace(/^refs\/heads\//, "");
|
|
1503
|
+
const head = env.SYSTEM_PULLREQUEST_SOURCECOMMITID || "HEAD";
|
|
1504
|
+
return `origin/${target}..${head}`;
|
|
1505
|
+
}
|
|
1506
|
+
};
|
|
1507
|
+
registerHostAdapter(azureAdapter);
|
|
1508
|
+
|
|
1509
|
+
// ../analyzer/src/host/gitlab.ts
|
|
1510
|
+
var gitlabAdapter = {
|
|
1511
|
+
id: "gitlab",
|
|
1512
|
+
name: "GitLab",
|
|
1513
|
+
// GitLab commit emails carry no reliable public handle encoding (unlike GitHub's noreply). We never
|
|
1514
|
+
// guess an identity from an email, so there is nothing to resolve zero-network here.
|
|
1515
|
+
handleFromEmail() {
|
|
1516
|
+
return null;
|
|
1517
|
+
},
|
|
1518
|
+
handleKey(handle2) {
|
|
1519
|
+
return `gl:${handle2.toLowerCase()}`;
|
|
1520
|
+
},
|
|
1521
|
+
// GitLab CI sets these predefined variables on a merge-request pipeline. CI_MERGE_REQUEST_DIFF_BASE_SHA
|
|
1522
|
+
// is the merge-base of the MR; CI_COMMIT_SHA is the head — so base..head is the MR's own commits.
|
|
1523
|
+
// (Needs the MR pipeline / a deep-enough fetch for those SHAs.) Any absence → null (recent history).
|
|
1524
|
+
ciRange(env) {
|
|
1525
|
+
if (env.CI_PIPELINE_SOURCE !== "merge_request_event") return null;
|
|
1526
|
+
const base = env.CI_MERGE_REQUEST_DIFF_BASE_SHA;
|
|
1527
|
+
const head = env.CI_COMMIT_SHA;
|
|
1528
|
+
if (base && head) return `${base}..${head}`;
|
|
1529
|
+
return null;
|
|
1530
|
+
}
|
|
1531
|
+
};
|
|
1532
|
+
registerHostAdapter(gitlabAdapter);
|
|
1533
|
+
|
|
1534
|
+
// src/scope.ts
|
|
1535
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
1536
|
+
function repoRoot() {
|
|
1537
|
+
try {
|
|
1538
|
+
return execFileSync4("git", ["-c", "core.quotePath=false", "rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
|
|
1539
|
+
} catch {
|
|
1540
|
+
return null;
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
function resolveRange(explicit) {
|
|
1544
|
+
if (explicit) return { range: explicit };
|
|
1545
|
+
const ci = detectCiRange();
|
|
1546
|
+
return ci ? { range: ci.range, autoHost: ci.host } : {};
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// src/commands/check.ts
|
|
1550
|
+
function checkCommand(args) {
|
|
1551
|
+
const json = args.includes("--json");
|
|
1552
|
+
const explicit = args.find((a) => !a.startsWith("--"));
|
|
1553
|
+
const root = repoRoot();
|
|
1554
|
+
if (!root) {
|
|
1555
|
+
console.error("madeby check: not a git repository.");
|
|
1556
|
+
process.exit(2);
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
const { range, autoHost } = resolveRange(explicit);
|
|
1560
|
+
const result = evaluateRepoDisclosure(root, { range });
|
|
1561
|
+
if (json) {
|
|
1562
|
+
console.log(JSON.stringify({ range: range ?? null, autoHost: autoHost ?? null, ...result }, null, 2));
|
|
1563
|
+
process.exit(result.pass ? 0 : 1);
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
if (result.warning) console.warn(`madeby check: ${result.warning}`);
|
|
1567
|
+
if (autoHost) console.log(`madeby check: auto-scoped to the ${autoHost} PR range.`);
|
|
1568
|
+
console.log(`madeby check \u2014 ${result.summary}`);
|
|
1569
|
+
if (result.undisclosed.length) {
|
|
1570
|
+
console.log(`
|
|
1571
|
+
Undisclosed commits (${result.undisclosed.length}):`);
|
|
1572
|
+
for (const v of result.undisclosed.slice(0, 20)) {
|
|
1573
|
+
console.log(` \u2717 ${v.ref} ${v.subject.slice(0, 60)}`);
|
|
1574
|
+
}
|
|
1575
|
+
if (result.undisclosed.length > 20) console.log(` \u2026 and ${result.undisclosed.length - 20} more`);
|
|
1576
|
+
console.log(
|
|
1577
|
+
`
|
|
1578
|
+
To disclose: add a Co-Authored-By / Generated-by trailer, affirm your own authorship with 'madeby me', add a DCO Signed-off-by, sign the commit, or record your AI session with 'madeby ai'. (Disclosure, not a ban \u2014 what this project requires is set in .madeby/policy.json.)`
|
|
1579
|
+
);
|
|
1580
|
+
}
|
|
1581
|
+
process.exit(result.pass ? 0 : 1);
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
// src/commands/ai.ts
|
|
1585
|
+
var ISSUE_URL = "https://github.com/MacDougherty/MadeBy/issues/new";
|
|
1586
|
+
function aiCommand(args) {
|
|
1587
|
+
const root = repoRoot();
|
|
1588
|
+
if (!root) {
|
|
1589
|
+
console.error("madeby ai: not a git repository.");
|
|
1590
|
+
process.exit(2);
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
const makeSample = args.includes("--make-parser-sample");
|
|
1594
|
+
const positional = args.filter((a) => !a.startsWith("--"));
|
|
1595
|
+
if (makeSample) {
|
|
1596
|
+
const s = writeParserSample(root, { transcriptPath: positional[0] });
|
|
1597
|
+
if (s.error === "transcript-not-found") {
|
|
1598
|
+
console.error("madeby ai: session log not found \u2014 pass it: madeby ai --make-parser-sample <log>");
|
|
1599
|
+
process.exit(1);
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
console.log(
|
|
1603
|
+
`madeby ai: ${s.message}.
|
|
1604
|
+
\u2192 It contains only the log's structure + model ids \u2014 all code, prompts, paths, and secrets
|
|
1605
|
+
were redacted locally. Review it, then attach it to a new issue so we can add a parser:
|
|
1606
|
+
${ISSUE_URL}`
|
|
1607
|
+
);
|
|
1608
|
+
process.exit(0);
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
const res = recordAiSpans(root, { transcriptPath: positional[0], ref: positional[1] });
|
|
1612
|
+
if (res.error === "transcript-not-found") {
|
|
1613
|
+
console.error("madeby ai: session log not found \u2014 pass it: madeby ai <transcript.jsonl> [ref]");
|
|
1614
|
+
process.exit(1);
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
if (res.error === "unrecognized-format") {
|
|
1618
|
+
console.error(
|
|
1619
|
+
"madeby ai: unrecognized log format \u2014 no registered tool parser matched (Claude Code, aider, and\n SpecStory are supported today). Help add your tool WITHOUT sharing any code or prompts:\n madeby ai --make-parser-sample <log> # writes a content-free format sample to review + share"
|
|
1620
|
+
);
|
|
1621
|
+
process.exit(1);
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
if (!res.written) {
|
|
1625
|
+
console.log(`madeby ai: ${res.message}. Nothing recorded.`);
|
|
1626
|
+
process.exit(0);
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
console.log(
|
|
1630
|
+
`madeby ai: wrote ${res.path} \u2014 [${res.tool}] ${res.matched} witnessed file(s) matched, ${res.discarded} discarded (not present in the checkout).`
|
|
1631
|
+
);
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
// src/commands/me.ts
|
|
1635
|
+
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
1636
|
+
function git3(root, args) {
|
|
1637
|
+
return execFileSync5("git", ["-C", root, ...args], { encoding: "utf8" }).trim();
|
|
1638
|
+
}
|
|
1639
|
+
var HUMAN_ATTEST = /^[ \t]*(?:Authored-by-human|Human-authored(?:-by)?)[ \t]*:/im;
|
|
1640
|
+
function meCommand(_args) {
|
|
1641
|
+
const root = repoRoot();
|
|
1642
|
+
if (!root) {
|
|
1643
|
+
console.error("madeby me: not a git repository.");
|
|
1644
|
+
process.exit(2);
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
let name = "";
|
|
1648
|
+
let email = "";
|
|
1649
|
+
try {
|
|
1650
|
+
name = git3(root, ["config", "user.name"]);
|
|
1651
|
+
email = git3(root, ["config", "user.email"]);
|
|
1652
|
+
} catch {
|
|
1653
|
+
}
|
|
1654
|
+
if (!name || !email) {
|
|
1655
|
+
console.error("madeby me: set your git identity first (git config user.name / user.email).");
|
|
1656
|
+
process.exit(1);
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
try {
|
|
1660
|
+
execFileSync5("git", ["-C", root, "diff", "--cached", "--quiet"]);
|
|
1661
|
+
} catch {
|
|
1662
|
+
console.error("madeby me: you have staged changes. Commit them first \u2014 `me` affirms HEAD, not your index.");
|
|
1663
|
+
process.exit(1);
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
let subject = "";
|
|
1667
|
+
let message = "";
|
|
1668
|
+
try {
|
|
1669
|
+
subject = git3(root, ["show", "-s", "--format=%s", "HEAD"]);
|
|
1670
|
+
message = git3(root, ["show", "-s", "--format=%B", "HEAD"]);
|
|
1671
|
+
} catch {
|
|
1672
|
+
console.error("madeby me: no commit to affirm \u2014 make a commit first.");
|
|
1673
|
+
process.exit(1);
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
if (HUMAN_ATTEST.test(message)) {
|
|
1677
|
+
console.log("madeby me: HEAD already affirms human authorship. Nothing to do.");
|
|
1678
|
+
process.exit(0);
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
execFileSync5("git", ["-C", root, "commit", "--amend", "--no-edit", "--trailer", `Authored-by-human: ${name} <${email}>`]);
|
|
1682
|
+
console.log(`madeby me: affirmed human authorship on HEAD \u2014 "${subject}" (Authored-by-human: ${name}).`);
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
// src/commands/who.ts
|
|
1686
|
+
function parseLines(arg) {
|
|
1687
|
+
if (!arg) return {};
|
|
1688
|
+
const m = /^--lines=(\d+)(?:-(\d+))?$/.exec(arg);
|
|
1689
|
+
if (!m) return {};
|
|
1690
|
+
const startLine = Number(m[1]);
|
|
1691
|
+
const endLine = m[2] ? Number(m[2]) : startLine;
|
|
1692
|
+
return { startLine, endLine };
|
|
1693
|
+
}
|
|
1694
|
+
function whoCommand(args) {
|
|
1695
|
+
const json = args.includes("--json");
|
|
1696
|
+
const path = args.find((a) => !a.startsWith("--"));
|
|
1697
|
+
const { startLine, endLine } = parseLines(args.find((a) => a.startsWith("--lines=")));
|
|
1698
|
+
const root = repoRoot();
|
|
1699
|
+
if (!root) {
|
|
1700
|
+
console.error("madeby who: not a git repository.");
|
|
1701
|
+
process.exit(2);
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
if (path) {
|
|
1705
|
+
const res = provenanceOf(root, path, { startLine, endLine });
|
|
1706
|
+
if (json) {
|
|
1707
|
+
console.log(JSON.stringify(res, null, 2));
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
console.log(`made by whom? \u2014 ${res.path}${startLine ? ` (lines ${startLine}-${endLine})` : ""}`);
|
|
1711
|
+
if (res.records.length === 0) {
|
|
1712
|
+
console.log(" (no record \u2014 origin unknown)");
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
for (const r of res.records) {
|
|
1716
|
+
const span = r.startLine ? `${r.startLine}-${r.endLine ?? r.startLine}` : "\u2014";
|
|
1717
|
+
console.log(` [${r.confidence}] L${span} ${r.evidence}`);
|
|
1718
|
+
}
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
const map = provenanceMap(root, {});
|
|
1722
|
+
if (json) {
|
|
1723
|
+
console.log(JSON.stringify(map, null, 2));
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
console.log(map.summary);
|
|
1727
|
+
for (const w of map.witnessed) {
|
|
1728
|
+
console.log(` \u270E ${w.path} [${w.models.join(", ")}]`);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
// src/init-repo.ts
|
|
1733
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1734
|
+
import { dirname as dirname2, join as join4 } from "node:path";
|
|
1735
|
+
var GH_WORKFLOW = `name: Disclosure
|
|
1736
|
+
on: pull_request
|
|
1737
|
+
jobs:
|
|
1738
|
+
madeby:
|
|
1739
|
+
runs-on: ubuntu-latest
|
|
1740
|
+
steps:
|
|
1741
|
+
- uses: actions/checkout@v4
|
|
1742
|
+
with:
|
|
1743
|
+
fetch-depth: 0
|
|
1744
|
+
- uses: MacDougherty/MadeBy/packages/action@v0.1.0
|
|
1745
|
+
`;
|
|
1746
|
+
function writeIfAbsent(abs, rel, content, created, skipped) {
|
|
1747
|
+
if (existsSync3(abs)) {
|
|
1748
|
+
skipped.push(rel);
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
mkdirSync2(dirname2(abs), { recursive: true });
|
|
1752
|
+
writeFileSync2(abs, content);
|
|
1753
|
+
created.push(rel);
|
|
1754
|
+
}
|
|
1755
|
+
function initRepo(root, opts = {}) {
|
|
1756
|
+
const mode = opts.mode ?? "advisory";
|
|
1757
|
+
const host = opts.host ?? "github";
|
|
1758
|
+
const created = [];
|
|
1759
|
+
const skipped = [];
|
|
1760
|
+
writeIfAbsent(join4(root, ".madeby", "policy.json"), ".madeby/policy.json", JSON.stringify({ version: 0, mode }, null, 2) + "\n", created, skipped);
|
|
1761
|
+
if (host === "github") {
|
|
1762
|
+
writeIfAbsent(join4(root, ".github", "workflows", "disclosure.yml"), ".github/workflows/disclosure.yml", GH_WORKFLOW, created, skipped);
|
|
1763
|
+
}
|
|
1764
|
+
const nextSteps = ["Commit the created files."];
|
|
1765
|
+
if (host === "azure-devops") {
|
|
1766
|
+
nextSteps.push("Add the Azure Pipelines template (packages/action/azure-pipelines-disclosure.yml) and wire it as a PR build-validation branch policy.");
|
|
1767
|
+
} else if (host !== "github") {
|
|
1768
|
+
nextSteps.push(`Add a CI step that runs 'npx madeby check' on pull requests for ${host}.`);
|
|
1769
|
+
}
|
|
1770
|
+
if (mode !== "required") {
|
|
1771
|
+
nextSteps.push(`When ready to enforce: set mode to "required", then make the check required on the protected branch \u2014 the host's control plane (GitHub: gh api branch protection; Azure: a build-validation policy). See the MCP resource madeby://guide/enforce for exact commands.`);
|
|
1772
|
+
}
|
|
1773
|
+
nextSteps.push("Contributors disclose origin with a Co-Authored-By / Generated-by trailer (AI), an Authored-by-human trailer ('madeby me'), a DCO Signed-off-by, a signed commit, or 'madeby ai' (record your own AI session's witnessed spans).");
|
|
1774
|
+
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.");
|
|
1775
|
+
return { created, skipped, policyMode: mode, host, nextSteps };
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
// src/commands/init.ts
|
|
1779
|
+
function initCommand(args) {
|
|
1780
|
+
const root = repoRoot();
|
|
1781
|
+
if (!root) {
|
|
1782
|
+
console.error("madeby init: not a git repository.");
|
|
1783
|
+
process.exit(2);
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
const mode = args.includes("--required") ? "required" : "advisory";
|
|
1787
|
+
const hostIdx = args.indexOf("--host");
|
|
1788
|
+
const host = hostIdx >= 0 ? args[hostIdx + 1] : void 0;
|
|
1789
|
+
const r = initRepo(root, { mode, host });
|
|
1790
|
+
for (const f of r.created) console.log(` + ${f}`);
|
|
1791
|
+
for (const f of r.skipped) console.log(` \xB7 ${f} (exists \u2014 left as-is)`);
|
|
1792
|
+
console.log(`
|
|
1793
|
+
madeby init \u2014 policy: ${r.policyMode}, host: ${r.host}`);
|
|
1794
|
+
console.log("Next:");
|
|
1795
|
+
for (const s of r.nextSteps) console.log(` - ${s}`);
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
// src/mcp.ts
|
|
1799
|
+
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
1800
|
+
import { createInterface } from "node:readline";
|
|
1801
|
+
var DEFAULT_PROTOCOL = "2024-11-05";
|
|
1802
|
+
var SERVER_INFO = { name: "madeby", version: "0.1.0" };
|
|
1803
|
+
function rootFor(path) {
|
|
1804
|
+
const cwd = path && path.length ? path : process.cwd();
|
|
1805
|
+
return execFileSync6("git", ["-C", cwd, "-c", "core.quotePath=false", "rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
|
|
1806
|
+
}
|
|
1807
|
+
var repoInput = {
|
|
1808
|
+
type: "object",
|
|
1809
|
+
properties: {
|
|
1810
|
+
path: { type: "string", description: "path to the repo (default: the server's working directory)" },
|
|
1811
|
+
range: { type: "string", description: "git commit range, e.g. origin/main..HEAD (default: recent history)" }
|
|
1812
|
+
}
|
|
1813
|
+
};
|
|
1814
|
+
var TOOLS = [
|
|
1815
|
+
{
|
|
1816
|
+
name: "init",
|
|
1817
|
+
description: "Align a repository with MadeBy in one call: create .madeby/policy.json (advisory by default \u2014 reports, never fails) and the CI disclosure check for the host (github default; azure-devops guided). Idempotent \u2014 never clobbers an existing policy or workflow. Returns the files created/skipped and the recommended next steps. Read the resources: madeby://guide/align (full workflow), madeby://guide/enforce (make the check required \u2014 the host's control plane, exact commands), madeby://guide/backfill (account for existing history honestly \u2014 never fabricate).",
|
|
1818
|
+
inputSchema: {
|
|
1819
|
+
type: "object",
|
|
1820
|
+
properties: {
|
|
1821
|
+
path: { type: "string", description: "path to the repo (default: the server's working directory)" },
|
|
1822
|
+
mode: { enum: ["advisory", "required"], description: "policy mode to seed (default: advisory)" },
|
|
1823
|
+
host: { type: "string", description: "CI host to wire: github (default) or azure-devops" }
|
|
1824
|
+
}
|
|
1825
|
+
},
|
|
1826
|
+
handler: (args) => initRepo(rootFor(args.path), { mode: args.mode, host: args.host })
|
|
1827
|
+
},
|
|
1828
|
+
{
|
|
1829
|
+
name: "check",
|
|
1830
|
+
description: "Evaluate a repo's commits against its .madeby/policy.json disclosure policy (the maintainer gate). Returns pass/fail, the policy mode, and the undisclosed commits. The policy file is optional \u2014 with no file (or an invalid one) it degrades to advisory (report-only, never a false gate).",
|
|
1831
|
+
inputSchema: repoInput,
|
|
1832
|
+
handler: (args) => evaluateRepoDisclosure(rootFor(args.path), { range: args.range })
|
|
1833
|
+
},
|
|
1834
|
+
{
|
|
1835
|
+
name: "who",
|
|
1836
|
+
description: "Made by whom? Read what's ON THE RECORD about who made this. With `path`: one file's origin \u2014 witnessed AI spans (model/tool, precise) where captured, last-touch commit disclosure from git blame elsewhere (approximate, labeled), unknown otherwise. With no `path`: a repo-wide map (the orientation to read BEFORE you work) \u2014 the witnessed AI surface plus commit-level disclosure coverage, cheap (no per-file blame). The universal contract is the line range: for a symbol, turn it into a line range with your own tooling (tree-sitter/LSP/ctags) and pass it \u2014 MadeBy never parses code. Disclosure, not detection: it reports what's disclosed and never asserts 'human'.",
|
|
1837
|
+
inputSchema: {
|
|
1838
|
+
type: "object",
|
|
1839
|
+
properties: {
|
|
1840
|
+
path: { type: "string", description: "repo-relative file to read one file's origin (omit for a repo-wide map)" },
|
|
1841
|
+
repo: { type: "string", description: "path to the repo (default: the server's working directory)" },
|
|
1842
|
+
startLine: { type: "number", description: "with `path`: optional 1-based start of a line range" },
|
|
1843
|
+
endLine: { type: "number", description: "with `path`: optional 1-based end of a line range" },
|
|
1844
|
+
prefix: { type: "string", description: "map only: limit to a path prefix, e.g. src/" },
|
|
1845
|
+
commitLimit: { type: "number", description: "map only: recent commits to include in the coverage figure (default 500)" }
|
|
1846
|
+
}
|
|
1847
|
+
},
|
|
1848
|
+
handler: (args) => args.path ? provenanceOf(rootFor(args.repo), args.path, {
|
|
1849
|
+
startLine: args.startLine,
|
|
1850
|
+
endLine: args.endLine
|
|
1851
|
+
}) : provenanceMap(rootFor(args.repo), { prefix: args.prefix, commitLimit: args.commitLimit })
|
|
1852
|
+
},
|
|
1853
|
+
{
|
|
1854
|
+
name: "ai",
|
|
1855
|
+
description: "Made by AI (WRITE): record witnessed AI-authorship spans from YOUR OWN session log into the repo's .madeby/spans (asserted tier, self-reported) \u2014 how an agent DISCLOSES its own contribution. Honest by construction: it records only spans whose AI-authored content is STRUCTURALLY PRESENT in the checkout (survives reformatting); anything not present is discarded. Writes only derived attribution, locally \u2014 the log/content never leave the machine. It cannot claim a higher tier or attribute work to anyone else.",
|
|
1856
|
+
inputSchema: {
|
|
1857
|
+
type: "object",
|
|
1858
|
+
properties: {
|
|
1859
|
+
path: { type: "string", description: "path to the repo (default: the server's working directory)" },
|
|
1860
|
+
log: { type: "string", description: "session transcript path (default: auto-discover the Claude Code transcript)" },
|
|
1861
|
+
ref: { type: "string", description: "commit the spans anchor to (default: HEAD)" }
|
|
1862
|
+
}
|
|
1863
|
+
},
|
|
1864
|
+
handler: (args) => recordAiSpans(rootFor(args.path), { transcriptPath: args.log, ref: args.ref })
|
|
1865
|
+
}
|
|
1866
|
+
];
|
|
1867
|
+
var ALIGN_GUIDE = `# Aligning a repository with MadeBy
|
|
1868
|
+
|
|
1869
|
+
MadeBy is **disclosure, not detection** \u2014 it records what a commit *states* about its origin, never
|
|
1870
|
+
guesses whether code is AI. To align a repo:
|
|
1871
|
+
|
|
1872
|
+
1. **Assess** \u2014 call \`check\` (or \`who\` with no path for a repo-wide map) to see current coverage.
|
|
1873
|
+
2. **Set up** \u2014 call \`init\`: it writes \`.madeby/policy.json\` (advisory) and the CI disclosure check.
|
|
1874
|
+
Idempotent; it never clobbers an existing policy or workflow. Then commit the created files.
|
|
1875
|
+
3. **Disclose going forward** \u2014 contributors disclose origin with a Co-Authored-By / Generated-by
|
|
1876
|
+
trailer (AI), an \`Authored-by-human\` trailer (work they wrote themselves \u2014 the symmetric human
|
|
1877
|
+
claim; \`madeby me\` adds it), a DCO Signed-off-by, a signed commit, or \`ai\` (record your own AI
|
|
1878
|
+
session's witnessed spans, structurally verified and local). Human and AI disclosure ride the same ladder.
|
|
1879
|
+
4. **Account for existing history honestly** \u2014 do NOT fabricate disclosure for past commits. Read the
|
|
1880
|
+
\`madeby://guide/backfill\` resource: recognize what's already disclosed, attach recoverable
|
|
1881
|
+
evidence, and set an adoption boundary \u2014 pre-boundary history is labeled *unknown*, never invented.
|
|
1882
|
+
5. **Enforce when ready** \u2014 set the policy mode to \`required\`, then make the check required on the
|
|
1883
|
+
protected branch. That last step is the host's control plane (your credentials), so MadeBy doesn't
|
|
1884
|
+
do it for you \u2014 the exact commands are in the \`madeby://guide/enforce\` resource.
|
|
1885
|
+
6. **Verify** \u2014 call \`check\` again.
|
|
1886
|
+
|
|
1887
|
+
Nothing is hosted; nothing leaves the CI. The maintainer owns the policy.
|
|
1888
|
+
`;
|
|
1889
|
+
var ENFORCE_GUIDE = `# Enforcing the MadeBy disclosure check
|
|
1890
|
+
|
|
1891
|
+
MadeBy sets the policy (mode: required) but does NOT change your repository's protection settings \u2014
|
|
1892
|
+
that needs your credentials and is the host's control plane, so you (or an agent acting as you) make
|
|
1893
|
+
the check required. MadeBy deliberately keeps this an explicit, reviewable action.
|
|
1894
|
+
|
|
1895
|
+
## GitHub (needs repo admin + the gh CLI, authenticated)
|
|
1896
|
+
|
|
1897
|
+
1. Set the policy to required (or run: madeby init --required):
|
|
1898
|
+
in .madeby/policy.json set "mode": "required".
|
|
1899
|
+
2. Find the check's name: open any pull request and read the Checks tab. For the generated workflow
|
|
1900
|
+
the job name is "madeby".
|
|
1901
|
+
3. Add it as a required status check on the protected branch (e.g. main). If protection already
|
|
1902
|
+
exists, add the context:
|
|
1903
|
+
|
|
1904
|
+
echo '["madeby"]' | gh api -X POST \\
|
|
1905
|
+
repos/OWNER/REPO/branches/main/protection/required_status_checks/contexts --input -
|
|
1906
|
+
|
|
1907
|
+
If the branch has no protection yet, create it with a required_status_checks block that lists
|
|
1908
|
+
"madeby" (PUT repos/OWNER/REPO/branches/main/protection --input protection.json).
|
|
1909
|
+
|
|
1910
|
+
Confirm against the GitHub REST docs for "branch protection" and verify with the actual check name
|
|
1911
|
+
from a PR.
|
|
1912
|
+
|
|
1913
|
+
## Azure DevOps (build validation branch policy)
|
|
1914
|
+
|
|
1915
|
+
Project Settings -> Repositories -> (repo) -> Policies -> Branch (main) -> Build Validation -> Add:
|
|
1916
|
+
point it at the pipeline that includes azure-pipelines-disclosure.yml; set trigger automatic and
|
|
1917
|
+
required. CLI equivalent: az repos policy build create ... --branch main --blocking true.
|
|
1918
|
+
`;
|
|
1919
|
+
var BACKFILL_GUIDE = `# Honest backfill for an existing repo
|
|
1920
|
+
|
|
1921
|
+
You cannot retroactively disclose past commits by inventing origin \u2014 that is fabrication, and MadeBy
|
|
1922
|
+
refuses it. But you CAN make an honest, verifiable statement about history. Backfill in layers, and
|
|
1923
|
+
never raise a commit's origin above its evidence \u2014 unknown stays unknown, just explicitly so.
|
|
1924
|
+
|
|
1925
|
+
1. **Recognize what's already there** (free, pure evidence). Run \`check\` over FULL history:
|
|
1926
|
+
existing Co-Authored-By AI trailers, DCO Signed-off-by, signed commits, and bot authorship are
|
|
1927
|
+
recognized now even though they predate adoption. Much of your history may already be disclosed.
|
|
1928
|
+
|
|
1929
|
+
2. **Attach recoverable evidence** (opt-in, evidence-grade):
|
|
1930
|
+
- Old AI-tool session logs -> \`ai\` attaches structurally-verified spans to their commits (it
|
|
1931
|
+
only matches content actually present, so it cannot over-claim). Pass the transcript + the ref.
|
|
1932
|
+
- Committed AI-tool configs (.cursor/, CLAUDE.md, ...) are repo-level evidence the repo used AI.
|
|
1933
|
+
|
|
1934
|
+
3. **Set the adoption boundary** (the watermark). The commit that introduced .madeby/policy.json is
|
|
1935
|
+
your dated adoption boundary:
|
|
1936
|
+
|
|
1937
|
+
git log --diff-filter=A --format=%H -- .madeby/policy.json | tail -1
|
|
1938
|
+
|
|
1939
|
+
Report coverage RELATIVE to it: commits after the boundary are in-regime and measured; commits
|
|
1940
|
+
before are UNKNOWN-origin (never "human", never "AI") unless independently disclosed by step 1/2.
|
|
1941
|
+
This makes the gap explicit and dated instead of hidden.
|
|
1942
|
+
|
|
1943
|
+
4. **Optionally self-attest** what you can stand behind (asserted tier). The author may assert
|
|
1944
|
+
specific pre-boundary facts they honestly know ("I solo-authored the initial import"; "vendor/ is
|
|
1945
|
+
third-party"). It is labeled self-reported (asserted) \u2014 never proof \u2014 and is contestable.
|
|
1946
|
+
|
|
1947
|
+
The invariant: backfill adds only evidence you can point to, or an explicitly-labeled assertion; it
|
|
1948
|
+
never fabricates disclosure or upgrades origin beyond its evidence.
|
|
1949
|
+
`;
|
|
1950
|
+
var POLICY_SCHEMA = {
|
|
1951
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1952
|
+
title: ".madeby/policy.json",
|
|
1953
|
+
type: "object",
|
|
1954
|
+
required: ["version", "mode"],
|
|
1955
|
+
properties: {
|
|
1956
|
+
version: { const: 0 },
|
|
1957
|
+
mode: { enum: ["advisory", "required"], description: "advisory: report only (the default, and the safe degrade); required: fail on any undisclosed commit" },
|
|
1958
|
+
accept: { type: "array", items: { type: "string" }, description: "restrict what satisfies the policy (e.g. dco-signoff, ai-trailer, commit-signature); omit \u21D2 any recognized disclosure counts" }
|
|
1959
|
+
}
|
|
1960
|
+
};
|
|
1961
|
+
var RESOURCES = [
|
|
1962
|
+
{ uri: "madeby://guide/align", name: "Aligning a repo with MadeBy", description: "The step-by-step workflow to align a repository (for humans and agents).", mimeType: "text/markdown", text: ALIGN_GUIDE },
|
|
1963
|
+
{ uri: "madeby://guide/enforce", name: "Enforcing the disclosure check", description: "How to make the check required on a protected branch (GitHub / Azure DevOps) \u2014 the host's control plane, exact commands.", mimeType: "text/markdown", text: ENFORCE_GUIDE },
|
|
1964
|
+
{ uri: "madeby://guide/backfill", name: "Honest backfill for existing repos", description: "How to account for pre-adoption history honestly \u2014 recognize, attach evidence, set a boundary; never fabricate.", mimeType: "text/markdown", text: BACKFILL_GUIDE },
|
|
1965
|
+
{ uri: "madeby://schema/policy", name: ".madeby/policy.json schema", description: "JSON Schema for the disclosure policy file.", mimeType: "application/json", text: JSON.stringify(POLICY_SCHEMA, null, 2) }
|
|
1966
|
+
];
|
|
1967
|
+
function send(obj) {
|
|
1968
|
+
process.stdout.write(JSON.stringify(obj) + "\n");
|
|
1969
|
+
}
|
|
1970
|
+
function handle(msg) {
|
|
1971
|
+
const { id, method, params } = msg;
|
|
1972
|
+
switch (method) {
|
|
1973
|
+
case "initialize":
|
|
1974
|
+
send({
|
|
1975
|
+
jsonrpc: "2.0",
|
|
1976
|
+
id,
|
|
1977
|
+
result: {
|
|
1978
|
+
protocolVersion: params?.protocolVersion ?? DEFAULT_PROTOCOL,
|
|
1979
|
+
capabilities: { tools: {}, resources: {} },
|
|
1980
|
+
serverInfo: SERVER_INFO
|
|
1981
|
+
}
|
|
1982
|
+
});
|
|
1983
|
+
return;
|
|
1984
|
+
case "notifications/initialized":
|
|
1985
|
+
case "notifications/cancelled":
|
|
1986
|
+
return;
|
|
1987
|
+
// notifications get no response
|
|
1988
|
+
case "ping":
|
|
1989
|
+
send({ jsonrpc: "2.0", id, result: {} });
|
|
1990
|
+
return;
|
|
1991
|
+
case "tools/list":
|
|
1992
|
+
send({ jsonrpc: "2.0", id, result: { tools: TOOLS.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })) } });
|
|
1993
|
+
return;
|
|
1994
|
+
case "resources/list":
|
|
1995
|
+
send({ jsonrpc: "2.0", id, result: { resources: RESOURCES.map(({ uri, name, description, mimeType }) => ({ uri, name, description, mimeType })) } });
|
|
1996
|
+
return;
|
|
1997
|
+
case "resources/read": {
|
|
1998
|
+
const found = RESOURCES.find((r) => r.uri === params?.uri);
|
|
1999
|
+
if (!found) {
|
|
2000
|
+
send({ jsonrpc: "2.0", id, error: { code: -32602, message: `unknown resource: ${String(params?.uri)}` } });
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
2003
|
+
send({ jsonrpc: "2.0", id, result: { contents: [{ uri: found.uri, mimeType: found.mimeType, text: found.text }] } });
|
|
2004
|
+
return;
|
|
2005
|
+
}
|
|
2006
|
+
case "tools/call": {
|
|
2007
|
+
const tool = TOOLS.find((t) => t.name === params?.name);
|
|
2008
|
+
if (!tool) {
|
|
2009
|
+
send({ jsonrpc: "2.0", id, error: { code: -32602, message: `unknown tool: ${String(params?.name)}` } });
|
|
2010
|
+
return;
|
|
2011
|
+
}
|
|
2012
|
+
try {
|
|
2013
|
+
const out = tool.handler(params?.arguments ?? {});
|
|
2014
|
+
send({ jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify(out, null, 2) }] } });
|
|
2015
|
+
} catch (e) {
|
|
2016
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
2017
|
+
send({ jsonrpc: "2.0", id, result: { content: [{ type: "text", text: `error: ${message}` }], isError: true } });
|
|
2018
|
+
}
|
|
2019
|
+
return;
|
|
2020
|
+
}
|
|
2021
|
+
default:
|
|
2022
|
+
if (id !== void 0) send({ jsonrpc: "2.0", id, error: { code: -32601, message: `method not found: ${String(method)}` } });
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
function startMcpServer() {
|
|
2026
|
+
const rl = createInterface({ input: process.stdin });
|
|
2027
|
+
rl.on("line", (line) => {
|
|
2028
|
+
const trimmed = line.trim();
|
|
2029
|
+
if (!trimmed) return;
|
|
2030
|
+
let msg;
|
|
2031
|
+
try {
|
|
2032
|
+
msg = JSON.parse(trimmed);
|
|
2033
|
+
} catch {
|
|
2034
|
+
return;
|
|
2035
|
+
}
|
|
2036
|
+
handle(msg);
|
|
2037
|
+
});
|
|
2038
|
+
process.stderr.write("madeby mcp: ready (stdio; tools: init, check, who, ai; resources: align/enforce/backfill guides, policy schema)\n");
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
// src/madeby.ts
|
|
2042
|
+
function help() {
|
|
2043
|
+
console.log(`madeby \u2014 verifiable content provenance ("made by whom?")
|
|
2044
|
+
|
|
2045
|
+
Usage:
|
|
2046
|
+
madeby init Set MadeBy up: write .madeby/policy.json + the CI disclosure check.
|
|
2047
|
+
--required to enforce now; --host azure-devops for Azure Pipelines.
|
|
2048
|
+
madeby check [<range>] The gate: do commits meet .madeby/policy.json? (a verdict \u2014 exit
|
|
2049
|
+
code gates). <range> e.g. origin/main..HEAD; in CI it's auto-detected.
|
|
2050
|
+
--json for a machine-readable result.
|
|
2051
|
+
madeby who [<path>] Made by whom? Read what's on the record about who made this \u2014 one
|
|
2052
|
+
(alias: whom) file's origin, or the whole repo with no path. --lines=A-B to scope.
|
|
2053
|
+
madeby ai [<log>] [<ref>] Made by AI: record your AI session's witnessed spans into .madeby/spans.
|
|
2054
|
+
<log> defaults to the auto-discovered Claude Code transcript.
|
|
2055
|
+
madeby me Made by me: affirm you authored HEAD yourself (an Authored-by-human
|
|
2056
|
+
trailer \u2014 the symmetric human claim to \`ai\`).
|
|
2057
|
+
madeby mcp Run the MCP server (stdio) \u2014 the tools, agent-callable.
|
|
2058
|
+
madeby help Show this help.
|
|
2059
|
+
|
|
2060
|
+
Disclosure, never detection: madeby states what origin was disclosed; it never asserts whether
|
|
2061
|
+
code is AI. What a project requires is set in its .madeby/policy.json \u2014 the maintainer's call.`);
|
|
2062
|
+
}
|
|
2063
|
+
var [cmd, ...rest] = process.argv.slice(2);
|
|
2064
|
+
switch (cmd) {
|
|
2065
|
+
case "init":
|
|
2066
|
+
initCommand(rest);
|
|
2067
|
+
break;
|
|
2068
|
+
case "check":
|
|
2069
|
+
checkCommand(rest);
|
|
2070
|
+
break;
|
|
2071
|
+
case "who":
|
|
2072
|
+
case "whom":
|
|
2073
|
+
whoCommand(rest);
|
|
2074
|
+
break;
|
|
2075
|
+
case "ai":
|
|
2076
|
+
aiCommand(rest);
|
|
2077
|
+
break;
|
|
2078
|
+
case "me":
|
|
2079
|
+
meCommand(rest);
|
|
2080
|
+
break;
|
|
2081
|
+
case "mcp":
|
|
2082
|
+
startMcpServer();
|
|
2083
|
+
break;
|
|
2084
|
+
case void 0:
|
|
2085
|
+
case "help":
|
|
2086
|
+
case "--help":
|
|
2087
|
+
case "-h":
|
|
2088
|
+
help();
|
|
2089
|
+
break;
|
|
2090
|
+
default:
|
|
2091
|
+
console.error(`madeby: unknown command '${cmd}'
|
|
2092
|
+
`);
|
|
2093
|
+
help();
|
|
2094
|
+
process.exit(2);
|
|
2095
|
+
}
|