paperlint 13.0.0 → 13.0.1

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.
@@ -78,9 +78,32 @@ const inHead = (git, cwd, file) => {
78
78
  const t = git(cwd, ["ls-tree", "--name-only", "HEAD", "--", file]);
79
79
  return "why" in t ? t : { present: t.out.trim() !== "" };
80
80
  };
81
+ /**
82
+ * Every ancestor, not git's simplified history: a merge whose first parent already holds the same tree
83
+ * (a squash of the branch it merges) would otherwise hide that branch, and with it the commits that
84
+ * show the decision came first.
85
+ */
86
+ const WHOLE_HISTORY = ["--full-history"];
87
+ /** Every path ever under the study's folder, oldest commit first, each commit's paths after its sha and date. */
88
+ const FIRST_COMMITS = [
89
+ "log",
90
+ "--reverse",
91
+ ...WHOLE_HISTORY,
92
+ "--format=%x01%H%x09%cI",
93
+ "--name-only",
94
+ "--relative",
95
+ "--",
96
+ ".",
97
+ ];
81
98
  /** The decision's first commit (null: never committed), and whether HEAD's tree still holds it. */
82
99
  const decisionIn = (git, studyDir, decision) => {
83
- const d = git(studyDir, ["log", "--format=%H%x09%cI", "--", decision]);
100
+ const d = git(studyDir, [
101
+ "log",
102
+ ...WHOLE_HISTORY,
103
+ "--format=%H%x09%cI",
104
+ "--",
105
+ decision,
106
+ ]);
84
107
  if ("why" in d)
85
108
  return d;
86
109
  const lines = d.out.trim().split("\n");
@@ -100,18 +123,10 @@ function orderIn(git, studyDir, decision) {
100
123
  const d = decisionIn(git, studyDir, decision);
101
124
  if ("why" in d)
102
125
  return { kind: "unknown", why: d.why };
103
- const files = git(studyDir, [
104
- "log",
105
- "--reverse",
106
- "--format=%x01%H%x09%cI",
107
- "--name-only",
108
- "--relative",
109
- "--",
110
- ".",
111
- ]);
126
+ const files = git(studyDir, FIRST_COMMITS);
112
127
  if ("why" in files)
113
128
  return { kind: "unknown", why: files.why };
114
- const ordered = ordersOf(git, studyDir, d.at, firstCommitsOf(files.out));
129
+ const ordered = ordersOf(git, studyDir, { at: d.at, path: decision }, firstCommitsOf(files.out));
115
130
  return "why" in ordered
116
131
  ? { kind: "unknown", why: ordered.why }
117
132
  : {
@@ -123,16 +138,29 @@ function orderIn(git, studyDir, decision) {
123
138
  }
124
139
  const isFailed = (a) => "why" in a;
125
140
  const isOrdered = (a) => "order" in a;
126
- /** The order of `commit` against `decision`'s first commit. */
141
+ /**
142
+ * The order of `commit` against the decision: after when any commit that touched the decision is its
143
+ * strict ancestor — not only the decision's first commit, which a squash can copy onto another line
144
+ * of history — the same when `commit` itself first carries it, not after otherwise.
145
+ */
127
146
  const answerFor = (git, cwd, decision, commit) => {
128
- if (decision === null)
147
+ if (decision.at === null)
129
148
  return { commit, order: "not-after" };
130
- if (commit === decision.commit)
149
+ if (commit === decision.at.commit)
131
150
  return { commit, order: "same" };
132
- const a = git(cwd, ["merge-base", "--is-ancestor", decision.commit, commit], [0, 1]);
133
- if ("why" in a)
134
- return { commit, why: a.why };
135
- return { commit, order: a.status === 0 ? "after" : "not-after" };
151
+ const r = git(cwd, [
152
+ "rev-list",
153
+ ...WHOLE_HISTORY,
154
+ commit,
155
+ "--",
156
+ decision.path,
157
+ ]);
158
+ if ("why" in r)
159
+ return { commit, why: r.why };
160
+ const touched = r.out.split("\n").filter((sha) => sha !== "");
161
+ if (touched.some((sha) => sha !== commit))
162
+ return { commit, order: "after" };
163
+ return { commit, order: touched.length > 0 ? "same" : "not-after" };
136
164
  };
137
165
  /** Each file's first commit with its order against `decision`'s; git asked once per distinct commit. */
138
166
  function ordersOf(git, cwd, decision, firsts) {
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/adapters/git/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/adapters/git/index.ts"],"names":[],"mappings":"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"}
@@ -60,7 +60,10 @@ the same check and get the same answer. The design is
60
60
  **It asks git for an order, never pins a sha**, which is why it can live as a lint rule (rules in
61
61
  `eslint-rules/` may not run git: a pinned sha is deleted by a squash or a gc). A squash merge keeps
62
62
  the order when the decision was merged before the run — the decide-first workflow — and a shallow
63
- checkout is named, not read as a violation.
63
+ checkout is named, not read as a violation. A squash of a branch that held both the decision and the
64
+ run puts them in one commit, and that order is gone from the squashed history; merge such a branch
65
+ with a merge commit instead. History is read whole (`--full-history`): a merge whose first parent is
66
+ already identical to it (a squash of the branch it merges back) does not hide that branch's commits.
64
67
 
65
68
  ## Examples
66
69
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperlint",
3
- "version": "13.0.0",
3
+ "version": "13.0.1",
4
4
  "description": "A linter for scientific papers written in LaTeX: catches mistakes before you submit to a conference or journal.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -17,6 +17,8 @@ const ports = gitHistory(spawnProcess(), { PATH: String(process.env.PATH) });
17
17
  const T1 = "2026-10-16T10:00:00+05:00";
18
18
  const T2 = "2026-10-16T11:00:00+05:00";
19
19
  const T3 = "2026-10-16T12:00:00+05:00";
20
+ const T4 = "2026-10-16T13:00:00+05:00";
21
+ const T5 = "2026-10-16T14:00:00+05:00";
20
22
 
21
23
  it("a decision committed, then changed, then never committed: three states", () => {
22
24
  const repo = join(root, "committed");
@@ -75,6 +77,67 @@ it("outputs after the decision, in its commit, before it, and deleted since: eac
75
77
  });
76
78
  });
77
79
 
80
+ it("a squash on the first parent does not hide the order its merged-in branch recorded", () => {
81
+ const repo = join(root, "squashed");
82
+ initRepo(repo);
83
+ commit(repo, { "README.md": "r\n" }, T1);
84
+ git(repo, ["checkout", "-q", "-b", "study"], T1);
85
+ const decided = commit(repo, { "p/repro/S/decision.md": "d\n" }, T2);
86
+ const ran = commit(repo, { "p/repro/S/out.csv": "1\n" }, T3);
87
+ git(repo, ["checkout", "-q", "main"], T3);
88
+ commit(
89
+ repo,
90
+ { "p/repro/S/decision.md": "d\n", "p/repro/S/out.csv": "1\n" },
91
+ T4,
92
+ "squash",
93
+ );
94
+ git(repo, ["merge", "-q", "--no-ff", "-m", "merge", "study"], T5);
95
+ // Guards: the merge is TREESAME to the squash, its first parent, so git's default history
96
+ // simplification walks only the squash and reads decision and output as born in one commit.
97
+ expect(
98
+ ports.order(
99
+ absolutePath(join(repo, "p/repro/S")),
100
+ absolutePath(join(repo, "p/repro/S/decision.md")),
101
+ ),
102
+ ).toEqual({
103
+ kind: "read",
104
+ decision: { commit: decided, date: T2 },
105
+ decisionDeleted: false,
106
+ files: [
107
+ { path: "decision.md", commit: decided, date: T2, order: "same" },
108
+ { path: "out.csv", commit: ran, date: T3, order: "after" },
109
+ ],
110
+ });
111
+ });
112
+
113
+ it("a decision squashed into main, then its branch merged back: outputs on main stay after it", () => {
114
+ const repo = join(root, "decision-squashed");
115
+ initRepo(repo);
116
+ commit(repo, { "README.md": "r\n" }, T1);
117
+ git(repo, ["checkout", "-q", "-b", "decide"], T1);
118
+ const original = commit(repo, { "p/repro/S/decision.md": "d\n" }, T2);
119
+ git(repo, ["checkout", "-q", "main"], T2);
120
+ commit(repo, { "p/repro/S/decision.md": "d\n" }, T3, "squash");
121
+ const ran = commit(repo, { "p/repro/S/out.csv": "1\n" }, T4);
122
+ git(repo, ["merge", "-q", "--no-ff", "-m", "merge", "decide"], T5);
123
+ // Guards: the earliest decision commit is on the merged-back branch and is no ancestor of the run,
124
+ // but the squash that carried the same decision is; any commit holding the decision before counts.
125
+ expect(
126
+ ports.order(
127
+ absolutePath(join(repo, "p/repro/S")),
128
+ absolutePath(join(repo, "p/repro/S/decision.md")),
129
+ ),
130
+ ).toEqual({
131
+ kind: "read",
132
+ decision: { commit: original, date: T2 },
133
+ decisionDeleted: false,
134
+ files: [
135
+ { path: "decision.md", commit: original, date: T2, order: "same" },
136
+ { path: "out.csv", commit: ran, date: T4, order: "after" },
137
+ ],
138
+ });
139
+ });
140
+
78
141
  it("a shallow clone is named, not judged", () => {
79
142
  const repo = join(root, "deep");
80
143
  initRepo(repo);
@@ -15,6 +15,7 @@ const STUDY = absolutePath("/repo/papers/p/repro/S");
15
15
  const D = "d".repeat(40);
16
16
  const A = "a".repeat(40);
17
17
  const B = "b".repeat(40);
18
+ const C = "c".repeat(40);
18
19
  const DATE = "2026-10-16T15:23:00+05:00";
19
20
 
20
21
  /** HEAD's tree holds the decision, unless a test says otherwise. */
@@ -146,8 +147,15 @@ it("order: a shallow clone is said, not judged", () => {
146
147
  });
147
148
 
148
149
  it("order: each file's first commit against the decision's — after, the same, not after", () => {
149
- const log = `\x01${D}\t${DATE}\nx.csv\n\x01${A}\t${DATE}\na.csv\nx.csv\n\x01${B}\t${DATE}\nb.csv\n`;
150
- // Guards: the decision's own commit is `same`; the others are ordered by ancestry.
150
+ const log = `\x01${D}\t${DATE}\nx.csv\n\x01${A}\t${DATE}\na.csv\nx.csv\n\x01${B}\t${DATE}\nb.csv\n\x01${C}\t${DATE}\nc.csv\n`;
151
+ // The commits that touched the decision and are reachable from each one, as `rev-list` names them.
152
+ const touching: Readonly<Record<string, string>> = {
153
+ [A]: `${D}\n`,
154
+ [B]: "",
155
+ [C]: `${C}\n`,
156
+ };
157
+ // Guards: the decision's own commit is `same`, and so is another that first carries it (a squash);
158
+ // a commit with a decision commit among its strict ancestors is `after`; the rest are `not-after`.
151
159
  const result = gitHistory(
152
160
  scriptedProcess((c) => {
153
161
  const sub = String(c.args[2]);
@@ -155,7 +163,7 @@ it("order: each file's first commit against the decision's — after, the same,
155
163
  if (sub === "ls-tree") return IN_HEAD;
156
164
  if (sub === "log" && c.args.includes("--reverse")) return exitedWith(log);
157
165
  if (sub === "log") return exitedWith(`${A}\t${DATE}\n${D}\t${DATE}\n`);
158
- return exitedWith("", String(c.args[5]) === A ? 0 : 1);
166
+ return exitedWith(touching[String(c.args[4])] ?? "");
159
167
  }),
160
168
  {},
161
169
  ).order(STUDY, FILE);
@@ -167,13 +175,14 @@ it("order: each file's first commit against the decision's — after, the same,
167
175
  { path: "x.csv", commit: D, date: DATE, order: "same" },
168
176
  { path: "a.csv", commit: A, date: DATE, order: "after" },
169
177
  { path: "b.csv", commit: B, date: DATE, order: "not-after" },
178
+ { path: "c.csv", commit: C, date: DATE, order: "same" },
170
179
  ],
171
180
  });
172
181
  });
173
182
 
174
183
  it("order: no decision commit makes every file not-after; a failed ancestry question is a reason", () => {
175
184
  const log = `\x01${A}\t${DATE}\na.csv\n`;
176
- const answer = (mergeBase: ProcessExit, decisionLog: string) =>
185
+ const answer = (revList: ProcessExit, decisionLog: string) =>
177
186
  gitHistory(
178
187
  scriptedProcess((c) => {
179
188
  const sub = String(c.args[2]);
@@ -182,7 +191,7 @@ it("order: no decision commit makes every file not-after; a failed ancestry ques
182
191
  if (sub === "log" && c.args.includes("--reverse"))
183
192
  return exitedWith(log);
184
193
  if (sub === "log") return exitedWith(decisionLog);
185
- return mergeBase;
194
+ return revList;
186
195
  }),
187
196
  {},
188
197
  ).order(STUDY, FILE);
@@ -6,7 +6,7 @@
6
6
  * (`git status --porcelain`)
7
7
  * order whether the clone is shallow; the decision file's first commit; every file's first
8
8
  * commit under the study's folder (`git log --reverse --name-only`); and for each, whether
9
- * the decision's first commit is its strict ancestor (`git merge-base --is-ancestor`)
9
+ * any commit that touched the decision is its strict ancestor (`git rev-list -- <decision>`)
10
10
  */
11
11
  import type { AbsolutePath } from "../../domain/paths.ts";
12
12
  import type {
@@ -125,6 +125,25 @@ const inHead = (
125
125
  return "why" in t ? t : { present: t.out.trim() !== "" };
126
126
  };
127
127
 
128
+ /**
129
+ * Every ancestor, not git's simplified history: a merge whose first parent already holds the same tree
130
+ * (a squash of the branch it merges) would otherwise hide that branch, and with it the commits that
131
+ * show the decision came first.
132
+ */
133
+ const WHOLE_HISTORY = ["--full-history"] as const;
134
+
135
+ /** Every path ever under the study's folder, oldest commit first, each commit's paths after its sha and date. */
136
+ const FIRST_COMMITS = [
137
+ "log",
138
+ "--reverse",
139
+ ...WHOLE_HISTORY,
140
+ "--format=%x01%H%x09%cI",
141
+ "--name-only",
142
+ "--relative",
143
+ "--",
144
+ ".",
145
+ ] as const;
146
+
128
147
  /** The decision's first commit (null: never committed), and whether HEAD's tree still holds it. */
129
148
  const decisionIn = (
130
149
  git: Git,
@@ -133,7 +152,13 @@ const decisionIn = (
133
152
  ):
134
153
  | { readonly at: CommitAt | null; readonly deleted: boolean }
135
154
  | { readonly why: string } => {
136
- const d = git(studyDir, ["log", "--format=%H%x09%cI", "--", decision]);
155
+ const d = git(studyDir, [
156
+ "log",
157
+ ...WHOLE_HISTORY,
158
+ "--format=%H%x09%cI",
159
+ "--",
160
+ decision,
161
+ ]);
137
162
  if ("why" in d) return d;
138
163
  const lines = d.out.trim().split("\n");
139
164
  const first = commitAt(nth(lines, lines.length - 1));
@@ -153,17 +178,14 @@ function orderIn(
153
178
  if (shallow.out.trim() === "true") return { kind: "shallow" };
154
179
  const d = decisionIn(git, studyDir, decision);
155
180
  if ("why" in d) return { kind: "unknown", why: d.why };
156
- const files = git(studyDir, [
157
- "log",
158
- "--reverse",
159
- "--format=%x01%H%x09%cI",
160
- "--name-only",
161
- "--relative",
162
- "--",
163
- ".",
164
- ]);
181
+ const files = git(studyDir, FIRST_COMMITS);
165
182
  if ("why" in files) return { kind: "unknown", why: files.why };
166
- const ordered = ordersOf(git, studyDir, d.at, firstCommitsOf(files.out));
183
+ const ordered = ordersOf(
184
+ git,
185
+ studyDir,
186
+ { at: d.at, path: decision },
187
+ firstCommitsOf(files.out),
188
+ );
167
189
  return "why" in ordered
168
190
  ? { kind: "unknown", why: ordered.why }
169
191
  : {
@@ -185,29 +207,37 @@ const isOrdered = (
185
207
  a: Answer,
186
208
  ): a is { readonly commit: string; readonly order: Order } => "order" in a;
187
209
 
188
- /** The order of `commit` against `decision`'s first commit. */
210
+ /**
211
+ * The order of `commit` against the decision: after when any commit that touched the decision is its
212
+ * strict ancestor — not only the decision's first commit, which a squash can copy onto another line
213
+ * of history — the same when `commit` itself first carries it, not after otherwise.
214
+ */
189
215
  const answerFor = (
190
216
  git: Git,
191
217
  cwd: AbsolutePath,
192
- decision: CommitAt | null,
218
+ decision: { readonly at: CommitAt | null; readonly path: AbsolutePath },
193
219
  commit: string,
194
220
  ): Answer => {
195
- if (decision === null) return { commit, order: "not-after" };
196
- if (commit === decision.commit) return { commit, order: "same" };
197
- const a = git(
198
- cwd,
199
- ["merge-base", "--is-ancestor", decision.commit, commit],
200
- [0, 1],
201
- );
202
- if ("why" in a) return { commit, why: a.why };
203
- return { commit, order: a.status === 0 ? "after" : "not-after" };
221
+ if (decision.at === null) return { commit, order: "not-after" };
222
+ if (commit === decision.at.commit) return { commit, order: "same" };
223
+ const r = git(cwd, [
224
+ "rev-list",
225
+ ...WHOLE_HISTORY,
226
+ commit,
227
+ "--",
228
+ decision.path,
229
+ ]);
230
+ if ("why" in r) return { commit, why: r.why };
231
+ const touched = r.out.split("\n").filter((sha) => sha !== "");
232
+ if (touched.some((sha) => sha !== commit)) return { commit, order: "after" };
233
+ return { commit, order: touched.length > 0 ? "same" : "not-after" };
204
234
  };
205
235
 
206
236
  /** Each file's first commit with its order against `decision`'s; git asked once per distinct commit. */
207
237
  function ordersOf(
208
238
  git: Git,
209
239
  cwd: AbsolutePath,
210
- decision: CommitAt | null,
240
+ decision: { readonly at: CommitAt | null; readonly path: AbsolutePath },
211
241
  firsts: ReadonlyMap<string, CommitAt>,
212
242
  ): { readonly files: readonly FirstCommit[] } | { readonly why: string } {
213
243
  const answers = [...new Set([...firsts.values()].map((c) => c.commit))].map(