workproof 0.2.0 → 0.3.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 +1 -1
- package/dist/src/analyse.js +2 -2
- package/dist/src/figures/cadence.js +7 -3
- package/dist/src/figures/commits.js +5 -2
- package/dist/src/figures/identity.d.ts +15 -0
- package/dist/src/figures/identity.js +26 -3
- package/dist/src/git.d.ts +21 -0
- package/dist/src/git.js +24 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -172,7 +172,7 @@ snapshot files are removed before any of them is computed (see Gaming and bias).
|
|
|
172
172
|
|---|---|---|
|
|
173
173
|
| Tenure window | first to last commit by the author, or `--since/--until` | work before the first commit or after the last |
|
|
174
174
|
| Share of commits | non-merge commits by the author over all human non-merge commits in the window | what survived; a typo and a subsystem count the same |
|
|
175
|
-
| Cadence | active weeks, commits per active week, longest streak, release tags in tenure and the author's | a week with one commit and a week with forty both count as active |
|
|
175
|
+
| Cadence | active weeks, commits per active week, longest streak, release tags in tenure and the author's | a week with one commit and a week with forty both count as active; weeks are the author's own, read from the offset each commit records |
|
|
176
176
|
| Footprint | files touched, directories at or above a commit-share threshold, languages by lines added | anything the exclusion lists miss still counts |
|
|
177
177
|
| Test-file changes and documents created | share of test-file changes; `.md`, `.mdx`, `.rst` files whose oldest commit is the author's | test cases, coverage, or the quality of a document |
|
|
178
178
|
| Files authored | files alive at HEAD where the author's degree of authorship (Avelino et al.) is at least 3.293 and above 75% of the file's maximum | coefficients fitted on other systems; first authorship outweighs later rewrites |
|
package/dist/src/analyse.js
CHANGED
|
@@ -86,11 +86,11 @@ export async function analyseRepo(cwd, params, hooks = {}) {
|
|
|
86
86
|
const t = tenure(all, id, { ...(params.since ? { since: params.since } : {}), ...(params.until ? { until: params.until } : {}) });
|
|
87
87
|
const start = new Date(t.value.first + "T00:00:00Z");
|
|
88
88
|
const end = new Date(t.value.last + "T23:59:59Z");
|
|
89
|
-
const inTenure = all.filter((c) => c.
|
|
89
|
+
const inTenure = all.filter((c) => c.localDate >= start && c.localDate <= end);
|
|
90
90
|
let addedAll = 0;
|
|
91
91
|
let addedExcluded = 0;
|
|
92
92
|
for (const c of ex.raw) {
|
|
93
|
-
if (c.
|
|
93
|
+
if (c.localDate < start || c.localDate > end || c.parents > 1)
|
|
94
94
|
continue;
|
|
95
95
|
for (const f of c.files) {
|
|
96
96
|
if (f.added === null)
|
|
@@ -24,7 +24,7 @@ export function cadence(commits, tags, id, window) {
|
|
|
24
24
|
const mine = commits.filter((c) => isMine(c, id) && c.parents <= 1);
|
|
25
25
|
const perWeek = new Map();
|
|
26
26
|
for (const c of mine)
|
|
27
|
-
perWeek.set(isoWeek(c.
|
|
27
|
+
perWeek.set(isoWeek(c.localDate), (perWeek.get(isoWeek(c.localDate)) ?? 0) + 1);
|
|
28
28
|
const weeks = weeksBetween(window.first, window.last);
|
|
29
29
|
let streak = 0;
|
|
30
30
|
let longest = 0;
|
|
@@ -52,8 +52,12 @@ export function cadence(commits, tags, id, window) {
|
|
|
52
52
|
id: "cadence",
|
|
53
53
|
title: "Cadence",
|
|
54
54
|
value,
|
|
55
|
-
command: "git log --no-merges --format=%aI --author=<identity>; ISO weeks; git for-each-ref refs/tags with creatordate",
|
|
56
|
-
limits: [
|
|
55
|
+
command: "git log --no-merges --format=%aI --author=<identity>; ISO weeks in the author's own offset; git for-each-ref refs/tags with creatordate",
|
|
56
|
+
limits: [
|
|
57
|
+
"A week with one commit and a week with forty both count as active.",
|
|
58
|
+
"Tags are releases only if the project tags releases.",
|
|
59
|
+
"Weeks are ISO weeks read in the offset each commit records, so they are the author's weeks; a commit made Monday morning in Auckland counts as Monday, not as the previous week.",
|
|
60
|
+
],
|
|
57
61
|
};
|
|
58
62
|
return figure;
|
|
59
63
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isMine } from "./identity.js";
|
|
2
2
|
const day = (d) => d.toISOString().slice(0, 10);
|
|
3
3
|
export function tenure(commits, id, override) {
|
|
4
|
-
const mine = commits.filter((c) => isMine(c, id)).map((c) => c.
|
|
4
|
+
const mine = commits.filter((c) => isMine(c, id)).map((c) => c.localDate).sort((a, b) => a.getTime() - b.getTime());
|
|
5
5
|
const first = override.since ? new Date(override.since) : mine[0];
|
|
6
6
|
const last = override.until ? new Date(override.until) : mine[mine.length - 1];
|
|
7
7
|
const days = Math.round((last.getTime() - first.getTime()) / 86400000) + 1;
|
|
@@ -10,7 +10,10 @@ export function tenure(commits, id, override) {
|
|
|
10
10
|
title: "Tenure window",
|
|
11
11
|
value: { first: day(first), last: day(last), days },
|
|
12
12
|
command: override.since || override.until ? "--since/--until as given" : "git log --format=%aI --author=<identity>; first and last commit dates",
|
|
13
|
-
limits: [
|
|
13
|
+
limits: [
|
|
14
|
+
"Tenure is measured from commits, so work before the first commit or after the last is invisible.",
|
|
15
|
+
"The first and last days are read in the offset each commit records, so they are the author's calendar days rather than UTC days.",
|
|
16
|
+
],
|
|
14
17
|
};
|
|
15
18
|
}
|
|
16
19
|
export function commitShare(commits, id) {
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import type { Commit } from "../git.js";
|
|
2
2
|
import type { Identity } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Fold a name or address for matching. Two things defeat a plain `toLowerCase()`:
|
|
5
|
+
*
|
|
6
|
+
* Normalization. A repository authored on Linux can carry "José" decomposed while the same name
|
|
7
|
+
* typed on macOS arrives precomposed. They are the same name and compared unequal, and the error
|
|
8
|
+
* then listed an author that looked identical to what was typed, which nobody can debug.
|
|
9
|
+
*
|
|
10
|
+
* The Turkish dotted and dotless i. `"İ".toLowerCase()` is "i" followed by a combining dot above,
|
|
11
|
+
* and `"I".toLowerCase()` is "i" rather than "ı", so "İSMAİL YILMAZ" matched neither "İsmail
|
|
12
|
+
* Yılmaz" nor itself lowercased. A locale-aware fold cannot be applied globally, because it would
|
|
13
|
+
* turn English "I" into "ı", so the four i forms collapse to one instead. The cost is that two
|
|
14
|
+
* names differing only in dotted and dotless i match each other; the benefit is that a Turkish
|
|
15
|
+
* name matches itself in any case, and the error message prints the folded form that was tried.
|
|
16
|
+
*/
|
|
17
|
+
export declare function foldIdentity(s: string): string;
|
|
3
18
|
/**
|
|
4
19
|
* Resolve the author to report on. `author` entries match mailmapped emails or
|
|
5
20
|
* names, case-insensitively; with none given, the repository's configured
|
|
@@ -1,4 +1,27 @@
|
|
|
1
1
|
import { configuredEmail, configuredName } from "../git.js";
|
|
2
|
+
/**
|
|
3
|
+
* Fold a name or address for matching. Two things defeat a plain `toLowerCase()`:
|
|
4
|
+
*
|
|
5
|
+
* Normalization. A repository authored on Linux can carry "José" decomposed while the same name
|
|
6
|
+
* typed on macOS arrives precomposed. They are the same name and compared unequal, and the error
|
|
7
|
+
* then listed an author that looked identical to what was typed, which nobody can debug.
|
|
8
|
+
*
|
|
9
|
+
* The Turkish dotted and dotless i. `"İ".toLowerCase()` is "i" followed by a combining dot above,
|
|
10
|
+
* and `"I".toLowerCase()` is "i" rather than "ı", so "İSMAİL YILMAZ" matched neither "İsmail
|
|
11
|
+
* Yılmaz" nor itself lowercased. A locale-aware fold cannot be applied globally, because it would
|
|
12
|
+
* turn English "I" into "ı", so the four i forms collapse to one instead. The cost is that two
|
|
13
|
+
* names differing only in dotted and dotless i match each other; the benefit is that a Turkish
|
|
14
|
+
* name matches itself in any case, and the error message prints the folded form that was tried.
|
|
15
|
+
*/
|
|
16
|
+
export function foldIdentity(s) {
|
|
17
|
+
return s
|
|
18
|
+
.normalize("NFC")
|
|
19
|
+
.replace(/[\u0130\u0131Ii]/g, "i")
|
|
20
|
+
.replace(/\u0307/g, "")
|
|
21
|
+
.toLowerCase()
|
|
22
|
+
.normalize("NFC")
|
|
23
|
+
.trim();
|
|
24
|
+
}
|
|
2
25
|
/**
|
|
3
26
|
* Resolve the author to report on. `author` entries match mailmapped emails or
|
|
4
27
|
* names, case-insensitively; with none given, the repository's configured
|
|
@@ -7,19 +30,19 @@ import { configuredEmail, configuredName } from "../git.js";
|
|
|
7
30
|
export async function resolveIdentity(commits, author, cwd) {
|
|
8
31
|
const explicit = author && author.length ? author : [];
|
|
9
32
|
// Without --author, try the configured email, then the configured name.
|
|
10
|
-
const wanted = (explicit.length ? explicit : [await configuredEmail(cwd), await configuredName(cwd)]).
|
|
33
|
+
const wanted = (explicit.length ? explicit : [await configuredEmail(cwd), await configuredName(cwd)]).filter(Boolean).map(foldIdentity).filter(Boolean);
|
|
11
34
|
if (!wanted.length)
|
|
12
35
|
throw new Error(`no author given and git config has no user.email or user.name; pass --author. ${authorsHint(commits)}`);
|
|
13
36
|
const emails = new Set();
|
|
14
37
|
const names = new Set();
|
|
15
38
|
for (const c of commits) {
|
|
16
|
-
if (wanted.includes(c.email) || wanted.includes(c.name
|
|
39
|
+
if (wanted.includes(foldIdentity(c.email)) || wanted.includes(foldIdentity(c.name))) {
|
|
17
40
|
emails.add(c.email);
|
|
18
41
|
names.add(c.name);
|
|
19
42
|
}
|
|
20
43
|
}
|
|
21
44
|
if (!emails.size)
|
|
22
|
-
throw new Error(`no commits by ${wanted.join(" or ")} in this repository. ${authorsHint(commits)}`);
|
|
45
|
+
throw new Error(`no commits by ${wanted.join(" or ")} in this repository, comparing names and addresses folded to that form. ${authorsHint(commits)}`);
|
|
23
46
|
return { emails: [...emails].sort(), names: [...names].sort() };
|
|
24
47
|
}
|
|
25
48
|
/** The most frequent author names, so the error tells the user what to pass. */
|
package/dist/src/git.d.ts
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
* Settings that change what diff and blame attribute, pinned so two machines with different
|
|
3
3
|
* defaults produce the same figures. indentHeuristic became the default in git 2.14 and
|
|
4
4
|
* rename detection in 2.9; a report records the git version it ran under as well.
|
|
5
|
+
*
|
|
6
|
+
* `core.precomposeunicode` is true in every repository git creates on macOS, and it rewrites
|
|
7
|
+
* command-line arguments from decomposed to precomposed form. Paths here are read out of the
|
|
8
|
+
* tree and handed back as pathspecs, so a stored decomposed path came back as a different
|
|
9
|
+
* string and matched nothing: "fatal: no such path café.ts in HEAD", and the run ended with no
|
|
10
|
+
* report. Pinning it off keeps the path sent identical to the path git stored. A checkout
|
|
11
|
+
* authored on Linux carrying Korean, French, Turkish, Vietnamese, Portuguese or Spanish
|
|
12
|
+
* filenames was affected; ASCII repositories are unchanged either way.
|
|
5
13
|
*/
|
|
6
14
|
export declare const PINNED_CONFIG: string[];
|
|
7
15
|
export declare function git(args: string[], cwd: string, input?: string): Promise<string>;
|
|
@@ -18,6 +26,12 @@ export interface Commit {
|
|
|
18
26
|
email: string;
|
|
19
27
|
name: string;
|
|
20
28
|
date: Date;
|
|
29
|
+
/**
|
|
30
|
+
* The author's wall clock, as a Date whose UTC fields are the local calendar date and time
|
|
31
|
+
* that `%aI` reports. Calendar figures (which week, which day) must use this; anything
|
|
32
|
+
* ordering commits in time must use `date`, which is the instant.
|
|
33
|
+
*/
|
|
34
|
+
localDate: Date;
|
|
21
35
|
parents: number;
|
|
22
36
|
files: FileChange[];
|
|
23
37
|
/** Lower-cased emails from Co-authored-by trailers. */
|
|
@@ -33,6 +47,13 @@ export declare function listCommits(cwd: string, opts: {
|
|
|
33
47
|
until?: string;
|
|
34
48
|
max?: number;
|
|
35
49
|
}): Promise<Commit[]>;
|
|
50
|
+
/**
|
|
51
|
+
* `%aI` carries the author's offset, for example 2026-03-02T00:30:00+13:00. `new Date` keeps the
|
|
52
|
+
* instant and drops the offset, so a commit made on Monday morning in Auckland lands in the
|
|
53
|
+
* previous UTC week. This shifts the instant by the stated offset so the UTC getters read back
|
|
54
|
+
* the author's own calendar.
|
|
55
|
+
*/
|
|
56
|
+
export declare function wallClock(iso: string): Date;
|
|
36
57
|
export interface Tag {
|
|
37
58
|
name: string;
|
|
38
59
|
date: Date;
|
package/dist/src/git.js
CHANGED
|
@@ -3,8 +3,16 @@ import { execFile } from "node:child_process";
|
|
|
3
3
|
* Settings that change what diff and blame attribute, pinned so two machines with different
|
|
4
4
|
* defaults produce the same figures. indentHeuristic became the default in git 2.14 and
|
|
5
5
|
* rename detection in 2.9; a report records the git version it ran under as well.
|
|
6
|
+
*
|
|
7
|
+
* `core.precomposeunicode` is true in every repository git creates on macOS, and it rewrites
|
|
8
|
+
* command-line arguments from decomposed to precomposed form. Paths here are read out of the
|
|
9
|
+
* tree and handed back as pathspecs, so a stored decomposed path came back as a different
|
|
10
|
+
* string and matched nothing: "fatal: no such path café.ts in HEAD", and the run ended with no
|
|
11
|
+
* report. Pinning it off keeps the path sent identical to the path git stored. A checkout
|
|
12
|
+
* authored on Linux carrying Korean, French, Turkish, Vietnamese, Portuguese or Spanish
|
|
13
|
+
* filenames was affected; ASCII repositories are unchanged either way.
|
|
6
14
|
*/
|
|
7
|
-
export const PINNED_CONFIG = ["-c", "diff.renames=true", "-c", "diff.algorithm=myers", "-c", "diff.indentHeuristic=true", "-c", "core.autocrlf=false"];
|
|
15
|
+
export const PINNED_CONFIG = ["-c", "diff.renames=true", "-c", "diff.algorithm=myers", "-c", "diff.indentHeuristic=true", "-c", "core.autocrlf=false", "-c", "core.precomposeunicode=false"];
|
|
8
16
|
export function git(args, cwd, input) {
|
|
9
17
|
return new Promise((resolve, reject) => {
|
|
10
18
|
const child = execFile("git", [...PINNED_CONFIG, ...args], { cwd, encoding: "utf8", maxBuffer: 1024 * 1024 * 512 }, (err, stdout) => {
|
|
@@ -70,6 +78,7 @@ export async function listCommits(cwd, opts) {
|
|
|
70
78
|
email: email.toLowerCase(),
|
|
71
79
|
name: name,
|
|
72
80
|
date: new Date(iso),
|
|
81
|
+
localDate: wallClock(iso),
|
|
73
82
|
parents: parents ? parents.trim().split(" ").filter(Boolean).length : 0,
|
|
74
83
|
files,
|
|
75
84
|
coAuthors: coAuthorValues.map((v) => (v.match(/<([^>]+)>/)?.[1] ?? "").toLowerCase()).filter(Boolean),
|
|
@@ -79,6 +88,20 @@ export async function listCommits(cwd, opts) {
|
|
|
79
88
|
}
|
|
80
89
|
return commits;
|
|
81
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* `%aI` carries the author's offset, for example 2026-03-02T00:30:00+13:00. `new Date` keeps the
|
|
93
|
+
* instant and drops the offset, so a commit made on Monday morning in Auckland lands in the
|
|
94
|
+
* previous UTC week. This shifts the instant by the stated offset so the UTC getters read back
|
|
95
|
+
* the author's own calendar.
|
|
96
|
+
*/
|
|
97
|
+
export function wallClock(iso) {
|
|
98
|
+
const m = iso.match(/([+-])(\d{2}):?(\d{2})$/);
|
|
99
|
+
const instant = new Date(iso);
|
|
100
|
+
if (!m || /[Zz]$/.test(iso))
|
|
101
|
+
return instant;
|
|
102
|
+
const minutes = (m[1] === "-" ? -1 : 1) * (Number(m[2]) * 60 + Number(m[3]));
|
|
103
|
+
return new Date(instant.getTime() + minutes * 60000);
|
|
104
|
+
}
|
|
82
105
|
export async function listTags(cwd) {
|
|
83
106
|
const out = await git(["for-each-ref", "--format=%(refname:short)%09%(creatordate:iso-strict)%09%(taggeremail)%09%(*authoremail)%09%(authoremail)", "refs/tags"], cwd);
|
|
84
107
|
const tags = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "workproof",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|