claude-attribution 1.2.7 → 1.2.9
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/package.json +1 -1
- package/src/attribution/minimap.ts +160 -0
- package/src/cli.ts +1 -1
- package/src/commands/init.ts +33 -131
- package/src/commands/pr.ts +6 -3
- package/src/setup/install.ts +119 -1
- package/src/setup/templates/pr-metrics-workflow.yml +3 -1
package/package.json
CHANGED
|
@@ -176,3 +176,163 @@ export async function listMinimapNotes(repoRoot: string): Promise<string[]> {
|
|
|
176
176
|
return [];
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
|
+
|
|
180
|
+
// ─── Bulk init helpers ────────────────────────────────────────────────────────
|
|
181
|
+
// Exported for use by install.ts without importing init.ts (which auto-executes
|
|
182
|
+
// main() at module level and cannot be statically imported from other modules).
|
|
183
|
+
|
|
184
|
+
const CONCURRENCY = 8;
|
|
185
|
+
|
|
186
|
+
function buildResult(sha: string, files: MinimapFileState[]): MinimapResult {
|
|
187
|
+
let totalAi = 0,
|
|
188
|
+
totalHuman = 0,
|
|
189
|
+
totalLines = 0;
|
|
190
|
+
for (const f of files) {
|
|
191
|
+
totalAi += f.ai;
|
|
192
|
+
totalHuman += f.human;
|
|
193
|
+
totalLines += f.total;
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
commit: sha,
|
|
197
|
+
timestamp: new Date().toISOString(),
|
|
198
|
+
files,
|
|
199
|
+
totals: {
|
|
200
|
+
ai: totalAi,
|
|
201
|
+
human: totalHuman,
|
|
202
|
+
total: totalLines,
|
|
203
|
+
pctAi: totalLines > 0 ? Math.round((totalAi / totalLines) * 100) : 0,
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Mark all currently tracked files as AI-written at HEAD.
|
|
210
|
+
* Used by `claude-attribution init --ai` and the install baseline prompt.
|
|
211
|
+
*/
|
|
212
|
+
export async function buildAllAiMinimap(
|
|
213
|
+
repoRoot: string,
|
|
214
|
+
): Promise<MinimapResult> {
|
|
215
|
+
const sha = await run("git", ["rev-parse", "HEAD"], repoRoot);
|
|
216
|
+
const lsOutput = await run("git", ["ls-files"], repoRoot);
|
|
217
|
+
const allFiles = lsOutput ? lsOutput.split("\n").filter(Boolean) : [];
|
|
218
|
+
|
|
219
|
+
const minimapFiles: MinimapFileState[] = [];
|
|
220
|
+
for (let i = 0; i < allFiles.length; i += CONCURRENCY) {
|
|
221
|
+
const batch = allFiles.slice(i, i + CONCURRENCY);
|
|
222
|
+
const results = await Promise.all(
|
|
223
|
+
batch.map(async (relPath): Promise<MinimapFileState | null> => {
|
|
224
|
+
try {
|
|
225
|
+
const content = await run(
|
|
226
|
+
"git",
|
|
227
|
+
["show", `HEAD:${relPath}`],
|
|
228
|
+
repoRoot,
|
|
229
|
+
);
|
|
230
|
+
if (content.includes("\0")) return null;
|
|
231
|
+
const lines = content.split("\n");
|
|
232
|
+
const total = lines.length;
|
|
233
|
+
const aiHashes = new Set<string>();
|
|
234
|
+
for (const line of lines) {
|
|
235
|
+
if (line.trim() !== "") aiHashes.add(hashLine(line));
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
path: relPath,
|
|
239
|
+
ai_hashes: hashSetToString(aiHashes),
|
|
240
|
+
ai: total,
|
|
241
|
+
human: 0,
|
|
242
|
+
total,
|
|
243
|
+
pctAi: total > 0 ? 100 : 0,
|
|
244
|
+
} satisfies MinimapFileState;
|
|
245
|
+
} catch {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
}),
|
|
249
|
+
);
|
|
250
|
+
minimapFiles.push(
|
|
251
|
+
...results.filter((r): r is MinimapFileState => r !== null),
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
return buildResult(sha, minimapFiles);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Mark files modified since sinceDate (YYYY-MM-DD) as AI-written at HEAD.
|
|
259
|
+
* Files with no commits since that date are treated as human-written.
|
|
260
|
+
*/
|
|
261
|
+
export async function buildAiSinceMinimap(
|
|
262
|
+
repoRoot: string,
|
|
263
|
+
sinceDate: string,
|
|
264
|
+
): Promise<MinimapResult> {
|
|
265
|
+
const sha = await run("git", ["rev-parse", "HEAD"], repoRoot);
|
|
266
|
+
const lsOutput = await run("git", ["ls-files"], repoRoot);
|
|
267
|
+
const allFiles = lsOutput ? lsOutput.split("\n").filter(Boolean) : [];
|
|
268
|
+
|
|
269
|
+
// Collect all files changed in commits since the given date
|
|
270
|
+
const commitLog = await run(
|
|
271
|
+
"git",
|
|
272
|
+
["log", `--since=${sinceDate}`, "--format=%H"],
|
|
273
|
+
repoRoot,
|
|
274
|
+
);
|
|
275
|
+
const commits = commitLog ? commitLog.split("\n").filter(Boolean) : [];
|
|
276
|
+
const aiFileSet = new Set<string>();
|
|
277
|
+
for (const commitSha of commits) {
|
|
278
|
+
try {
|
|
279
|
+
const changed = await run(
|
|
280
|
+
"git",
|
|
281
|
+
["diff-tree", "--no-commit-id", "-r", "--name-only", commitSha],
|
|
282
|
+
repoRoot,
|
|
283
|
+
);
|
|
284
|
+
for (const f of changed.split("\n").filter(Boolean)) {
|
|
285
|
+
aiFileSet.add(f);
|
|
286
|
+
}
|
|
287
|
+
} catch {
|
|
288
|
+
// Non-fatal: skip this commit's file list
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const minimapFiles: MinimapFileState[] = [];
|
|
293
|
+
for (let i = 0; i < allFiles.length; i += CONCURRENCY) {
|
|
294
|
+
const batch = allFiles.slice(i, i + CONCURRENCY);
|
|
295
|
+
const results = await Promise.all(
|
|
296
|
+
batch.map(async (relPath): Promise<MinimapFileState | null> => {
|
|
297
|
+
try {
|
|
298
|
+
const content = await run(
|
|
299
|
+
"git",
|
|
300
|
+
["show", `HEAD:${relPath}`],
|
|
301
|
+
repoRoot,
|
|
302
|
+
);
|
|
303
|
+
if (content.includes("\0")) return null;
|
|
304
|
+
const lines = content.split("\n");
|
|
305
|
+
const total = lines.length;
|
|
306
|
+
if (!aiFileSet.has(relPath)) {
|
|
307
|
+
return {
|
|
308
|
+
path: relPath,
|
|
309
|
+
ai_hashes: "",
|
|
310
|
+
ai: 0,
|
|
311
|
+
human: total,
|
|
312
|
+
total,
|
|
313
|
+
pctAi: 0,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
const aiHashes = new Set<string>();
|
|
317
|
+
for (const line of lines) {
|
|
318
|
+
if (line.trim() !== "") aiHashes.add(hashLine(line));
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
path: relPath,
|
|
322
|
+
ai_hashes: hashSetToString(aiHashes),
|
|
323
|
+
ai: total,
|
|
324
|
+
human: 0,
|
|
325
|
+
total,
|
|
326
|
+
pctAi: total > 0 ? 100 : 0,
|
|
327
|
+
} satisfies MinimapFileState;
|
|
328
|
+
} catch {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
}),
|
|
332
|
+
);
|
|
333
|
+
minimapFiles.push(
|
|
334
|
+
...results.filter((r): r is MinimapFileState => r !== null),
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
return buildResult(sha, minimapFiles);
|
|
338
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -92,7 +92,7 @@ Commands:
|
|
|
92
92
|
uninstall [repo] Remove hooks from a repo (default: current directory)
|
|
93
93
|
metrics [id] Generate PR metrics report
|
|
94
94
|
pr [title] Create PR with metrics embedded (--draft, --base <branch>)
|
|
95
|
-
init [--ai
|
|
95
|
+
init [--ai | --ai-since <YYYY-MM-DD>] Set attribution baseline in the cumulative minimap
|
|
96
96
|
start Mark session start for per-ticket scoping
|
|
97
97
|
hook <name> Run an internal hook (used by installed git hooks)
|
|
98
98
|
version Print version
|
package/src/commands/init.ts
CHANGED
|
@@ -1,37 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* claude-attribution init [--ai | --human]
|
|
2
|
+
* claude-attribution init [--ai | --ai-since <YYYY-MM-DD> | --human]
|
|
3
3
|
*
|
|
4
4
|
* Initializes the cumulative attribution minimap for an existing repo.
|
|
5
5
|
*
|
|
6
|
-
* --ai
|
|
7
|
-
*
|
|
8
|
-
* --human
|
|
9
|
-
* are assumed human until Claude writes them.
|
|
10
|
-
*
|
|
11
|
-
* After running init --ai, the next `claude-attribution metrics` or PR will show
|
|
12
|
-
* the true codebase AI% instead of only the current session's delta.
|
|
6
|
+
* --ai Mark all currently tracked files as AI-written.
|
|
7
|
+
* --ai-since <date> Mark files modified since the given date as AI-written.
|
|
8
|
+
* --human (default) Confirm the human default — no minimap written.
|
|
13
9
|
*/
|
|
14
10
|
import { resolve } from "path";
|
|
15
|
-
import { execFile } from "child_process";
|
|
16
|
-
import { promisify } from "util";
|
|
17
|
-
import { hashLine } from "../attribution/differ.ts";
|
|
18
11
|
import {
|
|
19
|
-
|
|
12
|
+
buildAllAiMinimap,
|
|
13
|
+
buildAiSinceMinimap,
|
|
20
14
|
writeMinimap,
|
|
21
|
-
type MinimapFileState,
|
|
22
|
-
type MinimapResult,
|
|
23
15
|
} from "../attribution/minimap.ts";
|
|
24
16
|
|
|
25
|
-
const execFileAsync = promisify(execFile);
|
|
26
|
-
const CONCURRENCY = 8;
|
|
27
|
-
|
|
28
|
-
async function runGit(args: string[], cwd: string): Promise<string> {
|
|
29
|
-
const result = (await execFileAsync("git", args, { cwd })) as unknown as {
|
|
30
|
-
stdout: string;
|
|
31
|
-
};
|
|
32
|
-
return result.stdout.trim();
|
|
33
|
-
}
|
|
34
|
-
|
|
35
17
|
async function main() {
|
|
36
18
|
const repoRoot = resolve(process.cwd());
|
|
37
19
|
const flag = process.argv[2];
|
|
@@ -49,120 +31,40 @@ async function main() {
|
|
|
49
31
|
return;
|
|
50
32
|
}
|
|
51
33
|
|
|
52
|
-
if (flag
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
// --ai: mark entire current codebase as AI-written
|
|
59
|
-
let sha = "";
|
|
60
|
-
try {
|
|
61
|
-
sha = await runGit(["rev-parse", "HEAD"], repoRoot);
|
|
62
|
-
} catch {
|
|
63
|
-
console.error(
|
|
64
|
-
"Error: no commits found. Commit your files first, then run init --ai.",
|
|
34
|
+
if (flag === "--ai") {
|
|
35
|
+
const result = await buildAllAiMinimap(repoRoot);
|
|
36
|
+
await writeMinimap(result, repoRoot);
|
|
37
|
+
const pct = result.totals.pctAi;
|
|
38
|
+
console.log(
|
|
39
|
+
`✓ Marked ${result.files.length} files (${result.totals.total} lines, ${pct}% AI) as AI-written.`,
|
|
65
40
|
);
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const lsOutput = await runGit(["ls-files"], repoRoot);
|
|
70
|
-
const allFiles = lsOutput ? lsOutput.split("\n").filter(Boolean) : [];
|
|
71
|
-
|
|
72
|
-
if (allFiles.length === 0) {
|
|
73
|
-
console.error("Error: no tracked files found.");
|
|
74
|
-
process.exit(1);
|
|
41
|
+
return;
|
|
75
42
|
}
|
|
76
43
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
let processed = 0;
|
|
83
|
-
|
|
84
|
-
for (let i = 0; i < allFiles.length; i += CONCURRENCY) {
|
|
85
|
-
const batch = allFiles.slice(i, i + CONCURRENCY);
|
|
86
|
-
const batchResults = await Promise.all(
|
|
87
|
-
batch.map(async (relPath): Promise<MinimapFileState | null> => {
|
|
88
|
-
try {
|
|
89
|
-
const result = (await execFileAsync(
|
|
90
|
-
"git",
|
|
91
|
-
["show", `HEAD:${relPath}`],
|
|
92
|
-
{ cwd: repoRoot },
|
|
93
|
-
)) as unknown as { stdout: string };
|
|
94
|
-
const content = result.stdout;
|
|
95
|
-
|
|
96
|
-
// Skip binary files
|
|
97
|
-
if (content.includes("\0")) return null;
|
|
98
|
-
|
|
99
|
-
const lines = content.split("\n");
|
|
100
|
-
const total = lines.length;
|
|
101
|
-
|
|
102
|
-
// init --ai is an explicit declaration: every line (including blank)
|
|
103
|
-
// is AI-written. Build ai_hashes from all non-blank lines so the
|
|
104
|
-
// carry-forward algorithm in future commits works correctly; count
|
|
105
|
-
// blank lines as AI in the totals.
|
|
106
|
-
const aiHashes = new Set<string>();
|
|
107
|
-
for (const line of lines) {
|
|
108
|
-
if (line.trim() !== "") {
|
|
109
|
-
aiHashes.add(hashLine(line));
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return {
|
|
114
|
-
path: relPath,
|
|
115
|
-
ai_hashes: hashSetToString(aiHashes),
|
|
116
|
-
ai: total,
|
|
117
|
-
human: 0,
|
|
118
|
-
total,
|
|
119
|
-
pctAi: total > 0 ? 100 : 0,
|
|
120
|
-
} satisfies MinimapFileState;
|
|
121
|
-
} catch {
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
}),
|
|
125
|
-
);
|
|
126
|
-
const valid = batchResults.filter((r): r is MinimapFileState => r !== null);
|
|
127
|
-
minimapFiles.push(...valid);
|
|
128
|
-
processed += batch.length;
|
|
129
|
-
if (processed % 100 === 0 || processed === allFiles.length) {
|
|
130
|
-
process.stdout.write(`\r ${processed} / ${allFiles.length} files...`);
|
|
44
|
+
if (flag === "--ai-since") {
|
|
45
|
+
const date = process.argv[3];
|
|
46
|
+
if (!date) {
|
|
47
|
+
console.error("Usage: claude-attribution init --ai-since <YYYY-MM-DD>");
|
|
48
|
+
process.exit(1);
|
|
131
49
|
}
|
|
50
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
|
51
|
+
console.error(`Invalid date format: ${date}. Expected YYYY-MM-DD.`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
const result = await buildAiSinceMinimap(repoRoot, date);
|
|
55
|
+
await writeMinimap(result, repoRoot);
|
|
56
|
+
const pct = result.totals.pctAi;
|
|
57
|
+
console.log(
|
|
58
|
+
`✓ Marked ${result.files.length} files (${result.totals.total} lines, ${pct}% AI) as AI-written since ${date}.`,
|
|
59
|
+
);
|
|
60
|
+
return;
|
|
132
61
|
}
|
|
133
62
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
totalHuman = 0,
|
|
138
|
-
totalLines = 0;
|
|
139
|
-
for (const f of minimapFiles) {
|
|
140
|
-
totalAi += f.ai;
|
|
141
|
-
totalHuman += f.human;
|
|
142
|
-
totalLines += f.total;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const minimapResult: MinimapResult = {
|
|
146
|
-
commit: sha,
|
|
147
|
-
timestamp: new Date().toISOString(),
|
|
148
|
-
files: minimapFiles,
|
|
149
|
-
totals: {
|
|
150
|
-
ai: totalAi,
|
|
151
|
-
human: totalHuman,
|
|
152
|
-
total: totalLines,
|
|
153
|
-
pctAi: totalLines > 0 ? Math.round((totalAi / totalLines) * 100) : 0,
|
|
154
|
-
},
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
await writeMinimap(minimapResult, repoRoot);
|
|
158
|
-
|
|
159
|
-
const pct = minimapResult.totals.pctAi;
|
|
160
|
-
console.log(
|
|
161
|
-
`✓ Marked ${minimapFiles.length} files (${totalLines} lines, ${pct}% AI) as AI-written.`,
|
|
162
|
-
);
|
|
163
|
-
console.log(
|
|
164
|
-
" Push to share with team: git push origin refs/notes/claude-attribution-map",
|
|
63
|
+
console.error(`Unknown flag: ${flag}`);
|
|
64
|
+
console.error(
|
|
65
|
+
"Usage: claude-attribution init [--ai | --ai-since <YYYY-MM-DD> | --human]",
|
|
165
66
|
);
|
|
67
|
+
process.exit(1);
|
|
166
68
|
}
|
|
167
69
|
|
|
168
70
|
main().catch((err) => {
|
package/src/commands/pr.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { collectMetrics, renderMetrics } from "../metrics/collect.ts";
|
|
|
21
21
|
const execFileAsync = promisify(execFile);
|
|
22
22
|
|
|
23
23
|
const METRICS_PLACEHOLDER = "<!-- claude-attribution metrics -->";
|
|
24
|
+
const METRICS_END = "<!-- /claude-attribution metrics -->";
|
|
24
25
|
|
|
25
26
|
const BUILTIN_TEMPLATE = `## Description
|
|
26
27
|
|
|
@@ -125,12 +126,14 @@ async function main() {
|
|
|
125
126
|
? await readFile(templatePath, "utf8")
|
|
126
127
|
: BUILTIN_TEMPLATE;
|
|
127
128
|
|
|
128
|
-
// Inject metrics
|
|
129
|
+
// Inject metrics wrapped in start/end markers so CI can find and replace on
|
|
130
|
+
// subsequent synchronize events without appending a second block.
|
|
131
|
+
const metricsSection = `${METRICS_PLACEHOLDER}\n${metricsBlock}\n${METRICS_END}`;
|
|
129
132
|
let body: string;
|
|
130
133
|
if (template.includes(METRICS_PLACEHOLDER)) {
|
|
131
|
-
body = template.replace(METRICS_PLACEHOLDER,
|
|
134
|
+
body = template.replace(METRICS_PLACEHOLDER, metricsSection);
|
|
132
135
|
} else {
|
|
133
|
-
body = template.trimEnd() + "\n\n" +
|
|
136
|
+
body = template.trimEnd() + "\n\n" + metricsSection;
|
|
134
137
|
}
|
|
135
138
|
|
|
136
139
|
// Push the current branch to remote. Always push only the branch ref (not
|
package/src/setup/install.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { readFile, writeFile, appendFile, mkdir } from "fs/promises";
|
|
|
9
9
|
import { existsSync } from "fs";
|
|
10
10
|
import { execFile } from "child_process";
|
|
11
11
|
import { promisify } from "util";
|
|
12
|
+
import { createInterface } from "readline";
|
|
12
13
|
import { resolve, join } from "path";
|
|
13
14
|
import {
|
|
14
15
|
ATTRIBUTION_ROOT,
|
|
@@ -20,11 +21,125 @@ import {
|
|
|
20
21
|
type HooksConfig,
|
|
21
22
|
} from "./shared.ts";
|
|
22
23
|
import { configureRequiredCheck } from "./branch-protection.ts";
|
|
24
|
+
import {
|
|
25
|
+
listMinimapNotes,
|
|
26
|
+
writeMinimap,
|
|
27
|
+
buildAllAiMinimap,
|
|
28
|
+
buildAiSinceMinimap,
|
|
29
|
+
} from "../attribution/minimap.ts";
|
|
23
30
|
|
|
24
31
|
const execFileAsync = promisify(execFile);
|
|
25
32
|
|
|
26
33
|
const CLI_BIN = resolve(ATTRIBUTION_ROOT, "bin", "claude-attribution");
|
|
27
34
|
|
|
35
|
+
// ─── Baseline init prompt ────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
38
|
+
|
|
39
|
+
async function promptLine(question: string): Promise<string> {
|
|
40
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
rl.question(question, (answer) => {
|
|
43
|
+
rl.close();
|
|
44
|
+
resolve(answer.trim());
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* After install, ask the developer how to treat existing code in the minimap.
|
|
51
|
+
* Skipped if the minimap was already initialized (flag file or existing notes).
|
|
52
|
+
* Never throws — any failure falls back to a printed note.
|
|
53
|
+
*/
|
|
54
|
+
async function promptBaselineInit(
|
|
55
|
+
repoRoot: string,
|
|
56
|
+
claudeDir: string,
|
|
57
|
+
): Promise<void> {
|
|
58
|
+
const flagPath = join(claudeDir, "attribution-state", "baseline-initialized");
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
// Idempotent: skip if already set
|
|
62
|
+
if (existsSync(flagPath)) return;
|
|
63
|
+
const existingNotes = await listMinimapNotes(repoRoot);
|
|
64
|
+
if (existingNotes.length > 0) return;
|
|
65
|
+
|
|
66
|
+
// Verify there are commits to work with
|
|
67
|
+
try {
|
|
68
|
+
await execFileAsync("git", ["rev-parse", "HEAD"], { cwd: repoRoot });
|
|
69
|
+
} catch {
|
|
70
|
+
console.log(
|
|
71
|
+
"\n ℹ️ Run 'claude-attribution init --ai' after your first commit to set the attribution baseline.",
|
|
72
|
+
);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!process.stdin.isTTY) {
|
|
77
|
+
console.log(
|
|
78
|
+
"\n ℹ️ Run 'claude-attribution init [--ai | --ai-since <YYYY-MM-DD>]' to set the attribution baseline.",
|
|
79
|
+
);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log(
|
|
84
|
+
"\n Attribution baseline — how should existing code be treated?",
|
|
85
|
+
);
|
|
86
|
+
console.log(
|
|
87
|
+
" [1] AI — all current code was written with Claude Code",
|
|
88
|
+
);
|
|
89
|
+
console.log(
|
|
90
|
+
" [2] Human — assume human-written (AI accumulates from here)",
|
|
91
|
+
);
|
|
92
|
+
console.log(
|
|
93
|
+
" [3] AI since — mark commits after a date as AI-written",
|
|
94
|
+
);
|
|
95
|
+
console.log(
|
|
96
|
+
" [4] Skip — decide later (run: claude-attribution init --ai)",
|
|
97
|
+
);
|
|
98
|
+
const raw = await promptLine(" Choice [2]: ");
|
|
99
|
+
const choice = raw === "" ? 2 : parseInt(raw, 10);
|
|
100
|
+
|
|
101
|
+
if (choice === 1) {
|
|
102
|
+
console.log(" Marking all files as AI-written...");
|
|
103
|
+
const result = await buildAllAiMinimap(repoRoot);
|
|
104
|
+
await writeMinimap(result, repoRoot);
|
|
105
|
+
await writeFile(flagPath, "ai");
|
|
106
|
+
const { pctAi, total } = result.totals;
|
|
107
|
+
console.log(
|
|
108
|
+
`✓ Baseline set: AI (${result.files.length} files, ${total} lines, ${pctAi}% AI)`,
|
|
109
|
+
);
|
|
110
|
+
} else if (choice === 2) {
|
|
111
|
+
await writeFile(flagPath, "human");
|
|
112
|
+
console.log(
|
|
113
|
+
"✓ Baseline set: human (AI attribution accumulates from this point)",
|
|
114
|
+
);
|
|
115
|
+
} else if (choice === 3) {
|
|
116
|
+
// Prompt for date with validation loop
|
|
117
|
+
let date = "";
|
|
118
|
+
while (true) {
|
|
119
|
+
date = await promptLine(" Start date (YYYY-MM-DD): ");
|
|
120
|
+
if (DATE_RE.test(date)) break;
|
|
121
|
+
console.log(
|
|
122
|
+
` Invalid format: ${date || "(empty)"}. Use YYYY-MM-DD (e.g. 2026-01-15).`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
console.log(` Marking files changed since ${date} as AI-written...`);
|
|
126
|
+
const result = await buildAiSinceMinimap(repoRoot, date);
|
|
127
|
+
await writeMinimap(result, repoRoot);
|
|
128
|
+
await writeFile(flagPath, `ai-since:${date}`);
|
|
129
|
+
const { pctAi, total } = result.totals;
|
|
130
|
+
console.log(
|
|
131
|
+
`✓ Baseline set: AI since ${date} (${result.files.length} files, ${total} lines, ${pctAi}% AI)`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
// choice === 4 or unrecognized: Skip — write no flag, ask again next install
|
|
135
|
+
} catch {
|
|
136
|
+
console.log("\n ℹ️ Could not set attribution baseline automatically.");
|
|
137
|
+
console.log(
|
|
138
|
+
" Run 'claude-attribution init [--ai | --ai-since <YYYY-MM-DD>]' manually.",
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
28
143
|
async function main() {
|
|
29
144
|
const args = process.argv.slice(2);
|
|
30
145
|
const runnerFlagIdx = args.findIndex((a: string) => a === "--runner");
|
|
@@ -178,7 +293,10 @@ async function main() {
|
|
|
178
293
|
// 5. Check branch protection and offer to add required status check
|
|
179
294
|
await configureRequiredCheck(targetRepo);
|
|
180
295
|
|
|
181
|
-
// 6.
|
|
296
|
+
// 6. Prompt for attribution baseline (skipped on re-runs)
|
|
297
|
+
await promptBaselineInit(targetRepo, claudeDir);
|
|
298
|
+
|
|
299
|
+
// 7. Record installed version for auto-upgrade tracking
|
|
182
300
|
const pkg = JSON.parse(
|
|
183
301
|
await readFile(join(ATTRIBUTION_ROOT, "package.json"), "utf8"),
|
|
184
302
|
) as { version: string };
|
|
@@ -23,7 +23,9 @@ jobs:
|
|
|
23
23
|
git fetch origin refs/notes/claude-attribution-map:refs/notes/claude-attribution-map || true
|
|
24
24
|
|
|
25
25
|
- name: Install claude-attribution
|
|
26
|
-
run:
|
|
26
|
+
run: |
|
|
27
|
+
npm install -g --prefix "${HOME}/.npm-global" claude-attribution
|
|
28
|
+
echo "${HOME}/.npm-global/bin" >> "$GITHUB_PATH"
|
|
27
29
|
|
|
28
30
|
- name: Generate metrics
|
|
29
31
|
run: claude-attribution metrics > /tmp/claude-attribution-metrics.md || true
|