workproof 0.1.1 → 0.1.3

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 CHANGED
@@ -1,9 +1,14 @@
1
1
  <p align="center"><img src="assets/wordmark.svg" width="480" alt="workproof"></p>
2
2
 
3
+ <p align="center">English | <a href="README.tr.md">Türkçe</a></p>
4
+
3
5
  <p align="center"><em>Your best work is in private repos. Prove it anyway.</em></p>
4
6
 
5
7
  <p align="center">
6
8
  <img src="https://img.shields.io/npm/v/workproof?style=flat-square&color=111111&label=npm" alt="npm">
9
+ <img src="https://img.shields.io/npm/dm/workproof?style=flat-square&color=111111" alt="npm downloads">
10
+ <img src="https://img.shields.io/github/actions/workflow/status/Bubblegunn/workproof/ci.yml?style=flat-square&color=111111&label=ci" alt="ci">
11
+ <img src="https://img.shields.io/bundlephobia/minzip/workproof?style=flat-square&color=111111" alt="minzipped size">
7
12
  <img src="https://img.shields.io/github/stars/Bubblegunn/workproof?style=flat-square&color=111111" alt="stars">
8
13
  <img src="https://img.shields.io/badge/license-MIT-111111?style=flat-square" alt="MIT">
9
14
  </p>
@@ -119,15 +124,91 @@ workproof verify <report.json> [--repo <dir>]...
119
124
  --repo <dir> repository to analyse (repeatable; several produce one combined report)
120
125
  --since / --until override the tenure window
121
126
  --sample <n> blame every n-th file (default: 1; 7 for very large repositories)
127
+ --max-commits <n> read only the newest n commits (escape hatch for enormous histories)
122
128
  --depth <n> directory depth for ownership (default: 2)
123
129
  --paths include directory paths
124
130
  --emails include author emails
125
131
  --narrate append a model-written paragraph
132
+ --badge also write <out>.badge.json, a shields.io endpoint document
126
133
  --out <basename> output basename (default: workproof-report)
