workproof 0.3.0 → 0.4.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 +73 -0
- package/dist/src/analyse.d.ts +6 -0
- package/dist/src/analyse.js +18 -2
- package/dist/src/cli.js +6 -0
- package/dist/src/figures/identity.d.ts +20 -0
- package/dist/src/figures/identity.js +54 -0
- package/dist/src/report.d.ts +0 -1
- package/dist/src/report.js +34 -2
- package/dist/src/schema.js +7 -0
- package/package.json +2 -2
- package/schema/report.schema.json +166 -31
package/README.md
CHANGED
|
@@ -144,6 +144,37 @@ only comparable when that line matches:
|
|
|
144
144
|
How: `git blame --line-porcelain -w -M --ignore-revs-file .git-blame-ignore-revs HEAD -- <file> over a deterministic 1-in-1 file sample (surviving-lines 0.1.1: FNV-1a on path); generated, vendored and lock files excluded`
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
+
## When you are two people
|
|
148
|
+
|
|
149
|
+
Git has no idea that the address on your laptop and the address on your work machine belong to
|
|
150
|
+
one person. It counts them as two authors, so a report about one of them describes half your
|
|
151
|
+
work and says nothing about the rest, and every figure in it is quietly too small. This is the
|
|
152
|
+
most common way these numbers go wrong and nothing normally tells you.
|
|
153
|
+
|
|
154
|
+
The report tells you:
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
**1 other identity in this repository looks like the same person and was not counted here**:
|
|
158
|
+
Efe Genc. The same name, "Efe Genc", on another address. Every figure below therefore describes
|
|
159
|
+
only the work under the identities named above. If they are the same person, re-run with each
|
|
160
|
+
address passed to `--author`, or merge them in a `.mailmap`.
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The same line appears as a warning on stderr while the run is happening, and as
|
|
164
|
+
`identity.possiblySplit` in the JSON. Three signals raise it: the same name on another address,
|
|
165
|
+
a GitHub noreply login that matches your address or your name, and the same address name on
|
|
166
|
+
another domain. Role addresses like `dev@` and bot addresses are ignored, because sharing one
|
|
167
|
+
proves nothing.
|
|
168
|
+
|
|
169
|
+
It is a disclosure, not a correction. The tool cannot know whether two addresses are one person,
|
|
170
|
+
so it never merges them for you: pass each address to `--author`, or write a `.mailmap`, which
|
|
171
|
+
`git log`, `git blame` and `git shortlog` all read as well.
|
|
172
|
+
[`surviving-lines --identities`](https://github.com/Bubblegunn/surviving-lines) prints the
|
|
173
|
+
`.mailmap` lines for a whole repository if you want to fix it once for everyone.
|
|
174
|
+
|
|
175
|
+
The addresses themselves stay out of the document unless you pass `--emails`, the same rule the
|
|
176
|
+
rest of the report follows.
|
|
177
|
+
|
|
147
178
|
## In plain language
|
|
148
179
|
|
|
149
180
|
Each repository section of the Markdown report opens with a paragraph a non-engineer can
|
|
@@ -198,6 +229,16 @@ Three commands, in increasing order of what the reader needs to have.
|
|
|
198
229
|
compares HEAD, then recomputes every figure and prints what differs. A report from another
|
|
199
230
|
repository stops at the fingerprint. If HEAD moved since the report, it says so and shows
|
|
200
231
|
which figures changed.
|
|
232
|
+
The hash is reproducible **across operating systems, git versions, locales and directory
|
|
233
|
+
names**, for the same repository at the same commit with the same command and the same
|
|
234
|
+
`--fingerprint-key`. It is not reproducible across a rewritten history, a different sample
|
|
235
|
+
or a different seed, and it is not meant to be: those change the measurement. Two fields
|
|
236
|
+
in the report are deliberately outside the hash, because a verifier on another machine
|
|
237
|
+
cannot reproduce them: the local directory name, and the git version. They stay in the
|
|
238
|
+
document, where they explain why two runs might legitimately differ. Before 0.4.0 both
|
|
239
|
+
were inside it, so the same repository in a folder with a different name hashed
|
|
240
|
+
differently; a test now pins all of this, and CI checks it on Linux, macOS and Windows.
|
|
241
|
+
|
|
201
242
|
- `workproof attest report.json` writes `report.intoto.json`, an
|
|
202
243
|
[in-toto](https://in-toto.io) v1 Statement whose subject is the report hash and whose
|
|
203
244
|
predicate carries the tool version, parameters, HEAD, keyed fingerprint, git version and
|
|
@@ -452,6 +493,28 @@ with an attestation if the reader cannot reach the repository.
|
|
|
452
493
|
It measures survivorship and activity, not quality, review, design or mentoring. It does
|
|
453
494
|
not rank people. It does not replace references. It is not a legal document.
|
|
454
495
|
|
|
496
|
+
## What the research says about numbers like these
|
|
497
|
+
|
|
498
|
+
Repository-derived figures have a literature, and most of it is a warning. Kalliamvakou,
|
|
499
|
+
Gousios and Blincoe catalogued the traps in mining a forge, including that most repositories
|
|
500
|
+
are personal and inactive and that a large share of merged pull requests are not recorded as
|
|
501
|
+
merged ([MSR 2014](https://doi.org/10.1145/2597073.2597074), extended in [EMSE
|
|
502
|
+
2015](https://doi.org/10.1007/s10664-015-9393-5)). Bird and colleagues related ownership
|
|
503
|
+
concentration to defects rather than to credit, which is a different question from this one
|
|
504
|
+
and is not evidence that a high share means good work. On survival, Spinellis, Louridas and
|
|
505
|
+
Kechagia put the median line lifespan at about 2.4 years across 89 repositories ([PeerJ CS
|
|
506
|
+
7:e372, 2021](https://doi.org/10.7717/peerj-cs.372)), and Gurov found over half of all lines
|
|
507
|
+
are never deleted in 120 TypeScript repositories ([arXiv
|
|
508
|
+
2606.04993](https://arxiv.org/abs/2606.04993)).
|
|
509
|
+
|
|
510
|
+
None of that work measures a person's share of the surviving code, which is what the
|
|
511
|
+
surviving-lines figure here reports. A first measurement of how far that share diverges from
|
|
512
|
+
commit share, over twelve repositories, is in
|
|
513
|
+
[surviving-lines/research](https://github.com/Bubblegunn/surviving-lines/tree/main/research):
|
|
514
|
+
the same person tops both in 25 of 36 runs, and among substantial contributors the two
|
|
515
|
+
differ by a median of 7.5 percentage points. That is the honest size of the effect this
|
|
516
|
+
report exists to expose, measured rather than asserted.
|
|
517
|
+
|
|
455
518
|
## Where it comes from
|
|
456
519
|
|
|
457
520
|
The method is written up in
|
|
@@ -461,6 +524,16 @@ workproof's only dependency. Degree of authorship follows Avelino, Hora and Vale
|
|
|
461
524
|
(2016), major contributors follow Bird et al. (2011), and the absence factor is the CHAOSS
|
|
462
525
|
Contributor Absence Factor.
|
|
463
526
|
|
|
527
|
+
## Thanks
|
|
528
|
+
|
|
529
|
+
[@edwardsong08](https://github.com/edwardsong08) sent two pull requests on the day this was
|
|
530
|
+
published, without knowing me: `--format markdown|json|both`, so a pipeline can ask for one file
|
|
531
|
+
instead of two ([#9](https://github.com/Bubblegunn/workproof/pull/9)), and a language map that
|
|
532
|
+
covers Elixir, Scala, Haskell, Lua, R, Objective-C and Zig
|
|
533
|
+
([#3](https://github.com/Bubblegunn/workproof/pull/3)). Both shipped in 0.1.3 and are in every
|
|
534
|
+
release since. The second one is why the footprint figure names the right language for anyone
|
|
535
|
+
outside the handful I had thought to list.
|
|
536
|
+
|
|
464
537
|
## Development
|
|
465
538
|
|
|
466
539
|
```
|
package/dist/src/analyse.d.ts
CHANGED
|
@@ -38,6 +38,12 @@ export interface RepoReport {
|
|
|
38
38
|
emails: string[];
|
|
39
39
|
names: string[];
|
|
40
40
|
count: number;
|
|
41
|
+
/** Addresses here that look like the subject and were not counted as them. */
|
|
42
|
+
possiblySplit?: {
|
|
43
|
+
names: string[];
|
|
44
|
+
emails?: string[];
|
|
45
|
+
reasons: string[];
|
|
46
|
+
};
|
|
41
47
|
};
|
|
42
48
|
/** What the figures were computed with, so a verifier can tell a drift from an edit. */
|
|
43
49
|
environment: {
|
package/dist/src/analyse.js
CHANGED
|
@@ -8,7 +8,7 @@ import { isBot, excludedSet } from "./exclusions.js";
|
|
|
8
8
|
// surviving-lines ships plain ESM JavaScript without type declarations.
|
|
9
9
|
// @ts-ignore
|
|
10
10
|
import { globToRegExp } from "surviving-lines/bin/surviving-lines.js";
|
|
11
|
-
import { resolveIdentity } from "./figures/identity.js";
|
|
11
|
+
import { resolveIdentity, possibleSplits } from "./figures/identity.js";
|
|
12
12
|
import { tenure, commitShare } from "./figures/commits.js";
|
|
13
13
|
import { cadence } from "./figures/cadence.js";
|
|
14
14
|
import { footprint, testsAndDocs } from "./figures/footprint.js";
|
|
@@ -83,6 +83,9 @@ export async function analyseRepo(cwd, params, hooks = {}) {
|
|
|
83
83
|
const ex = await applyExclusions(cwd, everything, params);
|
|
84
84
|
const all = ex.commits;
|
|
85
85
|
const id = await resolveIdentity(all, params.author, cwd);
|
|
86
|
+
// Before any figure is computed: does someone else here look like the same person? A split
|
|
87
|
+
// identity makes every figure below describe half the work, silently.
|
|
88
|
+
const splits = possibleSplits(all, id);
|
|
86
89
|
const t = tenure(all, id, { ...(params.since ? { since: params.since } : {}), ...(params.until ? { until: params.until } : {}) });
|
|
87
90
|
const start = new Date(t.value.first + "T00:00:00Z");
|
|
88
91
|
const end = new Date(t.value.last + "T23:59:59Z");
|
|
@@ -138,7 +141,20 @@ export async function analyseRepo(cwd, params, hooks = {}) {
|
|
|
138
141
|
head: await headSha(cwd),
|
|
139
142
|
fingerprint: fingerprint(await rootCommit(cwd), await remoteUrl(cwd), key),
|
|
140
143
|
fingerprintKeyed: true,
|
|
141
|
-
identity: {
|
|
144
|
+
identity: {
|
|
145
|
+
emails: params.emails ? [...new Set(id.emails.map(publicEmail))] : [],
|
|
146
|
+
names: id.names,
|
|
147
|
+
count: id.emails.length,
|
|
148
|
+
...(splits.length
|
|
149
|
+
? {
|
|
150
|
+
possiblySplit: {
|
|
151
|
+
names: [...new Set(splits.map((sp) => sp.name))],
|
|
152
|
+
...(params.emails ? { emails: [...new Set(splits.map((sp) => publicEmail(sp.email)))] } : {}),
|
|
153
|
+
reasons: [...new Set(splits.map((sp) => sp.reason))],
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
: {}),
|
|
157
|
+
},
|
|
142
158
|
environment: { git: await gitVersion(cwd), blame, ignoreRevs, seed: params.seed ?? "" },
|
|
143
159
|
excluded: { botCommits: ex.botCommits, files: ex.excluded.size, linesAddedShare: addedAll ? addedExcluded / addedAll : 0, enabled: ex.enabled },
|
|
144
160
|
figures,
|
package/dist/src/cli.js
CHANGED
|
@@ -245,6 +245,12 @@ async function main() {
|
|
|
245
245
|
await writeFile(`${out}.badge.json`, JSON.stringify(badgeFor(report), null, 2));
|
|
246
246
|
written.push(`${out}.badge.json`);
|
|
247
247
|
}
|
|
248
|
+
for (const repo of report.repositories) {
|
|
249
|
+
const split = repo.identity.possiblySplit;
|
|
250
|
+
if (!split)
|
|
251
|
+
continue;
|
|
252
|
+
progress(`${repo.name}: warning, ${split.names.length} other identity in this repository looks like the same person and is not in these figures (${split.names.join(", ")}). Pass every address to --author, or merge them in a .mailmap.`);
|
|
253
|
+
}
|
|
248
254
|
console.log(`wrote ${written.join(" and ")} in ${((Date.now() - started) / 1000).toFixed(1)}s`);
|
|
249
255
|
}
|
|
250
256
|
const entry = process.argv[1] ? pathToFileURL(process.argv[1]).href : "";
|
|
@@ -22,3 +22,23 @@ export declare function foldIdentity(s: string): string;
|
|
|
22
22
|
*/
|
|
23
23
|
export declare function resolveIdentity(commits: Commit[], author: string[] | undefined, cwd: string): Promise<Identity>;
|
|
24
24
|
export declare const isMine: (c: Commit, id: Identity) => boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Addresses in this repository that look like the subject but were not counted as them.
|
|
27
|
+
*
|
|
28
|
+
* A person who commits from a laptop and a work machine is two authors to git, so a report
|
|
29
|
+
* describes half of their work and says nothing about the other half. That is a silent wrong
|
|
30
|
+
* number in a tool whose whole claim is that its numbers can be checked, so the report
|
|
31
|
+
* discloses the suspicion rather than quietly resolving it: only the author knows whether two
|
|
32
|
+
* addresses are one person, and `--author` takes both when they are.
|
|
33
|
+
*/
|
|
34
|
+
export declare function possibleSplits(commits: {
|
|
35
|
+
name: string;
|
|
36
|
+
email: string;
|
|
37
|
+
}[], identity: {
|
|
38
|
+
emails: string[];
|
|
39
|
+
names: string[];
|
|
40
|
+
}): {
|
|
41
|
+
name: string;
|
|
42
|
+
email: string;
|
|
43
|
+
reason: string;
|
|
44
|
+
}[];
|
|
@@ -54,3 +54,57 @@ function authorsHint(commits) {
|
|
|
54
54
|
return top.length ? `Authors here: ${top.join(", ")}. Pass one with --author.` : "The repository has no commits.";
|
|
55
55
|
}
|
|
56
56
|
export const isMine = (c, id) => id.emails.includes(c.email);
|
|
57
|
+
/** Local parts that belong to a role rather than a person, so sharing one proves nothing. */
|
|
58
|
+
const GENERIC_LOCAL = new Set(["dev", "admin", "info", "me", "git", "hello", "mail", "noreply", "no-reply", "contact", "support", "team", "root", "user", "build", "ci", "bot", "test", "email", "work", "home"]);
|
|
59
|
+
const isBotAddress = (mail) => /\[bot\]@|^(?:dependabot|renovate|github-actions|greenkeeper)\b/i.test(mail);
|
|
60
|
+
/** The GitHub login inside `12345+login@users.noreply.github.com`, or the older form. */
|
|
61
|
+
const githubLogin = (mail) => {
|
|
62
|
+
const m = /^(?:\d+\+)?([^@+]+)@users\.noreply\.github\.com$/i.exec(mail);
|
|
63
|
+
return m ? m[1].toLowerCase() : null;
|
|
64
|
+
};
|
|
65
|
+
const localPart = (mail) => mail.split("@")[0].replace(/^\d+\+/, "");
|
|
66
|
+
const loose = (s) => foldIdentity(s).replace(/[\s._-]/g, "");
|
|
67
|
+
/**
|
|
68
|
+
* Addresses in this repository that look like the subject but were not counted as them.
|
|
69
|
+
*
|
|
70
|
+
* A person who commits from a laptop and a work machine is two authors to git, so a report
|
|
71
|
+
* describes half of their work and says nothing about the other half. That is a silent wrong
|
|
72
|
+
* number in a tool whose whole claim is that its numbers can be checked, so the report
|
|
73
|
+
* discloses the suspicion rather than quietly resolving it: only the author knows whether two
|
|
74
|
+
* addresses are one person, and `--author` takes both when they are.
|
|
75
|
+
*/
|
|
76
|
+
export function possibleSplits(commits, identity) {
|
|
77
|
+
const mine = new Set(identity.emails.map(foldIdentity));
|
|
78
|
+
const myNames = identity.names.map(foldIdentity);
|
|
79
|
+
const myLogins = identity.emails.map(githubLogin).filter((l) => l !== null);
|
|
80
|
+
const myLocals = identity.emails.map(localPart);
|
|
81
|
+
const seen = new Set();
|
|
82
|
+
const out = [];
|
|
83
|
+
for (const c of commits) {
|
|
84
|
+
const email = c.email;
|
|
85
|
+
if (mine.has(foldIdentity(email)) || seen.has(foldIdentity(email)) || isBotAddress(email))
|
|
86
|
+
continue;
|
|
87
|
+
let reason = "";
|
|
88
|
+
if (myNames.includes(foldIdentity(c.name))) {
|
|
89
|
+
reason = `the same name, "${c.name}", on another address`;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
const theirLogin = githubLogin(email);
|
|
93
|
+
const theirLocal = localPart(email);
|
|
94
|
+
if (theirLogin && (myLocals.some((l) => loose(l) === loose(theirLogin)) || myNames.some((n) => loose(n) === loose(theirLogin)))) {
|
|
95
|
+
reason = `the GitHub login "${theirLogin}" matches yours`;
|
|
96
|
+
}
|
|
97
|
+
else if (myLogins.some((l) => loose(l) === loose(theirLocal) || myNames.some((n) => loose(n) === loose(theirLocal)))) {
|
|
98
|
+
reason = `the address name "${theirLocal}" matches your GitHub login`;
|
|
99
|
+
}
|
|
100
|
+
else if (!theirLogin && myLocals.some((l) => l === theirLocal && l.length >= 3 && !GENERIC_LOCAL.has(l))) {
|
|
101
|
+
reason = `the same address name, "${theirLocal}", on another domain`;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (reason) {
|
|
105
|
+
seen.add(foldIdentity(email));
|
|
106
|
+
out.push({ name: c.name, email, reason });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
package/dist/src/report.d.ts
CHANGED
|
@@ -10,7 +10,6 @@ export interface Report {
|
|
|
10
10
|
/** sha256 of the RFC 8785 canonical JSON of { params, repositories }. */
|
|
11
11
|
hash: string;
|
|
12
12
|
}
|
|
13
|
-
/** The hash a report with these parameters and repositories must carry. */
|
|
14
13
|
export declare const hashOf: (params: unknown, repositories: unknown) => string;
|
|
15
14
|
export declare function buildReport(repositories: RepoReport[], params: Params, meta: {
|
|
16
15
|
version: string;
|
package/dist/src/report.js
CHANGED
|
@@ -3,7 +3,34 @@ import { canonicalize } from "./canonical.js";
|
|
|
3
3
|
import { publicEmail } from "./analyse.js";
|
|
4
4
|
import { plainSummary } from "./summary.js";
|
|
5
5
|
/** The hash a report with these parameters and repositories must carry. */
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* What of a repository the hash covers: everything a stranger with the same repository at
|
|
8
|
+
* the same commit can reproduce, and nothing else.
|
|
9
|
+
*
|
|
10
|
+
* Two fields are recorded in the report and deliberately left out. `name` is the local
|
|
11
|
+
* directory's basename, so the same repository cloned to a different folder hashed
|
|
12
|
+
* differently. `environment.git` is the local git version string, so two people with the
|
|
13
|
+
* same repository and different git hashed differently. Both were reproduced with real
|
|
14
|
+
* runs before 0.4.0. They stay in the document because they explain why two runs might
|
|
15
|
+
* legitimately differ; they are not something a verifier can be asked to reproduce.
|
|
16
|
+
*
|
|
17
|
+
* The blame flags, the ignore-revs file and the seed stay in, because those are the
|
|
18
|
+
* question the caller asked, not the machine they asked it on.
|
|
19
|
+
*/
|
|
20
|
+
function forHash(repositories) {
|
|
21
|
+
if (!Array.isArray(repositories))
|
|
22
|
+
return repositories;
|
|
23
|
+
return repositories.map((repo) => {
|
|
24
|
+
if (repo === null || typeof repo !== "object")
|
|
25
|
+
return repo;
|
|
26
|
+
const { name: _name, environment, ...rest } = repo;
|
|
27
|
+
if (environment === null || typeof environment !== "object")
|
|
28
|
+
return rest;
|
|
29
|
+
const { git: _git, ...restEnvironment } = environment;
|
|
30
|
+
return { ...rest, environment: restEnvironment };
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export const hashOf = (params, repositories) => createHash("sha256").update(canonicalize({ params, repositories: forHash(repositories) })).digest("hex");
|
|
7
34
|
/**
|
|
8
35
|
* What of the parameters the report may carry: never the fingerprint key, never a GitHub
|
|
9
36
|
* noreply login, and without --emails no author given as an email address.
|
|
@@ -72,7 +99,12 @@ export function renderMarkdown(report, narrative) {
|
|
|
72
99
|
``,
|
|
73
100
|
];
|
|
74
101
|
for (const repo of report.repositories) {
|
|
75
|
-
out.push(`## ${repo.name}`, ``, `HEAD \`${repo.head.slice(0, 12)}\` · fingerprint \`${repo.fingerprint.slice(0, 16)}\` · identities: ${repo.identity.names.join(", ")}${repo.identity.emails.length ? ` (${repo.identity.emails.join(", ")})` : ""}`, ``, repo.
|
|
102
|
+
out.push(`## ${repo.name}`, ``, `HEAD \`${repo.head.slice(0, 12)}\` · fingerprint \`${repo.fingerprint.slice(0, 16)}\` · identities: ${repo.identity.names.join(", ")}${repo.identity.emails.length ? ` (${repo.identity.emails.join(", ")})` : ""}`, ``, ...(repo.identity.possiblySplit
|
|
103
|
+
? [
|
|
104
|
+
`**${plural(repo.identity.possiblySplit.names.length, "other identity", "other identities")} in this repository look${repo.identity.possiblySplit.names.length === 1 ? "s" : ""} like the same person and ${repo.identity.possiblySplit.names.length === 1 ? "was" : "were"} not counted here**: ${repo.identity.possiblySplit.names.join(", ")}${repo.identity.possiblySplit.emails?.length ? ` (${repo.identity.possiblySplit.emails.join(", ")})` : ""}. ${repo.identity.possiblySplit.reasons.map((r, i) => (i === 0 ? r.charAt(0).toUpperCase() + r.slice(1) : r)).join("; ")}. Every figure below therefore describes only the work under the identities named above. If they are the same person, re-run with each address passed to \`--author\`, or merge them in a \`.mailmap\`.`,
|
|
105
|
+
``,
|
|
106
|
+
]
|
|
107
|
+
: []), repo.excluded.enabled
|
|
76
108
|
? `excluded ${plural(repo.excluded.botCommits, "bot commit", "bot commits")} and ${plural(repo.excluded.files, "generated, vendored or lock file", "generated, vendored or lock files")} (${pct(repo.excluded.linesAddedShare)} of lines added)`
|
|
77
109
|
: `exclusions off (--no-exclusions): bot commits and generated files are counted`, ``, `### In plain language`, ``, plainSummary(repo), ``, `That paragraph is assembled from the figures below by a fixed rule, with no model involved, so it says nothing the numbers do not.`, ``);
|
|
78
110
|
for (const f of repo.figures) {
|
package/dist/src/schema.js
CHANGED
|
@@ -31,6 +31,13 @@ function checkRepository(problems, path, r) {
|
|
|
31
31
|
expect(problems, `${path}.identity.emails`, Array.isArray(id.emails), "array");
|
|
32
32
|
expect(problems, `${path}.identity.names`, Array.isArray(id.names), "array");
|
|
33
33
|
expect(problems, `${path}.identity.count`, typeof id.count === "number", "number");
|
|
34
|
+
if (id.possiblySplit !== undefined && expect(problems, `${path}.identity.possiblySplit`, isObject(id.possiblySplit), "object")) {
|
|
35
|
+
const ps = id.possiblySplit;
|
|
36
|
+
expect(problems, `${path}.identity.possiblySplit.names`, Array.isArray(ps.names), "array");
|
|
37
|
+
expect(problems, `${path}.identity.possiblySplit.reasons`, Array.isArray(ps.reasons), "array");
|
|
38
|
+
if (ps.emails !== undefined)
|
|
39
|
+
expect(problems, `${path}.identity.possiblySplit.emails`, Array.isArray(ps.emails), "array");
|
|
40
|
+
}
|
|
34
41
|
}
|
|
35
42
|
if (expect(problems, `${path}.environment`, isObject(repo.environment), "object")) {
|
|
36
43
|
const env = repo.environment;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "workproof",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Turn a private git repository into a verifiable engineering report for one author, without showing any code: thirteen figures from git, a hash anyone can recompute offline, verify, and an in-toto attestation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/src/index.js",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"scripts": {
|
|
25
25
|
"build": "tsc -p tsconfig.json",
|
|
26
26
|
"lint": "tsc -p tsconfig.json --noEmit",
|
|
27
|
-
"test": "npm run build && node --test dist/test/figures.test.js dist/test/exclusions.test.js dist/test/adversarial.test.js dist/test/integrity.test.js dist/test/attest.test.js dist/test/report.test.js dist/test/summary.test.js dist/test/cli.test.js dist/test/action.test.js test/release.test.mjs",
|
|
27
|
+
"test": "npm run build && node --test dist/test/figures.test.js dist/test/exclusions.test.js dist/test/adversarial.test.js dist/test/integrity.test.js dist/test/attest.test.js dist/test/report.test.js dist/test/summary.test.js dist/test/cli.test.js dist/test/action.test.js test/release.test.mjs test/suite.test.mjs",
|
|
28
28
|
"prepublishOnly": "npm test",
|
|
29
29
|
"examples": "npm run build && node examples/basic.mjs",
|
|
30
30
|
"release": "node scripts/release.mjs",
|
|
@@ -4,52 +4,169 @@
|
|
|
4
4
|
"title": "workproof report",
|
|
5
5
|
"description": "A verifiable engineering report for one author. hash is sha256 over the RFC 8785 canonical JSON of { params, repositories }.",
|
|
6
6
|
"type": "object",
|
|
7
|
-
"required": [
|
|
7
|
+
"required": [
|
|
8
|
+
"tool",
|
|
9
|
+
"schemaVersion",
|
|
10
|
+
"version",
|
|
11
|
+
"generatedAt",
|
|
12
|
+
"params",
|
|
13
|
+
"repositories",
|
|
14
|
+
"hash"
|
|
15
|
+
],
|
|
8
16
|
"properties": {
|
|
9
|
-
"tool": {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
"tool": {
|
|
18
|
+
"const": "workproof"
|
|
19
|
+
},
|
|
20
|
+
"schemaVersion": {
|
|
21
|
+
"const": 2
|
|
22
|
+
},
|
|
23
|
+
"version": {
|
|
24
|
+
"type": "string"
|
|
25
|
+
},
|
|
26
|
+
"generatedAt": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"format": "date-time"
|
|
29
|
+
},
|
|
30
|
+
"params": {
|
|
31
|
+
"type": "object"
|
|
32
|
+
},
|
|
33
|
+
"hash": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"pattern": "^[0-9a-f]{64}$"
|
|
36
|
+
},
|
|
15
37
|
"repositories": {
|
|
16
38
|
"type": "array",
|
|
17
39
|
"minItems": 1,
|
|
18
40
|
"items": {
|
|
19
41
|
"type": "object",
|
|
20
|
-
"required": [
|
|
42
|
+
"required": [
|
|
43
|
+
"name",
|
|
44
|
+
"head",
|
|
45
|
+
"fingerprint",
|
|
46
|
+
"identity",
|
|
47
|
+
"environment",
|
|
48
|
+
"excluded",
|
|
49
|
+
"figures"
|
|
50
|
+
],
|
|
21
51
|
"properties": {
|
|
22
|
-
"name": {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"
|
|
52
|
+
"name": {
|
|
53
|
+
"type": "string"
|
|
54
|
+
},
|
|
55
|
+
"head": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"pattern": "^[0-9a-f]{40}$"
|
|
58
|
+
},
|
|
59
|
+
"fingerprint": {
|
|
60
|
+
"type": "string",
|
|
61
|
+
"pattern": "^[0-9a-f]{64}$"
|
|
62
|
+
},
|
|
63
|
+
"fingerprintKeyed": {
|
|
64
|
+
"type": "boolean"
|
|
65
|
+
},
|
|
26
66
|
"identity": {
|
|
27
67
|
"type": "object",
|
|
28
|
-
"required": [
|
|
68
|
+
"required": [
|
|
69
|
+
"emails",
|
|
70
|
+
"names",
|
|
71
|
+
"count"
|
|
72
|
+
],
|
|
29
73
|
"properties": {
|
|
30
|
-
"emails": {
|
|
31
|
-
|
|
32
|
-
|
|
74
|
+
"emails": {
|
|
75
|
+
"type": "array",
|
|
76
|
+
"items": {
|
|
77
|
+
"type": "string"
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"names": {
|
|
81
|
+
"type": "array",
|
|
82
|
+
"items": {
|
|
83
|
+
"type": "string"
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"count": {
|
|
87
|
+
"type": "integer",
|
|
88
|
+
"minimum": 0
|
|
89
|
+
},
|
|
90
|
+
"possiblySplit": {
|
|
91
|
+
"type": "object",
|
|
92
|
+
"properties": {
|
|
93
|
+
"names": {
|
|
94
|
+
"type": "array",
|
|
95
|
+
"items": {
|
|
96
|
+
"type": "string"
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"emails": {
|
|
100
|
+
"type": "array",
|
|
101
|
+
"items": {
|
|
102
|
+
"type": "string"
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
"reasons": {
|
|
106
|
+
"type": "array",
|
|
107
|
+
"items": {
|
|
108
|
+
"type": "string"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
"required": [
|
|
113
|
+
"names",
|
|
114
|
+
"reasons"
|
|
115
|
+
]
|
|
116
|
+
}
|
|
33
117
|
}
|
|
34
118
|
},
|
|
35
119
|
"environment": {
|
|
36
120
|
"type": "object",
|
|
37
|
-
"required": [
|
|
121
|
+
"required": [
|
|
122
|
+
"git",
|
|
123
|
+
"blame"
|
|
124
|
+
],
|
|
38
125
|
"properties": {
|
|
39
|
-
"git": {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"
|
|
126
|
+
"git": {
|
|
127
|
+
"type": "string"
|
|
128
|
+
},
|
|
129
|
+
"blame": {
|
|
130
|
+
"type": "array",
|
|
131
|
+
"items": {
|
|
132
|
+
"type": "string"
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
"ignoreRevs": {
|
|
136
|
+
"type": [
|
|
137
|
+
"string",
|
|
138
|
+
"null"
|
|
139
|
+
]
|
|
140
|
+
},
|
|
141
|
+
"seed": {
|
|
142
|
+
"type": "string"
|
|
143
|
+
}
|
|
43
144
|
}
|
|
44
145
|
},
|
|
45
146
|
"excluded": {
|
|
46
147
|
"type": "object",
|
|
47
|
-
"required": [
|
|
148
|
+
"required": [
|
|
149
|
+
"botCommits",
|
|
150
|
+
"files",
|
|
151
|
+
"linesAddedShare"
|
|
152
|
+
],
|
|
48
153
|
"properties": {
|
|
49
|
-
"botCommits": {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
154
|
+
"botCommits": {
|
|
155
|
+
"type": "integer",
|
|
156
|
+
"minimum": 0
|
|
157
|
+
},
|
|
158
|
+
"files": {
|
|
159
|
+
"type": "integer",
|
|
160
|
+
"minimum": 0
|
|
161
|
+
},
|
|
162
|
+
"linesAddedShare": {
|
|
163
|
+
"type": "number",
|
|
164
|
+
"minimum": 0,
|
|
165
|
+
"maximum": 1
|
|
166
|
+
},
|
|
167
|
+
"enabled": {
|
|
168
|
+
"type": "boolean"
|
|
169
|
+
}
|
|
53
170
|
}
|
|
54
171
|
},
|
|
55
172
|
"figures": {
|
|
@@ -57,13 +174,31 @@
|
|
|
57
174
|
"minItems": 1,
|
|
58
175
|
"items": {
|
|
59
176
|
"type": "object",
|
|
60
|
-
"required": [
|
|
177
|
+
"required": [
|
|
178
|
+
"id",
|
|
179
|
+
"title",
|
|
180
|
+
"value",
|
|
181
|
+
"command",
|
|
182
|
+
"limits"
|
|
183
|
+
],
|
|
61
184
|
"properties": {
|
|
62
|
-
"id": {
|
|
63
|
-
|
|
185
|
+
"id": {
|
|
186
|
+
"type": "string",
|
|
187
|
+
"minLength": 1
|
|
188
|
+
},
|
|
189
|
+
"title": {
|
|
190
|
+
"type": "string"
|
|
191
|
+
},
|
|
64
192
|
"value": {},
|
|
65
|
-
"command": {
|
|
66
|
-
|
|
193
|
+
"command": {
|
|
194
|
+
"type": "string"
|
|
195
|
+
},
|
|
196
|
+
"limits": {
|
|
197
|
+
"type": "array",
|
|
198
|
+
"items": {
|
|
199
|
+
"type": "string"
|
|
200
|
+
}
|
|
201
|
+
}
|
|
67
202
|
}
|
|
68
203
|
}
|
|
69
204
|
}
|