workproof 0.1.2 → 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
@@ -6,6 +6,9 @@
6
6
 
7
7
  <p align="center">
8
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">
9
12
  <img src="https://img.shields.io/github/stars/Bubblegunn/workproof?style=flat-square&color=111111" alt="stars">
10
13
  <img src="https://img.shields.io/badge/license-MIT-111111?style=flat-square" alt="MIT">
11
14
  </p>
@@ -126,8 +129,10 @@ workproof verify <report.json> [--repo <dir>]...
126
129
  --paths include directory paths
127
130
  --emails include author emails
128
131
  --narrate append a model-written paragraph
132
+ --badge also write <out>.badge.json, a shields.io endpoint document
129
133
  --out <basename> output basename (default: workproof-report)
130
- --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
131
136
  ```
132
137
 
133
138
  A `.mailmap` in the repository merges an author's several addresses. Progress lines go to
@@ -135,6 +140,58 @@ stderr while history is read and files are blamed, so a long run is visibly aliv
135
140
  history of hundreds of thousands of commits, `--max-commits` bounds the read and the report
136
141
  records that it did.
137
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"
189
+ ```
190
+
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
+
138
195
  ## Can it be gamed?
139
196
 
140
197
  Partly, and the report is built so the gaming shows.
@@ -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
 
@@ -18,16 +18,20 @@ Turn a git repository into a verifiable engineering report for one author, witho
18
18
  --paths include directory paths in the report (off by default)
19
19
  --emails include author emails in the report (off by default)
20
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
21
22
  --out <basename> output basename (default: workproof-report)
22
- --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)
23
25
  -h, --help this text`;
24
26
  export function parse(argv) {
25
27
  const params = { depth: 2, threshold: 0.5, minCommits: 5, paths: false, emails: false };
26
28
  const repos = [];
27
29
  const authors = [];
28
30
  let out = "workproof-report";
31
+ let format = "both";
29
32
  let json = false;
30
33
  let doNarrate = false;
34
+ let badge = false;
31
35
  let verifyFile;
32
36
  if (argv[0] === "verify") {
33
37
  verifyFile = argv[1];
@@ -66,10 +70,20 @@ export function parse(argv) {
66
70
  params.emails = true;
67
71
  else if (a === "--narrate")
68
72
  doNarrate = true;
73
+ else if (a === "--badge")
74
+ badge = true;
69
75
  else if (a === "--out")
70
76
  out = next();
71
- 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") {
72
84
  json = true;
85
+ format = "json";
86
+ }
73
87
  else if (a === "-h" || a === "--help") {
74
88
  console.log(HELP);
75
89
  process.exit(0);
@@ -81,11 +95,11 @@ export function parse(argv) {
81
95
  params.author = authors;
82
96
  if (!repos.length)
83
97
  repos.push(process.cwd());
84
- return { params, repos, out, json, doNarrate, verifyFile };
98
+ return { params, repos, out, format, json, doNarrate, badge, verifyFile };
85
99
  }
86
100
  async function main() {
87
101
  const started = Date.now();
88
- const { params, repos, out, json, doNarrate, verifyFile } = parse(process.argv.slice(2));
102
+ const { params, repos, out, format, json, doNarrate, badge, verifyFile } = parse(process.argv.slice(2));
89
103
  const progress = (m) => process.stderr.write(`${m}\n`);
90
104
  if (verifyFile) {
91
105
  const report = JSON.parse(await readFile(verifyFile, "utf8"));
@@ -112,13 +126,24 @@ async function main() {
112
126
  throw new Error("--narrate needs WORKPROOF_API_URL, WORKPROOF_API_KEY and WORKPROOF_MODEL");
113
127
  narrative = await narrate(report, { url, key, model });
114
128
  }
115
- if (json) {
129
+ if (format === "json") {
116
130
  console.log(JSON.stringify(report, null, 2));
117
131
  return;
118
132
  }
119
- await writeFile(`${out}.json`, JSON.stringify(report, null, 2));
120
- await writeFile(`${out}.md`, renderMarkdown(report, narrative));
121
- console.log(`wrote ${out}.md and ${out}.json in ${((Date.now() - started) / 1000).toFixed(1)}s`);
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`);
122
147
  }
123
148
  const entry = process.argv[1] ? pathToFileURL(process.argv[1]).href : "";
124
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) {
@@ -6,3 +6,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.2",
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
  }