127
- --json print the JSON to stdout instead of writing files
134
+ --format <mode> write markdown, json, or both (default: both); json prints to stdout
135
+ --json same as --format json
136
+ ```
137
+
138
+ A `.mailmap` in the repository merges an author's several addresses. Progress lines go to
139
+ stderr while history is read and files are blamed, so a long run is visibly alive; on a
140
+ history of hundreds of thousands of commits, `--max-commits` bounds the read and the report
141
+ records that it did.
142
+
143
+ ## Badge
144
+
145
+ `--badge` writes `workproof-report.badge.json` next to the report, in the
146
+ [shields.io endpoint format](https://shields.io/badges/endpoint-badge):
147
+
148
+ ```json
149
+ { "schemaVersion": 1, "label": "workproof", "message": "70.6% surviving lines · 24.1% commits", "color": "1f3fbf" }
150
+ ```
151
+
152
+ Commit it to a public repository (your portfolio, a gist) and point shields at the raw URL:
153
+
154
+ ```
155
+ ![workproof](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/<you>/<repo>/main/workproof-report.badge.json&style=flat-square)
156
+ ```
157
+
158
+ A badge is a claim, not evidence. Keep the JSON report next to it; the report is what a
159
+ reader verifies, the badge is only how they find it.
160
+
161
+ ## GitHub Action
162
+
163
+ The repository ships a composite action that runs workproof on a checkout and posts one
164
+ sticky comment on the pull request with the six figures. `fetch-depth: 0` is required, or
165
+ the history the figures come from is missing; `author` is required because a GitHub login
166
+ does not map reliably onto commit identities.
167
+
168
+ ```yaml
169
+ name: workproof
170
+ on:
171
+ pull_request:
172
+ permissions:
173
+ contents: read
174
+ pull-requests: write
175
+ jobs:
176
+ report:
177
+ runs-on: ubuntu-latest
178
+ steps:
179
+ - uses: actions/checkout@v4
180
+ with:
181
+ fetch-depth: 0
182
+ - uses: actions/setup-node@v4
183
+ with:
184
+ node-version: 22
185
+ - uses: Bubblegunn/workproof@main
186
+ with:
187
+ author: ada@example.com
188
+ sample: "1"
128
189
  ```
129
190
 
130
- A `.mailmap` in the repository merges an author's several addresses.
191
+ The comment is updated in place on later pushes (it carries a marker), and the JSON report
192
+ stays in the workspace as `workproof-report.json` for an `upload-artifact` step if you want
193
+ the evidence kept with the run. Set `comment: "false"` to only produce the files.
194
+
195
+ ## Can it be gamed?
196
+
197
+ Partly, and the report is built so the gaming shows.
198
+
199
+ - Commit spam moves commit share and cadence, and nothing else. Surviving lines come from
200
+ `git blame` at HEAD, so a thousand empty commits add zero surviving lines, and the gap
201
+ between the two shares is printed side by side.
202
+ - Vendoring a library inflates lines added. The languages figure and the owned-directory
203
+ list (with `--paths`) show where those lines landed, and a reviewer sees a directory
204
+ named `vendor` or `node_modules` owning most of them.
205
+ - Rewriting history to change authorship changes the root commit or HEAD, so the
206
+ fingerprint and HEAD in an older report stop matching.
207
+ - The verifier runs against the same repository. A report that does not reproduce is worse
208
+ than no report, which is the incentive the tool relies on.
209
+
210
+ What it cannot catch: a genuinely large, low-value contribution. That is what references
211
+ are for.
131
212
 
132
213
  ## For candidates
133
214
 
package/README.tr.md ADDED
@@ -0,0 +1,188 @@
1
+ <p align="center"><img src="assets/wordmark.svg" width="480" alt="workproof"></p>
2
+
3
+ <p align="center"><a href="README.md">English</a> | Türkçe</p>
4
+
5
+ <p align="center"><em>En iyi işin özel repolarda. Yine de kanıtla.</em></p>
6
+
7
+ <p align="center">
8
+ <img src="https://img.shields.io/npm/v/workproof?style=flat-square&color=111111&label=npm" alt="npm">
9
+ <img src="https://img.shields.io/github/stars/Bubblegunn/workproof?style=flat-square&color=111111" alt="stars">
10
+ <img src="https://img.shields.io/badge/license-MIT-111111?style=flat-square" alt="MIT">
11
+ </p>
12
+
13
+ workproof, bir git deposunu tek bir yazar için doğrulanabilir bir mühendislik raporuna
14
+ çevirir; hiçbir kod göstermeden. Paylaşamadığınız depoda çalıştırırsınız. Okuyan kişi altı
15
+ figür, her birinin arkasındaki tam komutu, her birinin neyi gösteremediğini ve bir hash alır.
16
+ Aynı depoya sahip herkes `verify` çalıştırıp sayıların yeniden üretilip üretilmediğini görür.
17
+
18
+ ## 30 saniye
19
+
20
+ ```
21
+ cd your-private-repo
22
+ npx workproof
23
+ ```
24
+
25
+ Bu, `workproof-report.md` (bir özgeçmişe, portfolyoya, vize başvurusuna yapıştırın) ve
26
+ `workproof-report.json` (araçlar ve doğrulama için) dosyalarını yazar. Birinin raporunu
27
+ kontrol etmek için:
28
+
29
+ ```
30
+ npx workproof verify workproof-report.json
31
+ ```
32
+
33
+ ## Bir rapor neye benzer
34
+
35
+ Bu, [langchain-ai/openwiki](https://github.com/langchain-ai/openwiki) deposunun `1e6d54c`
36
+ sürümünde bir bakımcı için 5 Eylül 2026'da `--author "Colin Francis" --sample 5` ile alınan
37
+ gerçek çıktıdır; yollar ve e-postalar gizli (varsayılanlar):
38
+
39
+ ```
40
+ ## openwiki
41
+
42
+ HEAD 1e6d54cdfeec · fingerprint 82aa401bbba056f1 · identities: Colin Francis
43
+
44
+ ### Tenure window
45
+ 2026-07-06 to 2026-09-03 (60 days)
46
+
47
+ ### Share of commits in tenure
48
+ 71 of 295 non-merge commits, 24.1%
49
+
50
+ ### Cadence
51
+ 9 active weeks of 9, 7.9 commits per active week, longest streak 9 weeks
52
+ 1 of 21 release tags in tenure
53
+
54
+ ### Footprint
55
+ 694 files touched
56
+ 16 directories with a commit share at or above the threshold (paths hidden; run with --paths)
57
+ languages by lines added: TypeScript 80.4%, JSON 9.6%, Markdown 7.0%, JavaScript 1.6%, YAML 1.4%
58
+
59
+ ### Tests and documentation
60
+ 393 of 657 test-file changes, 59.8%
61
+ 116 documents authored
62
+
63
+ ### Surviving lines at HEAD
64
+ 23,317 of 33,038 surviving lines, 70.6% (files 123/548, sample 1 in 5)
65
+ ```
66
+
67
+ Her figürün altında rapor iki satır daha basar: onu üreten git komutuyla `How:` ve
68
+ `What this cannot show:`. Son bölüm `Integrity`: rapor hash'i ve depo parmak izi.
69
+
70
+ İki payı birlikte okuyun. Bu kişi penceresindeki commit'lerin %24,1'ini ve hâlâ yaşayan
71
+ satırların %70,6'sını yazmış. Bir commit sayısı onu küçük bir katkıcı olarak adlandırırdı. Bu
72
+ boşluk, iki yönde de, bir raporun birinin işi hakkında söyleyebileceği en dürüst şeydir.
73
+
74
+ ## Ne ölçer
75
+
76
+ Altı figürün hepsi git'ten gelir, başka hiçbir şeyden değil.
77
+
78
+ | figür | ne | neyi gösteremez |
79
+ |---|---|---|
80
+ | Görev penceresi | yazarın ilk ve son commit'i, ya da `--since/--until` | ilk commit'ten önceki ya da sonuncudan sonraki iş |
81
+ | Commit payı | penceredeki merge dışı commit'ler içinde yazarın merge dışı commit'leri | neyin sağ kaldığı; bir yazım hatası ile bir alt sistem aynı sayılır |
82
+ | Tempo | aktif haftalar, aktif hafta başına commit, en uzun seri, penceredeki sürüm etiketleri ve yazarınkiler | bir commit'lik hafta ile kırk commit'lik hafta ikisi de aktif sayılır |
83
+ | Ayak izi | dokunulan dosyalar, commit payı eşiğin üstündeki dizinler, eklenen satıra göre diller | üretilmiş ve vendored dosyalar commit'leyeni şişirir |
84
+ | Testler ve dokümanlar | test dosyası değişikliklerinin payı, yazılan dokümanlar | test vakaları, kapsam ya da bir dokümanın kalitesi |
85
+ | Hayatta kalan satırlar | HEAD'de yaşayan satırların payı, deterministik dosya örneği üzerinde `git blame -w -M`, [surviving-lines](https://github.com/Bubblegunn/surviving-lines) ile | liyakat; bilerek silinen kod kimseye sayılmaz |
86
+
87
+ ## Doğrulama nasıl çalışır
88
+
89
+ - JSON deponun HEAD'ini, bir **parmak izi** (kök commit ve normalleştirilmiş remote adresinin
90
+ sha256'sı; depo adı verilmeden tanımlanır), kullanılan kimlik adlarını, `surviving-lines`
91
+ sürümünü, her parametreyi ve parametreler ile figürlerin bir **hash**'ini taşır.
92
+ - `workproof verify report.json` her figürü gösterdiğiniz depoda yeniden hesaplar ve bir
93
+ eşleşme tablosu basar. HEAD rapordan beri ilerlediyse bunu söyler ve hangi figürlerin
94
+ değiştiğini gösterir.
95
+ - Bir işe alım yöneticisinin iki şeye ihtiyacı vardır: rapor ve depoya okuma erişimi (ya da
96
+ şirket içinde tek komutu çalıştıracak bir çalışan). Depodan hiçbir şey çıkmaz.
97
+
98
+ ## Gizlilik
99
+
100
+ - Kod içeriği, asla. Araç `git log --numstat` ve `git blame` okur ve sayılar üretir.
101
+ - Varsayılan olarak dosya yolu yok. `--paths`, yapılandırılan `--depth` (varsayılan 2)
102
+ derinliğinde dizin adları ekler, dosya asla.
103
+ - Varsayılan olarak e-posta adresi yok. `--emails` ekler; onsuz, yazdığınız `--author`
104
+ bile saklanan parametrelerde `(email hidden)` ile değiştirilir.
105
+ - İsteğe bağlı anlatı (`--narrate`) figürleri, yalnızca figürleri, seçtiğiniz bir model uç
106
+ noktasına gönderir (`WORKPROOF_API_URL`, `WORKPROOF_API_KEY`, `WORKPROOF_MODEL`;
107
+ OpenAI uyumlu ya da Anthropic). Paragraf "Generated narrative (not verified)" başlığı
108
+ altına eklenir ve hash'in dışındadır.
109
+
110
+ ## Seçenekler
111
+
112
+ ```
113
+ workproof [options] [--repo <dir>]...
114
+ workproof verify <report.json> [--repo <dir>]...
115
+
116
+ --author <email|name> identity to report on (repeatable; default: git config user.email)
117
+ --repo <dir> repository to analyse (repeatable; several produce one combined report)
118
+ --since / --until override the tenure window
119
+ --sample <n> blame every n-th file (default: 1; 7 for very large repositories)
120
+ --max-commits <n> read only the newest n commits (escape hatch for enormous histories)
121
+ --depth <n> directory depth for ownership (default: 2)
122
+ --paths include directory paths
123
+ --emails include author emails
124
+ --narrate append a model-written paragraph
125
+ --out <basename> output basename (default: workproof-report)
126
+ --json print the JSON to stdout instead of writing files
127
+ ```
128
+
129
+ Depodaki bir `.mailmap`, bir yazarın birden çok adresini birleştirir. Geçmiş okunurken ve
130
+ dosyalar blame'lenirken ilerleme satırları stderr'e gider, uzun bir çalıştırma canlı
131
+ görünür; yüz binlerce commit'lik bir geçmişte `--max-commits` okumayı sınırlar ve rapor bunu
132
+ kaydeder.
133
+
134
+ ## Oyunlanabilir mi?
135
+
136
+ Kısmen; rapor, oyunun görünmesi için tasarlandı.
137
+
138
+ - Commit spam'i commit payını ve tempoyu oynatır, başka bir şeyi değil. Hayatta kalan satırlar
139
+ HEAD'deki `git blame`'den gelir; bin boş commit sıfır hayatta kalan satır ekler ve iki pay
140
+ yan yana basılır.
141
+ - Bir kütüphaneyi vendor'lamak eklenen satırları şişirir. Diller figürü ve (`--paths` ile)
142
+ sahiplenilen dizin listesi o satırların nereye düştüğünü gösterir; inceleyen, satırların
143
+ çoğunu `vendor` ya da `node_modules` adlı bir dizinin sahiplendiğini görür.
144
+ - Yazarlığı değiştirmek için geçmişi yeniden yazmak kök commit'i ya da HEAD'i değiştirir;
145
+ eski bir rapordaki parmak izi ve HEAD eşleşmez olur.
146
+ - Doğrulayıcı aynı depoya karşı çalışır. Yeniden üretilemeyen bir rapor, hiç rapor
147
+ olmamasından kötüdür; aracın dayandığı teşvik budur.
148
+
149
+ Yakalayamadığı: gerçekten büyük ama düşük değerli bir katkı. Referanslar bunun içindir.
150
+
151
+ ## Adaylar için
152
+
153
+ Gurur duyduğunuz ve gösteremediğiniz her depoda çalıştırın. Markdown'ı portfolyonuza, zaten
154
+ yazacağınız cümlenin ("frontend'i ben yaptım") yanına koyun ve sayılar cümleyi taşısın. JSON'u
155
+ saklayın; inceleyenin doğruladığı odur.
156
+
157
+ ## İşe alanlar için
158
+
159
+ JSON'u ve adayın eski şirketinden birinin üzerinde `npx workproof verify` çalıştırmasını
160
+ isteyin. Tablo ya yeniden üretilir ya üretilmez. Depo ilerlediyse araç hangi figürlerin
161
+ değiştiğini ve bunun neden beklendiğini söyler.
162
+
163
+ ## Vize ve göç kanıtı için
164
+
165
+ workproof, en güçlü işin özel depolarda olduğu ve "bana güven"in kanıt sayılmadığı bir
166
+ Birleşik Krallık Global Talent başvurusu için yapıldı. Bir rapor, yöntemi ekli bir ölçümdür,
167
+ bir tavsiye değil; orada bulunmuş insanların mektuplarıyla birlikte kullanın.
168
+
169
+ ## Bunu yapmaz
170
+
171
+ Hayatta kalmayı ve etkinliği ölçer; kaliteyi, incelemeyi, tasarımı ya da mentorluğu değil.
172
+ İnsanları sıralamaz. Referansların yerine geçmez. Hukuki bir belge değildir.
173
+
174
+ ## Nereden geliyor
175
+
176
+ Yöntem
177
+ [How to show engineering ownership when the repositories are private](https://efe-genc-portfolio.vercel.app/writing/showing-ownership-private-repositories/)
178
+ yazısında anlatılıyor. Blame örneklemesi workproof'un tek bağımlılığı olan
179
+ [surviving-lines](https://github.com/Bubblegunn/surviving-lines) paketidir.
180
+
181
+ ## Geliştirme
182
+
183
+ ```
184
+ npm ci
185
+ npm test # tsc build, then node:test over the compiled tests (fixture repositories built in a temp dir)
186
+ ```
187
+
188
+ MIT.
@@ -9,6 +9,12 @@ export interface Params {
9
9
  minCommits: number;
10
10
  paths: boolean;
11
11
  emails: boolean;
12
+ /** Escape hatch for enormous histories: read only the newest n commits. */
13
+ maxCommits?: number;
14
+ }
15
+ export interface AnalyseHooks {
16
+ /** Called with short status lines while history is read and files are blamed. */
17
+ progress?: (message: string) => void;
12
18
  }
13
19
  export interface RepoReport {
14
20
  name: string;
@@ -23,4 +29,4 @@ export interface RepoReport {
23
29
  }
24
30
  /** sha256 of the root commit and the normalised remote: identifies a repository without naming it. */
25
31
  export declare function fingerprint(root: string, remote: string): string;
26
- export declare function analyseRepo(cwd: string, params: Params): Promise<RepoReport>;
32
+ export declare function analyseRepo(cwd: string, params: Params, hooks?: AnalyseHooks): Promise<RepoReport>;
@@ -22,9 +22,12 @@ const survivingVersion = () => {
22
22
  return "unknown";
23
23
  }
24
24
  };
25
- export async function analyseRepo(cwd, params) {
25
+ export async function analyseRepo(cwd, params, hooks = {}) {
26
+ const say = hooks.progress ?? (() => { });
26
27
  await assertRepository(cwd);
27
- const all = await listCommits(cwd, {});
28
+ say(`${basename(cwd)}: reading history${params.maxCommits ? ` (newest ${params.maxCommits} commits)` : ""}...`);
29
+ const all = await listCommits(cwd, params.maxCommits ? { max: params.maxCommits } : {});
30
+ say(`${basename(cwd)}: ${all.length.toLocaleString("en-US")} commits read`);
28
31
  const id = await resolveIdentity(all, params.author, cwd);
29
32
  const t = tenure(all, id, { ...(params.since ? { since: params.since } : {}), ...(params.until ? { until: params.until } : {}) });
30
33
  const start = new Date(t.value.first + "T00:00:00Z");
@@ -42,8 +45,11 @@ export async function analyseRepo(cwd, params) {
42
45
  cadence(inTenure, tags, id, { first: t.value.first, last: t.value.last }),
43
46
  fp,
44
47
  testsAndDocs(inTenure, id),
45
- await survivingLines(cwd, id, { sample, version: survivingVersion() }),
46
48
  ];
49
+ say(`${basename(cwd)}: blaming files (1 in ${sample} sample)...`);
50
+ const surviving = await survivingLines(cwd, id, { sample, version: survivingVersion() });
51
+ say(`${basename(cwd)}: blamed ${surviving.value.filesSampled} of ${surviving.value.filesTotal} files`);
52
+ figures.push(surviving);
47
53
  return {
48
54
  name: basename(cwd),
49
55
  head: await headSha(cwd),
@@ -0,0 +1,13 @@
1
+ import type { Report } from "./report.js";
2
+ /** shields.io endpoint document: https://shields.io/badges/endpoint-badge */
3
+ export interface Badge {
4
+ schemaVersion: 1;
5
+ label: string;
6
+ message: string;
7
+ color: string;
8
+ }
9
+ /**
10
+ * A badge for the first repository in the report. It is a claim, not evidence:
11
+ * the JSON report next to it is what a reader verifies.
12
+ */
13
+ export declare function badgeFor(report: Report): Badge;
@@ -0,0 +1,20 @@
1
+ const pct = (x) => `${(x * 100).toFixed(1)}%`;
2
+ /**
3
+ * A badge for the first repository in the report. It is a claim, not evidence:
4
+ * the JSON report next to it is what a reader verifies.
5
+ */
6
+ export function badgeFor(report) {
7
+ const repo = report.repositories[0];
8
+ if (!repo)
9
+ throw new Error("the report has no repositories");
10
+ const surviving = repo.figures.find((f) => f.id === "survivingLines");
11
+ const commits = repo.figures.find((f) => f.id === "commitShare");
12
+ if (!surviving || !commits)
13
+ throw new Error("the report has no surviving-lines or commit-share figure");
14
+ return {
15
+ schemaVersion: 1,
16
+ label: "workproof",
17
+ message: `${pct(surviving.value.share)} surviving lines · ${pct(commits.value.share)} commits`,
18
+ color: "1f3fbf",
19
+ };
20
+ }
package/dist/src/cli.d.ts CHANGED
@@ -1,11 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import type { Params } from "./index.js";
3
+ type OutputFormat = "both" | "markdown" | "json";
3
4
  interface Cli {
4
5
  params: Params;
5
6
  repos: string[];
6
7
  out: string;
8
+ format: OutputFormat;
7
9
  json: boolean;
8
10
  doNarrate: boolean;
11
+ badge: boolean;
9
12
  verifyFile: string | undefined;
10
13
  }
11
14
  export declare function parse(argv: string[]): Cli;
package/dist/src/cli.js CHANGED
@@ -3,7 +3,7 @@ import { readFile, writeFile } from "node:fs/promises";
3
3
  import { resolve } from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import { createRequire } from "node:module";
6
- import { analyseRepo, buildReport, renderMarkdown, verifyReport, narrate } from "./index.js";
6
+ import { analyseRepo, buildReport, renderMarkdown, verifyReport, narrate, badgeFor } from "./index.js";
7
7
  const HELP = `usage: workproof [options] [--repo <dir>]...
