@trofimyukdev/repo-truth 0.0.0-stage → 0.1.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.
Files changed (120) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +273 -2
  3. package/dist/action.d.ts +52 -0
  4. package/dist/action.js +182 -0
  5. package/dist/action.js.map +1 -0
  6. package/dist/checks/action-pins.d.ts +52 -0
  7. package/dist/checks/action-pins.js +196 -0
  8. package/dist/checks/action-pins.js.map +1 -0
  9. package/dist/checks/autosquash.d.ts +23 -0
  10. package/dist/checks/autosquash.js +85 -0
  11. package/dist/checks/autosquash.js.map +1 -0
  12. package/dist/checks/bidi-controls.d.ts +58 -0
  13. package/dist/checks/bidi-controls.js +172 -0
  14. package/dist/checks/bidi-controls.js.map +1 -0
  15. package/dist/checks/binary-files.d.ts +53 -0
  16. package/dist/checks/binary-files.js +157 -0
  17. package/dist/checks/binary-files.js.map +1 -0
  18. package/dist/checks/config-changes.d.ts +57 -0
  19. package/dist/checks/config-changes.js +175 -0
  20. package/dist/checks/config-changes.js.map +1 -0
  21. package/dist/checks/credentials.d.ts +35 -0
  22. package/dist/checks/credentials.js +138 -0
  23. package/dist/checks/credentials.js.map +1 -0
  24. package/dist/checks/install-scripts.d.ts +39 -0
  25. package/dist/checks/install-scripts.js +173 -0
  26. package/dist/checks/install-scripts.js.map +1 -0
  27. package/dist/checks/lint-rules.d.ts +49 -0
  28. package/dist/checks/lint-rules.js +184 -0
  29. package/dist/checks/lint-rules.js.map +1 -0
  30. package/dist/checks/path-policy.d.ts +40 -0
  31. package/dist/checks/path-policy.js +195 -0
  32. package/dist/checks/path-policy.js.map +1 -0
  33. package/dist/checks/raised-timeouts.d.ts +66 -0
  34. package/dist/checks/raised-timeouts.js +248 -0
  35. package/dist/checks/raised-timeouts.js.map +1 -0
  36. package/dist/checks/script-changes.d.ts +51 -0
  37. package/dist/checks/script-changes.js +199 -0
  38. package/dist/checks/script-changes.js.map +1 -0
  39. package/dist/checks/secret-files.d.ts +58 -0
  40. package/dist/checks/secret-files.js +165 -0
  41. package/dist/checks/secret-files.js.map +1 -0
  42. package/dist/checks/strictness.d.ts +59 -0
  43. package/dist/checks/strictness.js +260 -0
  44. package/dist/checks/strictness.js.map +1 -0
  45. package/dist/checks/submodules.d.ts +51 -0
  46. package/dist/checks/submodules.js +172 -0
  47. package/dist/checks/submodules.js.map +1 -0
  48. package/dist/checks/suppressions.d.ts +53 -0
  49. package/dist/checks/suppressions.js +203 -0
  50. package/dist/checks/suppressions.js.map +1 -0
  51. package/dist/checks/workflow-permissions.d.ts +56 -0
  52. package/dist/checks/workflow-permissions.js +237 -0
  53. package/dist/checks/workflow-permissions.js.map +1 -0
  54. package/dist/checks/workflow-weakening.d.ts +38 -0
  55. package/dist/checks/workflow-weakening.js +172 -0
  56. package/dist/checks/workflow-weakening.js.map +1 -0
  57. package/dist/cli.d.ts +31 -0
  58. package/dist/cli.js +300 -0
  59. package/dist/cli.js.map +1 -0
  60. package/dist/commit-range.d.ts +28 -0
  61. package/dist/commit-range.js +152 -0
  62. package/dist/commit-range.js.map +1 -0
  63. package/dist/config.d.ts +35 -0
  64. package/dist/config.js +235 -0
  65. package/dist/config.js.map +1 -0
  66. package/dist/conflict-markers.d.ts +40 -0
  67. package/dist/conflict-markers.js +234 -0
  68. package/dist/conflict-markers.js.map +1 -0
  69. package/dist/dependency-sources.d.ts +32 -0
  70. package/dist/dependency-sources.js +260 -0
  71. package/dist/dependency-sources.js.map +1 -0
  72. package/dist/figure-claims.d.ts +35 -0
  73. package/dist/figure-claims.js +139 -0
  74. package/dist/figure-claims.js.map +1 -0
  75. package/dist/figure-text.d.ts +22 -0
  76. package/dist/figure-text.js +119 -0
  77. package/dist/figure-text.js.map +1 -0
  78. package/dist/index.d.ts +46 -0
  79. package/dist/index.js +38 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/large-blobs.d.ts +35 -0
  82. package/dist/large-blobs.js +163 -0
  83. package/dist/large-blobs.js.map +1 -0
  84. package/dist/lockfile-drift.d.ts +36 -0
  85. package/dist/lockfile-drift.js +206 -0
  86. package/dist/lockfile-drift.js.map +1 -0
  87. package/dist/lowered-thresholds.d.ts +70 -0
  88. package/dist/lowered-thresholds.js +281 -0
  89. package/dist/lowered-thresholds.js.map +1 -0
  90. package/dist/measurement-rule.d.ts +39 -0
  91. package/dist/measurement-rule.js +213 -0
  92. package/dist/measurement-rule.js.map +1 -0
  93. package/dist/portable-paths.d.ts +37 -0
  94. package/dist/portable-paths.js +215 -0
  95. package/dist/portable-paths.js.map +1 -0
  96. package/dist/registry.d.ts +43 -0
  97. package/dist/registry.js +144 -0
  98. package/dist/registry.js.map +1 -0
  99. package/dist/replay-plan.d.ts +36 -0
  100. package/dist/replay-plan.js +196 -0
  101. package/dist/replay-plan.js.map +1 -0
  102. package/dist/replayed-figures.d.ts +36 -0
  103. package/dist/replayed-figures.js +159 -0
  104. package/dist/replayed-figures.js.map +1 -0
  105. package/dist/snapshot-refreshes.d.ts +36 -0
  106. package/dist/snapshot-refreshes.js +202 -0
  107. package/dist/snapshot-refreshes.js.map +1 -0
  108. package/dist/stray-files.d.ts +52 -0
  109. package/dist/stray-files.js +234 -0
  110. package/dist/stray-files.js.map +1 -0
  111. package/dist/symlinks.d.ts +28 -0
  112. package/dist/symlinks.js +193 -0
  113. package/dist/symlinks.js.map +1 -0
  114. package/dist/trailers.d.ts +36 -0
  115. package/dist/trailers.js +110 -0
  116. package/dist/trailers.js.map +1 -0
  117. package/dist/weakened-tests.d.ts +94 -0
  118. package/dist/weakened-tests.js +458 -0
  119. package/dist/weakened-tests.js.map +1 -0
  120. package/package.json +31 -4
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Lockfile drift.
3
+ *
4
+ * The manifest (the root `package.json`) and the lockfile are two statements of
5
+ * one install. A range that changes a dependency field of the manifest without
6
+ * the lockfile leaves a pin that no longer matches what was asked for (a stale
7
+ * pin); a range that changes the lockfile without a dependency field of the
8
+ * manifest moves a version with no request behind it (an unrequested change).
9
+ * The two are reported in different words because the fix and the alarm differ.
10
+ *
11
+ * Lockfiles are never parsed: they are compared by git object id at the two
12
+ * revisions. Only the manifest is parsed, and only to compare its dependency
13
+ * fields, so a script or description edit is not drift. A repository with no
14
+ * manifest or no recognised lockfile gets a `skip` naming what was looked for.
15
+ */
16
+ import { execFile } from "node:child_process";
17
+ const NAME = "RT-06";
18
+ const MANIFEST = "package.json";
19
+ /** The manifest fields that say what to install. */
20
+ export const DEPENDENCY_FIELDS = [
21
+ "dependencies",
22
+ "devDependencies",
23
+ "peerDependencies",
24
+ "optionalDependencies",
25
+ "bundledDependencies",
26
+ "bundleDependencies",
27
+ ];
28
+ /** The lockfiles recognised at the repository root. */
29
+ export const LOCKFILES = [
30
+ "package-lock.json",
31
+ "npm-shrinkwrap.json",
32
+ "yarn.lock",
33
+ "pnpm-lock.yaml",
34
+ "bun.lock",
35
+ ];
36
+ function runGit(cwd, args) {
37
+ return new Promise((resolve, reject) => {
38
+ execFile("git", args, { cwd, maxBuffer: 1024 * 1024 * 1024, encoding: "buffer" }, (error, stdout, stderr) => {
39
+ if (error) {
40
+ const detail = stderr.toString("utf8").trim();
41
+ reject(new Error(detail.length > 0 ? detail : error.message));
42
+ return;
43
+ }
44
+ resolve(stdout);
45
+ });
46
+ });
47
+ }
48
+ /** Escape everything outside printable ASCII so a name cannot replay a control sequence. */
49
+ function escapeText(text) {
50
+ let escaped = "";
51
+ for (const char of text) {
52
+ const codePoint = char.codePointAt(0) ?? 0;
53
+ if (codePoint >= 0x20 && codePoint <= 0x7e) {
54
+ escaped += char;
55
+ }
56
+ else {
57
+ escaped += `\\u{${codePoint.toString(16).padStart(4, "0")}}`;
58
+ }
59
+ }
60
+ return escaped;
61
+ }
62
+ function quote(text) {
63
+ return `"${escapeText(text)}"`;
64
+ }
65
+ async function resolveCommit(cwd, revision) {
66
+ if (revision.length === 0 || revision.startsWith("-")) {
67
+ throw new Error(`revision ${quote(revision)} names no commit`);
68
+ }
69
+ try {
70
+ const out = await runGit(cwd, ["rev-parse", "--verify", "--quiet", `${revision}^{commit}`]);
71
+ const hash = out.toString("utf8").trim();
72
+ if (hash.length === 0) {
73
+ throw new Error("empty answer");
74
+ }
75
+ return hash;
76
+ }
77
+ catch (error) {
78
+ throw new Error(`revision ${quote(revision)} names no commit: ${error.message}`);
79
+ }
80
+ }
81
+ /** The blob id of a root file at a commit, or undefined when the commit has no such file. */
82
+ async function blobId(root, commit, path) {
83
+ const out = (await runGit(root, ["ls-tree", "-z", commit, "--", path])).toString("utf8");
84
+ const entry = out.split("\0")[0] ?? "";
85
+ const match = /^(\d+) (\w+) ([0-9a-f]+)\t(.*)$/s.exec(entry);
86
+ if (match === null || match[4] !== path || match[2] !== "blob") {
87
+ return undefined;
88
+ }
89
+ return match[3];
90
+ }
91
+ function canonical(value) {
92
+ if (Array.isArray(value)) {
93
+ return value.map(canonical).sort((x, y) => JSON.stringify(x).localeCompare(JSON.stringify(y)));
94
+ }
95
+ if (value !== null && typeof value === "object") {
96
+ const out = {};
97
+ for (const key of Object.keys(value).sort()) {
98
+ out[key] = canonical(value[key]);
99
+ }
100
+ return out;
101
+ }
102
+ return value;
103
+ }
104
+ /** The dependency fields of a manifest blob, in a form that compares by value. */
105
+ async function dependencyView(root, id, label) {
106
+ const text = (await runGit(root, ["cat-file", "blob", id])).toString("utf8");
107
+ let parsed;
108
+ try {
109
+ parsed = JSON.parse(text);
110
+ }
111
+ catch (error) {
112
+ throw new Error(`${MANIFEST} at ${label} is not valid JSON: ${error.message}`);
113
+ }
114
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
115
+ throw new Error(`${MANIFEST} at ${label} is not a JSON object`);
116
+ }
117
+ const fields = {};
118
+ for (const field of DEPENDENCY_FIELDS) {
119
+ if (Object.prototype.hasOwnProperty.call(parsed, field)) {
120
+ fields[field] = canonical(parsed[field]);
121
+ }
122
+ }
123
+ return JSON.stringify(fields);
124
+ }
125
+ /**
126
+ * Compare the manifest's dependency fields and the root lockfiles between the
127
+ * merge base of `base` and `candidate` and `candidate`. Fails closed: a
128
+ * revision that names no commit, or any git invocation that fails, is a `fail`
129
+ * record naming the cause - never a `pass`, and never a thrown error.
130
+ */
131
+ export async function checkLockfileDrift(options) {
132
+ try {
133
+ const { cwd, base, candidate } = options;
134
+ const baseCommit = await resolveCommit(cwd, base);
135
+ const candidateCommit = await resolveCommit(cwd, candidate);
136
+ const root = (await runGit(cwd, ["rev-parse", "--show-toplevel"])).toString("utf8").replace(/\r?\n$/, "");
137
+ let ancestor;
138
+ try {
139
+ ancestor = (await runGit(root, ["merge-base", baseCommit, candidateCommit])).toString("utf8").trim();
140
+ }
141
+ catch (error) {
142
+ throw new Error(`no merge base of ${quote(base)} and ${quote(candidate)}: ${error.message}`);
143
+ }
144
+ if (ancestor.length === 0) {
145
+ throw new Error(`no merge base of ${quote(base)} and ${quote(candidate)}`);
146
+ }
147
+ const range = `${quote(base)}..${quote(candidate)}`;
148
+ const looked = `looked for ${MANIFEST} and one of ${LOCKFILES.join(", ")} at the repository root`;
149
+ const manifestBefore = await blobId(root, ancestor, MANIFEST);
150
+ const manifestAfter = await blobId(root, candidateCommit, MANIFEST);
151
+ if (manifestBefore === undefined && manifestAfter === undefined) {
152
+ return { name: NAME, status: "skip", evidence: [`no ${MANIFEST} found in ${range}; ${looked}`] };
153
+ }
154
+ const lockfiles = [];
155
+ for (const name of LOCKFILES) {
156
+ const before = await blobId(root, ancestor, name);
157
+ const after = await blobId(root, candidateCommit, name);
158
+ if (before !== undefined || after !== undefined) {
159
+ lockfiles.push({ name, changed: before !== after });
160
+ }
161
+ }
162
+ if (lockfiles.length === 0) {
163
+ return { name: NAME, status: "skip", evidence: [`no recognised lockfile found in ${range}; ${looked}`] };
164
+ }
165
+ let dependenciesChanged = false;
166
+ if (manifestBefore !== manifestAfter) {
167
+ const viewBefore = manifestBefore === undefined ? "" : await dependencyView(root, manifestBefore, quote(base));
168
+ const viewAfter = manifestAfter === undefined ? "" : await dependencyView(root, manifestAfter, quote(candidate));
169
+ dependenciesChanged = viewBefore !== viewAfter;
170
+ }
171
+ const changedLocks = lockfiles.filter((lock) => lock.changed).map((lock) => lock.name);
172
+ const lockNames = lockfiles.map((lock) => lock.name).join(", ");
173
+ if (dependenciesChanged && changedLocks.length === 0) {
174
+ return {
175
+ name: NAME,
176
+ status: "fail",
177
+ evidence: [
178
+ `stale pin: a dependency field of ${MANIFEST} changed in ${range} but ${lockNames} did not, so the lockfile no longer records what the manifest asks for`,
179
+ ],
180
+ };
181
+ }
182
+ if (!dependenciesChanged && changedLocks.length > 0) {
183
+ return {
184
+ name: NAME,
185
+ status: "fail",
186
+ evidence: [
187
+ `unrequested change: ${changedLocks.join(", ")} changed in ${range} but no dependency field of ${MANIFEST} did, so a resolved version moved with no request behind it`,
188
+ ],
189
+ };
190
+ }
191
+ return {
192
+ name: NAME,
193
+ status: "pass",
194
+ evidence: [
195
+ dependenciesChanged
196
+ ? `${MANIFEST} dependency fields and ${changedLocks.join(", ")} changed together`
197
+ : `neither the dependency fields of ${MANIFEST} nor ${lockNames} changed`,
198
+ ],
199
+ };
200
+ }
201
+ catch (error) {
202
+ const message = error instanceof Error ? error.message : String(error);
203
+ return { name: NAME, status: "fail", evidence: [escapeText(message)] };
204
+ }
205
+ }
206
+ //# sourceMappingURL=lockfile-drift.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lockfile-drift.js","sourceRoot":"","sources":["../src/lockfile-drift.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAI9C,MAAM,IAAI,GAAG,OAAO,CAAC;AAYrB,MAAM,QAAQ,GAAG,cAAc,CAAC;AAEhC,oDAAoD;AACpD,MAAM,CAAC,MAAM,iBAAiB,GAAa;IACzC,cAAc;IACd,iBAAiB;IACjB,kBAAkB;IAClB,sBAAsB;IACtB,qBAAqB;IACrB,oBAAoB;CACrB,CAAC;AAEF,uDAAuD;AACvD,MAAM,CAAC,MAAM,SAAS,GAAa;IACjC,mBAAmB;IACnB,qBAAqB;IACrB,WAAW;IACX,gBAAgB;IAChB,UAAU;CACX,CAAC;AAEF,SAAS,MAAM,CAAC,GAAW,EAAE,IAAuB;IAClD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,QAAQ,CACN,KAAK,EACL,IAAgB,EAChB,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAC1D,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE;YACxB,IAAI,KAAK,EAAE,CAAC;gBACV,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;gBAC9C,MAAM,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;gBAC9D,OAAO;YACT,CAAC;YACD,OAAO,CAAC,MAAM,CAAC,CAAC;QAClB,CAAC,CACF,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED,4FAA4F;AAC5F,SAAS,UAAU,CAAC,IAAY;IAC9B,IAAI,OAAO,GAAG,EAAE,CAAC;IACjB,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;QACxB,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAC3C,IAAI,SAAS,IAAI,IAAI,IAAI,SAAS,IAAI,IAAI,EAAE,CAAC;YAC3C,OAAO,IAAI,IAAI,CAAC;QAClB,CAAC;aAAM,CAAC;YACN,OAAO,IAAI,OAAO,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC;QAC/D,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,KAAK,CAAC,IAAY;IACzB,OAAO,IAAI,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC;AACjC,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,GAAW,EAAE,QAAgB;IACxD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,IAAI,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACtD,MAAM,IAAI,KAAK,CAAC,YAAY,KAAK,CAAC,QAAQ,CAAC,kBAAkB,CAAC,CAAC;IACjE,CAAC;IACD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,GAAG,QAAQ,WAAW,CAAC,CAAC,CAAC;QAC5F,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;QACzC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtB,MAAM,IAAI,KAAK,CAAC,cAAc,CAAC,CAAC;QAClC,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CAAC,YAAY,KAAK,CAAC,QAAQ,CAAC,qBAAsB,KAAe,CAAC,OAAO,EAAE,CAAC,CAAC;IAC9F,CAAC;AACH,CAAC;AAED,6FAA6F;AAC7F,KAAK,UAAU,MAAM,CAAC,IAAY,EAAE,MAAc,EAAE,IAAY;IAC9D,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACzF,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACvC,MAAM,KAAK,GAAG,kCAAkC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC7D,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,MAAM,EAAE,CAAC;QAC/D,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,SAAS,SAAS,CAAC,KAAc;IAC/B,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACjG,CAAC;IACD,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAChD,MAAM,GAAG,GAA4B,EAAE,CAAC;QACxC,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;YAC5C,GAAG,CAAC,GAAG,CAAC,GAAG,SAAS,CAAE,KAAiC,CAAC,GAAG,CAAC,CAAC,CAAC;QAChE,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,kFAAkF;AAClF,KAAK,UAAU,cAAc,CAAC,IAAY,EAAE,EAAU,EAAE,KAAa;IACnE,MAAM,IAAI,GAAG,CAAC,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC7E,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CAAC,GAAG,QAAQ,OAAO,KAAK,uBAAwB,KAAe,CAAC,OAAO,EAAE,CAAC,CAAC;IAC5F,CAAC;IACD,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3E,MAAM,IAAI,KAAK,CAAC,GAAG,QAAQ,OAAO,KAAK,uBAAuB,CAAC,CAAC;IAClE,CAAC;IACD,MAAM,MAAM,GAA4B,EAAE,CAAC;IAC3C,KAAK,MAAM,KAAK,IAAI,iBAAiB,EAAE,CAAC;QACtC,IAAI,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC;YACxD,MAAM,CAAC,KAAK,CAAC,GAAG,SAAS,CAAE,MAAkC,CAAC,KAAK,CAAC,CAAC,CAAC;QACxE,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;AAChC,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,OAA6B;IACpE,IAAI,CAAC;QACH,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC;QACzC,MAAM,UAAU,GAAG,MAAM,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAClD,MAAM,eAAe,GAAG,MAAM,aAAa,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QAC5D,MAAM,IAAI,GAAG,CAAC,MAAM,MAAM,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,iBAAiB,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QAC1G,IAAI,QAAgB,CAAC;QACrB,IAAI,CAAC;YACH,QAAQ,GAAG,CAAC,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC,YAAY,EAAE,UAAU,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;QACvG,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CAAC,oBAAoB,KAAK,CAAC,IAAI,CAAC,QAAQ,KAAK,CAAC,SAAS,CAAC,KAAM,KAAe,CAAC,OAAO,EAAE,CAAC,CAAC;QAC1G,CAAC;QACD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC1B,MAAM,IAAI,KAAK,CAAC,oBAAoB,KAAK,CAAC,IAAI,CAAC,QAAQ,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;QAC7E,CAAC;QAED,MAAM,KAAK,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC;QACpD,MAAM,MAAM,GAAG,cAAc,QAAQ,eAAe,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,yBAAyB,CAAC;QAClG,MAAM,cAAc,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAC9D,MAAM,aAAa,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,eAAe,EAAE,QAAQ,CAAC,CAAC;QACpE,IAAI,cAAc,KAAK,SAAS,IAAI,aAAa,KAAK,SAAS,EAAE,CAAC;YAChE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,MAAM,QAAQ,aAAa,KAAK,KAAK,MAAM,EAAE,CAAC,EAAE,CAAC;QACnG,CAAC;QAED,MAAM,SAAS,GAAyC,EAAE,CAAC;QAC3D,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;YAC7B,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC;YAClD,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,eAAe,EAAE,IAAI,CAAC,CAAC;YACxD,IAAI,MAAM,KAAK,SAAS,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBAChD,SAAS,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,KAAK,KAAK,EAAE,CAAC,CAAC;YACtD,CAAC;QACH,CAAC;QACD,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC3B,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,mCAAmC,KAAK,KAAK,MAAM,EAAE,CAAC,EAAE,CAAC;QAC3G,CAAC;QAED,IAAI,mBAAmB,GAAG,KAAK,CAAC;QAChC,IAAI,cAAc,KAAK,aAAa,EAAE,CAAC;YACrC,MAAM,UAAU,GAAG,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,cAAc,CAAC,IAAI,EAAE,cAAc,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;YAC/G,MAAM,SAAS,GAAG,aAAa,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,cAAc,CAAC,IAAI,EAAE,aAAa,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;YACjH,mBAAmB,GAAG,UAAU,KAAK,SAAS,CAAC;QACjD,CAAC;QACD,MAAM,YAAY,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACvF,MAAM,SAAS,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAEhE,IAAI,mBAAmB,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACrD,OAAO;gBACL,IAAI,EAAE,IAAI;gBACV,MAAM,EAAE,MAAM;gBACd,QAAQ,EAAE;oBACR,oCAAoC,QAAQ,eAAe,KAAK,QAAQ,SAAS,wEAAwE;iBAC1J;aACF,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,mBAAmB,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACpD,OAAO;gBACL,IAAI,EAAE,IAAI;gBACV,MAAM,EAAE,MAAM;gBACd,QAAQ,EAAE;oBACR,uBAAuB,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,KAAK,+BAA+B,QAAQ,6DAA6D;iBACvK;aACF,CAAC;QACJ,CAAC;QACD,OAAO;YACL,IAAI,EAAE,IAAI;YACV,MAAM,EAAE,MAAM;YACd,QAAQ,EAAE;gBACR,mBAAmB;oBACjB,CAAC,CAAC,GAAG,QAAQ,0BAA0B,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,mBAAmB;oBACjF,CAAC,CAAC,oCAAoC,QAAQ,QAAQ,SAAS,UAAU;aAC5E;SACF,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACvE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC;IACzE,CAAC;AACH,CAAC"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Lowered thresholds.
3
+ *
4
+ * The cheapest way to make a coverage gate pass is to ask less of it: the
5
+ * minimum in the runner's configuration goes down, or the key goes away. This
6
+ * check reads the lines `base..candidate` changes in the files where a
7
+ * repository states its coverage minimums, pairs each value the range removes
8
+ * with the value it adds for the same setting, and reports a minimum that went
9
+ * down (`threshold-lowered`) or disappeared (`threshold-removed`) for a human
10
+ * to sanction. A raised, new or merely moved value says nothing.
11
+ *
12
+ * Only the DIFF is read, by blob id through git, from the merge base of the
13
+ * two revisions; a low minimum the base already holds is never reported.
14
+ *
15
+ * Which files and which spellings count lives in one exported array,
16
+ * `THRESHOLD_SPELLINGS`, read each time the check runs: a repository on another
17
+ * runner appends its own entry.
18
+ */
19
+ import type { CheckRecord } from "./index.js";
20
+ export type ThresholdCategory = "threshold-lowered" | "threshold-removed";
21
+ /** One spelling of a stated minimum. */
22
+ export interface ThresholdSpelling {
23
+ /** What the evidence calls this spelling. */
24
+ readonly name: string;
25
+ /** Tested against a changed path. */
26
+ readonly files: RegExp;
27
+ /**
28
+ * Tested against each line the range removes or adds in such a file. The
29
+ * named group `value` holds the number; the optional group `key` names the
30
+ * setting. A line may hold several matches.
31
+ */
32
+ readonly pattern: RegExp;
33
+ /** A line `pattern` matches. */
34
+ readonly example: string;
35
+ }
36
+ export declare const THRESHOLD_SPELLINGS: ThresholdSpelling[];
37
+ /** One minimum the range lowered or removed. */
38
+ export interface ThresholdFinding {
39
+ /** `<category>:<file>:<line>`. */
40
+ readonly id: string;
41
+ readonly category: ThresholdCategory;
42
+ readonly file: string;
43
+ /** The line in the candidate for a lowered value, in the base for a removed one. */
44
+ readonly line: number;
45
+ readonly name: string;
46
+ readonly key: string | null;
47
+ readonly oldValue: string;
48
+ readonly newValue: string | null;
49
+ /** The sanction's reason, or `null` while the finding is unsanctioned. */
50
+ readonly sanction: string | null;
51
+ }
52
+ export interface LoweredThresholdsRecord extends CheckRecord {
53
+ readonly findings: readonly ThresholdFinding[];
54
+ }
55
+ export interface LoweredThresholdsOptions {
56
+ readonly cwd: string;
57
+ /** The revision the range starts after. */
58
+ readonly base: string;
59
+ /** The revision the range ends at. */
60
+ readonly candidate: string;
61
+ /** Keys are finding ids or `<category>:<file>`; values are the reasons. */
62
+ readonly sanctions?: Readonly<Record<string, string>> | undefined;
63
+ }
64
+ /**
65
+ * Report the coverage minimums `base..candidate` lowers or removes. Fails while
66
+ * any finding is unsanctioned, and fails closed: a revision that names no
67
+ * commit, or any git invocation that fails, is a `fail` record naming the
68
+ * cause - never a `pass`, and never a thrown error.
69
+ */
70
+ export declare function checkLoweredThresholds(options: LoweredThresholdsOptions): Promise<LoweredThresholdsRecord>;
@@ -0,0 +1,281 @@
1
+ /**
2
+ * Lowered thresholds.
3
+ *
4
+ * The cheapest way to make a coverage gate pass is to ask less of it: the
5
+ * minimum in the runner's configuration goes down, or the key goes away. This
6
+ * check reads the lines `base..candidate` changes in the files where a
7
+ * repository states its coverage minimums, pairs each value the range removes
8
+ * with the value it adds for the same setting, and reports a minimum that went
9
+ * down (`threshold-lowered`) or disappeared (`threshold-removed`) for a human
10
+ * to sanction. A raised, new or merely moved value says nothing.
11
+ *
12
+ * Only the DIFF is read, by blob id through git, from the merge base of the
13
+ * two revisions; a low minimum the base already holds is never reported.
14
+ *
15
+ * Which files and which spellings count lives in one exported array,
16
+ * `THRESHOLD_SPELLINGS`, read each time the check runs: a repository on another
17
+ * runner appends its own entry.
18
+ */
19
+ import { execFile } from "node:child_process";
20
+ import { SANCTION_KEY } from "./weakened-tests.js";
21
+ const NAME = "RT-14";
22
+ const NUMBER = "(?<value>\\d+(?:\\.\\d+)?)";
23
+ const KEYS = "(?<key>lines|branches|functions|statements)";
24
+ const RUNNER_CONFIG = /(?:^|\/)(?:(?:vitest|vite|jest)\.config\.(?:[cm]?[jt]s|json)|package\.json|\.nycrc(?:\.json)?|\.c8rc(?:\.json)?)$/;
25
+ export const THRESHOLD_SPELLINGS = [
26
+ {
27
+ name: "coverage key",
28
+ files: RUNNER_CONFIG,
29
+ pattern: new RegExp(`(?<![\\w.-])["']?${KEYS}["']?\\s*:\\s*${NUMBER}`),
30
+ example: "lines: 80,",
31
+ },
32
+ {
33
+ name: "coverage flag",
34
+ files: /(?:^|\/)package\.json$/,
35
+ pattern: new RegExp(`(?<![\\w-])--${KEYS}(?:\\s+|=)${NUMBER}`),
36
+ example: '"check": "c8 check-coverage --lines 80",',
37
+ },
38
+ {
39
+ name: "pytest --cov-fail-under",
40
+ files: /(?:^|\/)(?:pyproject\.toml|setup\.cfg|tox\.ini|pytest\.ini)$/,
41
+ pattern: new RegExp(`(?<![\\w-])--cov-fail-under(?:\\s+|=)${NUMBER}`),
42
+ example: "addopts = --cov-fail-under=80",
43
+ },
44
+ {
45
+ name: "coverage.py fail_under",
46
+ files: /(?:^|\/)(?:pyproject\.toml|setup\.cfg|tox\.ini|\.coveragerc)$/,
47
+ pattern: new RegExp(`(?<![\\w-])fail_under\\s*=\\s*${NUMBER}`),
48
+ example: "fail_under = 80",
49
+ },
50
+ ];
51
+ function runGit(cwd, args) {
52
+ return new Promise((resolve, reject) => {
53
+ execFile("git", args, { cwd, maxBuffer: 1024 * 1024 * 1024, encoding: "utf8" }, (error, stdout, stderr) => {
54
+ if (error) {
55
+ reject(new Error(stderr.trim() || error.message));
56
+ return;
57
+ }
58
+ resolve(stdout);
59
+ });
60
+ });
61
+ }
62
+ /** Escape everything outside printable ASCII, so evidence cannot replay a control sequence. */
63
+ function escapeText(text) {
64
+ let escaped = "";
65
+ for (const char of text) {
66
+ const codePoint = char.codePointAt(0) ?? 0;
67
+ escaped += codePoint >= 0x20 && codePoint <= 0x7e ? char : `\\u{${codePoint.toString(16).padStart(4, "0")}}`;
68
+ }
69
+ return escaped;
70
+ }
71
+ function quote(text) {
72
+ return `"${escapeText(text)}"`;
73
+ }
74
+ async function resolveCommit(cwd, revision) {
75
+ // A revision that begins with a dash would be read as an option.
76
+ if (typeof revision !== "string" || revision.length === 0 || revision.startsWith("-")) {
77
+ throw new Error(`revision ${quote(String(revision))} does not name a commit`);
78
+ }
79
+ let hash = "";
80
+ try {
81
+ hash = (await runGit(cwd, ["rev-parse", "--verify", "--quiet", `${revision}^{commit}`])).trim();
82
+ }
83
+ catch (error) {
84
+ throw new Error(`revision ${quote(revision)} does not name a commit: ${error.message}`);
85
+ }
86
+ if (hash.length === 0) {
87
+ throw new Error(`revision ${quote(revision)} does not name a commit`);
88
+ }
89
+ return hash;
90
+ }
91
+ function toLines(text) {
92
+ if (text.includes("\0")) {
93
+ return [];
94
+ }
95
+ const lines = text.split("\n");
96
+ if (lines[lines.length - 1] === "") {
97
+ lines.pop();
98
+ }
99
+ return lines.map((line, index) => ({ number: index + 1, text: line.replace(/\r$/, "") }));
100
+ }
101
+ async function readBlob(cwd, blob) {
102
+ return blob === null ? [] : toLines(await runGit(cwd, ["cat-file", "blob", blob]));
103
+ }
104
+ async function diffBlobs(cwd, oldBlob, newBlob) {
105
+ const out = await runGit(cwd, ["diff", "-U0", "--no-color", "--no-ext-diff", "--no-textconv", oldBlob, newBlob]);
106
+ const removed = [];
107
+ const added = [];
108
+ let oldLine = 0;
109
+ let newLine = 0;
110
+ let inHunk = false;
111
+ for (const raw of out.split("\n")) {
112
+ const hunk = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(raw);
113
+ if (hunk !== null) {
114
+ oldLine = Number(hunk[1]);
115
+ newLine = Number(hunk[2]);
116
+ inHunk = true;
117
+ }
118
+ else if (inHunk && raw.startsWith("-")) {
119
+ removed.push({ number: oldLine, text: raw.slice(1).replace(/\r$/, "") });
120
+ oldLine += 1;
121
+ }
122
+ else if (inHunk && raw.startsWith("+")) {
123
+ added.push({ number: newLine, text: raw.slice(1).replace(/\r$/, "") });
124
+ newLine += 1;
125
+ }
126
+ }
127
+ return { removed, added };
128
+ }
129
+ async function readChanges(cwd, from, to) {
130
+ const out = await runGit(cwd, ["diff", "--raw", "-z", "--no-abbrev", "--find-renames", "--no-relative", from, to, "--"]);
131
+ const fields = out.split("\0");
132
+ const changes = [];
133
+ let i = 0;
134
+ while (i < fields.length) {
135
+ const head = fields[i] ?? "";
136
+ i += 1;
137
+ if (!head.startsWith(":")) {
138
+ continue;
139
+ }
140
+ const [oldMode = "", newMode = "", oldSha = "", newSha = "", status = ""] = head.slice(1).split(" ");
141
+ const first = fields[i] ?? "";
142
+ i += 1;
143
+ let second = first;
144
+ if (status.startsWith("R") || status.startsWith("C")) {
145
+ second = fields[i] ?? "";
146
+ i += 1;
147
+ }
148
+ const regular = (mode, sha) => mode.startsWith("100") && !/^0+$/.test(sha) ? sha : null;
149
+ const oldBlob = regular(oldMode, oldSha);
150
+ const newBlob = regular(newMode, newSha);
151
+ const lines = oldBlob !== null && newBlob !== null
152
+ ? await diffBlobs(cwd, oldBlob, newBlob)
153
+ : { removed: await readBlob(cwd, oldBlob), added: await readBlob(cwd, newBlob) };
154
+ changes.push({
155
+ oldPath: status.startsWith("A") ? null : first,
156
+ newPath: status.startsWith("D") ? null : second,
157
+ ...lines,
158
+ });
159
+ }
160
+ return changes;
161
+ }
162
+ function hitsOf(spelling, path, lines) {
163
+ if (path === null) {
164
+ return [];
165
+ }
166
+ spelling.files.lastIndex = 0;
167
+ if (!spelling.files.test(path)) {
168
+ return [];
169
+ }
170
+ const flags = spelling.pattern.flags.includes("g") ? spelling.pattern.flags : `${spelling.pattern.flags}g`;
171
+ const pattern = new RegExp(spelling.pattern.source, flags);
172
+ const hits = [];
173
+ for (const line of lines) {
174
+ for (const match of line.text.matchAll(pattern)) {
175
+ const value = match.groups?.["value"];
176
+ if (value !== undefined && Number.isFinite(Number(value))) {
177
+ hits.push({ line: line.number, key: match.groups?.["key"] ?? "", value });
178
+ }
179
+ }
180
+ }
181
+ return hits;
182
+ }
183
+ function draftsOf(changes, spellings) {
184
+ const drafts = [];
185
+ for (const change of changes) {
186
+ for (const spelling of spellings) {
187
+ const removed = hitsOf(spelling, change.oldPath, change.removed);
188
+ const added = hitsOf(spelling, change.newPath, change.added);
189
+ const keys = new Set([...removed, ...added].map((hit) => hit.key));
190
+ for (const key of keys) {
191
+ const before = removed.filter((hit) => hit.key === key);
192
+ const after = added.filter((hit) => hit.key === key);
193
+ before.forEach((old, index) => {
194
+ const now = after[index];
195
+ const shared = { name: spelling.name, key: key === "" ? null : key, oldValue: old.value };
196
+ if (now === undefined) {
197
+ drafts.push({ category: "threshold-removed", file: change.oldPath ?? "", line: old.line, newValue: null, ...shared });
198
+ }
199
+ else if (Number(now.value) < Number(old.value)) {
200
+ drafts.push({ category: "threshold-lowered", file: change.newPath ?? "", line: now.line, newValue: now.value, ...shared });
201
+ }
202
+ });
203
+ }
204
+ }
205
+ }
206
+ return drafts;
207
+ }
208
+ /** The text after the sanction key on every such line of the range's commit messages. */
209
+ async function readSanctionLines(cwd, from, to) {
210
+ const out = await runGit(cwd, ["log", "--format=%B%x00", `${from}..${to}`, "--"]);
211
+ const prefix = `${SANCTION_KEY}:`;
212
+ return out
213
+ .split(/[\n\0]/)
214
+ .map((line) => line.trim())
215
+ .filter((line) => line.startsWith(prefix))
216
+ .map((line) => line.slice(prefix.length).trim());
217
+ }
218
+ function sanctionFor(draft, id, lines, given) {
219
+ for (const key of [id, `${draft.category}:${draft.file}`]) {
220
+ const reason = Object.hasOwn(given, key) ? given[key] : undefined;
221
+ if (typeof reason === "string" && reason.trim().length > 0) {
222
+ return reason.trim();
223
+ }
224
+ for (const line of lines) {
225
+ const rest = line.slice(key.length);
226
+ if (line.startsWith(key) && /^\s+\S/.test(rest)) {
227
+ return rest.trim();
228
+ }
229
+ }
230
+ }
231
+ return null;
232
+ }
233
+ function describeFinding(finding) {
234
+ const setting = finding.key === null ? finding.name : `${finding.name} ${finding.key}`;
235
+ const moved = finding.newValue === null
236
+ ? `removed ${setting}, was ${finding.oldValue}`
237
+ : `lowered ${setting} from ${finding.oldValue} to ${finding.newValue}`;
238
+ const tail = finding.sanction === null ? "" : ` - sanctioned: ${finding.sanction}`;
239
+ return `${escapeText(finding.file)}:${finding.line}: ${escapeText(moved + tail)}`;
240
+ }
241
+ /**
242
+ * Report the coverage minimums `base..candidate` lowers or removes. Fails while
243
+ * any finding is unsanctioned, and fails closed: a revision that names no
244
+ * commit, or any git invocation that fails, is a `fail` record naming the
245
+ * cause - never a `pass`, and never a thrown error.
246
+ */
247
+ export async function checkLoweredThresholds(options) {
248
+ try {
249
+ const { cwd, base, candidate } = options;
250
+ const baseCommit = await resolveCommit(cwd, base);
251
+ const candidateCommit = await resolveCommit(cwd, candidate);
252
+ let ancestor;
253
+ try {
254
+ ancestor = (await runGit(cwd, ["merge-base", baseCommit, candidateCommit])).trim();
255
+ }
256
+ catch (error) {
257
+ throw new Error(`no merge base of ${quote(base)} and ${quote(candidate)}: ${error.message}`);
258
+ }
259
+ const changes = await readChanges(cwd, ancestor, candidateCommit);
260
+ const drafts = draftsOf(changes, THRESHOLD_SPELLINGS);
261
+ const lines = drafts.length === 0 ? [] : await readSanctionLines(cwd, baseCommit, candidateCommit);
262
+ const findings = drafts.map((draft) => {
263
+ const id = `${draft.category}:${draft.file}:${draft.line}`;
264
+ return { id, ...draft, sanction: sanctionFor(draft, id, lines, options.sanctions ?? {}) };
265
+ });
266
+ if (findings.length === 0) {
267
+ return { name: NAME, status: "pass", evidence: ["no threshold lowered or removed"], findings };
268
+ }
269
+ const open = findings.some((finding) => finding.sanction === null);
270
+ return { name: NAME, status: open ? "fail" : "pass", evidence: findings.map(describeFinding), findings };
271
+ }
272
+ catch (error) {
273
+ return {
274
+ name: NAME,
275
+ status: "fail",
276
+ evidence: [escapeText(error instanceof Error ? error.message : String(error))],
277
+ findings: [],
278
+ };
279
+ }
280
+ }
281
+ //# sourceMappingURL=lowered-thresholds.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lowered-thresholds.js","sourceRoot":"","sources":["../src/lowered-thresholds.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The measurement rule.
3
+ *
4
+ * "A number without a command and a date is not a fact." This check reads the
5
+ * bodies of the commits in `base..candidate` and the lines the range adds to
6
+ * changed `.md` files, finds the author's own sentences that assert a
7
+ * measured quantity, and reports the ones whose BLOCK holds no command or no
8
+ * date.
9
+ *
10
+ * A BLOCK is the whole body of one commit (the message without its subject
11
+ * line), or one paragraph of a document - the lines between two blank lines,
12
+ * a fenced block never split. A COMMAND is an inline code span or a fenced
13
+ * block in the block; a DATE is an ISO date in it.
14
+ *
15
+ * A CLAIM is a numeral that carries a unit of measure (`12 ms`, `40%`,
16
+ * `3 files`, `2x`). Digits alone are not a claim: a version, a SHA, an ISO
17
+ * date, a section or figure number, a line reference, or a path containing
18
+ * digits has no unit and is never matched. Lines inside a fenced block, lines
19
+ * beginning with `>`, and text inside an inline code span are not the
20
+ * author's own prose and are never read as claims.
21
+ *
22
+ * Git is always run with an argument vector, and every failure is a `fail`
23
+ * record - never a `pass`, never a throw.
24
+ */
25
+ import type { CheckRecord } from "./index.js";
26
+ /** Where to look, and which two revisions bound the range. */
27
+ export interface MeasurementRuleOptions {
28
+ /** The git repository to run in. */
29
+ readonly cwd: string;
30
+ /** The revision the range starts after. */
31
+ readonly base: string;
32
+ /** The revision the range ends at. */
33
+ readonly candidate: string;
34
+ }
35
+ /**
36
+ * Check the commit bodies in `base..candidate` and the lines the range adds
37
+ * to `.md` files. Fails closed on any git failure.
38
+ */
39
+ export declare function checkMeasurementRule(options: MeasurementRuleOptions): Promise<CheckRecord>;