8
8
  workproof verify <report.json> [--repo <dir>]...
9
9
 
@@ -13,20 +13,25 @@ Turn a git repository into a verifiable engineering report for one author, witho
13
13
  --repo <dir> repository to analyse (repeatable; default: current directory)
14
14
  --since / --until override the tenure window (dates git understands)
15
15
  --sample <n> blame every n-th file (default: 1, or 7 for very large repositories)
16
+ --max-commits <n> read only the newest n commits (escape hatch for enormous histories)
16
17
  --depth <n> directory depth for ownership (default: 2)
17
18
  --paths include directory paths in the report (off by default)
18
19
  --emails include author emails in the report (off by default)
19
20
  --narrate append a model-written paragraph; needs WORKPROOF_API_URL, WORKPROOF_API_KEY, WORKPROOF_MODEL
21
+ --badge also write <out>.badge.json, a shields.io endpoint document
20
22
  --out <basename> output basename (default: workproof-report)
21
- --json print the JSON to stdout instead of writing files
23
+ --format <mode> output markdown, json, or both (default: both)
24
+ --json print the JSON to stdout instead of writing files (legacy alias)
22
25
  -h, --help this text`;
23
26
  export function parse(argv) {
24
27
  const params = { depth: 2, threshold: 0.5, minCommits: 5, paths: false, emails: false };
25
28
  const repos = [];
26
29
  const authors = [];
27
30
  let out = "workproof-report";
31
+ let format = "both";
28
32
  let json = false;
29
33
  let doNarrate = false;
34
+ let badge = false;
30
35
  let verifyFile;
31
36
  if (argv[0] === "verify") {
32
37
  verifyFile = argv[1];
@@ -52,6 +57,11 @@ export function parse(argv) {
52
57
  params.until = next();
53
58
  else if (a === "--sample")
54
59
  params.sample = Number(next());
60
+ else if (a === "--max-commits") {
61
+ params.maxCommits = Number(next());
62
+ if (!Number.isInteger(params.maxCommits) || params.maxCommits < 1)
63
+ throw new Error("--max-commits must be an integer >= 1");
64
+ }
55
65
  else if (a === "--depth")
56
66
  params.depth = Number(next());
57
67
  else if (a === "--paths")
@@ -60,10 +70,20 @@ export function parse(argv) {
60
70
  params.emails = true;
61
71
  else if (a === "--narrate")
62
72
  doNarrate = true;
73
+ else if (a === "--badge")
74
+ badge = true;
63
75
  else if (a === "--out")
64
76
  out = next();
65
- else if (a === "--json")
77
+ else if (a === "--format") {
78
+ const value = next();
79
+ if (value !== "both" && value !== "markdown" && value !== "json")
80
+ throw new Error("--format must be markdown, json, or both");
81
+ format = value;
82
+ }
83
+ else if (a === "--json") {
66
84
  json = true;
85
+ format = "json";
86
+ }
67
87
  else if (a === "-h" || a === "--help") {
68
88
  console.log(HELP);
69
89
  process.exit(0);
@@ -75,10 +95,12 @@ export function parse(argv) {
75
95
  params.author = authors;
76
96
  if (!repos.length)
77
97
  repos.push(process.cwd());
78
- return { params, repos, out, json, doNarrate, verifyFile };
98
+ return { params, repos, out, format, json, doNarrate, badge, verifyFile };
79
99
  }
80
100
  async function main() {
81
- const { params, repos, out, json, doNarrate, verifyFile } = parse(process.argv.slice(2));
101
+ const started = Date.now();
102
+ const { params, repos, out, format, json, doNarrate, badge, verifyFile } = parse(process.argv.slice(2));
103
+ const progress = (m) => process.stderr.write(`${m}\n`);
82
104
  if (verifyFile) {
83
105
  const report = JSON.parse(await readFile(verifyFile, "utf8"));
84
106
  const result = await verifyReport(report, repos);
@@ -93,7 +115,7 @@ async function main() {
93
115
  const version = createRequire(import.meta.url)("../../package.json").version;
94
116
  const repositories = [];
95
117
  for (const dir of repos)
96
- repositories.push(await analyseRepo(dir, params));
118
+ repositories.push(await analyseRepo(dir, params, { progress }));
97
119
  const report = buildReport(repositories, params, { version, generatedAt: new Date().toISOString() });
98
120
  let narrative;
99
121
  if (doNarrate) {
@@ -104,13 +126,24 @@ async function main() {
104
126
  throw new Error("--narrate needs WORKPROOF_API_URL, WORKPROOF_API_KEY and WORKPROOF_MODEL");
105
127
  narrative = await narrate(report, { url, key, model });
106
128
  }
107
- if (json) {
129
+ if (format === "json") {
108
130
  console.log(JSON.stringify(report, null, 2));
109
131
  return;
110
132
  }
111
- await writeFile(`${out}.json`, JSON.stringify(report, null, 2));
112
- await writeFile(`${out}.md`, renderMarkdown(report, narrative));
113
- console.log(`wrote ${out}.md and ${out}.json`);
133
+ const written = [];
134
+ if (format === "markdown" || format === "both") {
135
+ await writeFile(`${out}.md`, renderMarkdown(report, narrative));
136
+ written.push(`${out}.md`);
137
+ }
138
+ if (format === "both") {
139
+ await writeFile(`${out}.json`, JSON.stringify(report, null, 2));
140
+ written.push(`${out}.json`);
141
+ }
142
+ if (badge) {
143
+ await writeFile(`${out}.badge.json`, JSON.stringify(badgeFor(report), null, 2));
144
+ written.push(`${out}.badge.json`);
145
+ }
146
+ console.log(`wrote ${written.join(" and ")} in ${((Date.now() - started) / 1000).toFixed(1)}s`);
114
147
  }
115
148
  const entry = process.argv[1] ? pathToFileURL(process.argv[1]).href : "";
116
149
  if (entry === import.meta.url || entry.endsWith("/workproof")) {
@@ -2,7 +2,8 @@ import { isMine } from "./identity.js";
2
2
  const LANG = {
3
3
  ts: "TypeScript", tsx: "TypeScript", js: "JavaScript", jsx: "JavaScript", mjs: "JavaScript", cjs: "JavaScript",
4
4
  py: "Python", cs: "C#", go: "Go", rs: "Rust", java: "Java", kt: "Kotlin", swift: "Swift", rb: "Ruby", php: "PHP",
5
- c: "C", h: "C", cpp: "C++", hpp: "C++", css: "CSS", scss: "CSS", html: "HTML", vue: "Vue", svelte: "Svelte",
5
+ c: "C", h: "C", cpp: "C++", hpp: "C++", ex: "Elixir", exs: "Elixir", scala: "Scala", sc: "Scala", hs: "Haskell", lhs: "Haskell", lua: "Lua", r: "R", m: "Objective-C", mm: "Objective-C", zig: "Zig",
6
+ css: "CSS", scss: "CSS", html: "HTML", vue: "Vue", svelte: "Svelte",
6
7
  sql: "SQL", sh: "Shell", yml: "YAML", yaml: "YAML", json: "JSON", md: "Markdown", mdx: "Markdown", tf: "Terraform", dart: "Dart",
7
8
  };
8
9
  export function languageOf(path) {
package/dist/src/git.d.ts CHANGED
@@ -16,6 +16,7 @@ export interface Commit {
16
16
  export declare function listCommits(cwd: string, opts: {
17
17
  since?: string;
18
18
  until?: string;
19
+ max?: number;
19
20
  }): Promise<Commit[]>;
20
21
  export interface Tag {
21
22
  name: string;
package/dist/src/git.js CHANGED
@@ -12,6 +12,8 @@ export async function listCommits(cwd, opts) {
12
12
  args.push(`--since=${opts.since}`);
13
13
  if (opts.until)
14
14
  args.push(`--until=${opts.until}`);
15
+ if (opts.max)
16
+ args.push(`--max-count=${opts.max}`);
15
17
  const out = await git(args, cwd);
16
18
  const commits = [];
17
19
  for (const block of out.split("\x1e")) {
@@ -1,8 +1,10 @@
1
1
  export { analyseRepo, fingerprint } from "./analyse.js";
2
- export type { Params, RepoReport } from "./analyse.js";
2
+ export type { Params, RepoReport, AnalyseHooks } from "./analyse.js";
3
3
  export { buildReport, renderMarkdown } from "./report.js";
4
4
  export type { Report } from "./report.js";
5
5
  export { verifyReport } from "./verify.js";
6
6
  export type { VerifyRow } from "./verify.js";
7
7
  export { narrate } from "./narrate.js";
8
8
  export type { Figure, Identity } from "./figures/types.js";
9
+ export { badgeFor } from "./badge.js";
10
+ export type { Badge } from "./badge.js";
package/dist/src/index.js CHANGED
@@ -2,3 +2,4 @@ export { analyseRepo, fingerprint } from "./analyse.js";
2
2
  export { buildReport, renderMarkdown } from "./report.js";
3
3
  export { verifyReport } from "./verify.js";
4
4
  export { narrate } from "./narrate.js";
5
+ export { badgeFor } from "./badge.js";
package/package.json CHANGED
@@ -1,25 +1,56 @@
1
1
  {
2
2
  "name": "workproof",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Turn a private git repository into a verifiable engineering report for one author, without showing any code.",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",
7
7
  "types": "./dist/src/index.d.ts",
8
- "exports": { ".": { "types": "./dist/src/index.d.ts", "import": "./dist/src/index.js" } },
9
- "bin": { "workproof": "./dist/src/cli.js" },
10
- "files": ["dist/src", "README.md", "LICENSE"],
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/src/index.d.ts",
11
+ "import": "./dist/src/index.js"
12
+ }
13
+ },
14
+ "bin": {
15
+ "workproof": "./dist/src/cli.js"
16
+ },
17
+ "files": [
18
+ "dist/src",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
11
22
  "scripts": {
12
23
  "build": "tsc -p tsconfig.json",
13
24
  "lint": "tsc -p tsconfig.json --noEmit",
14
25
  "test": "npm run build && node --test dist/test/figures.test.js dist/test/report.test.js dist/test/cli.test.js",
15
- "prepublishOnly": "npm test"
26
+ "prepublishOnly": "npm test",
27
+ "examples": "npm run build && node examples/basic.mjs"
28
+ },
29
+ "engines": {
30
+ "node": ">=20"
16
31
  },
17
- "engines": { "node": ">=20" },
18
- "keywords": ["git", "portfolio", "proof-of-work", "career", "blame", "engineering-evidence"],
32
+ "keywords": [
33
+ "git",
34
+ "portfolio",
35
+ "proof-of-work",
36
+ "career",
37
+ "blame",
38
+ "engineering-evidence"
39
+ ],
19
40
  "author": "Efe Genc",
20
41
  "license": "MIT",
21
- "repository": { "type": "git", "url": "git+https://github.com/Bubblegunn/workproof.git" },
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/Bubblegunn/workproof.git"
45
+ },
22
46
  "homepage": "https://github.com/Bubblegunn/workproof#readme",
23
- "dependencies": { "surviving-lines": "^0.1.1" },
24
- "devDependencies": { "@types/node": "^22.15.0", "typescript": "^5.8.0" }
47
+ "dependencies": {
48
+ "surviving-lines": "^0.1.1"
49
+ },
50
+ "devDependencies": {
51
+ "@arethetypeswrong/cli": "^0.18.5",
52
+ "@types/node": "^26.4.1",
53
+ "publint": "^0.3.24",
54
+ "typescript": "^7.0.2"
55
+ }
25
56
  }