paperlint 13.0.0 → 13.0.2
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/dist/adapters/git/index.js +46 -18
- package/dist/adapters/git/index.js.map +1 -1
- package/docs/rules/repro/outputs-before-decision.md +4 -1
- package/package.json +2 -2
- package/skills/analyze-sibling-paper/SKILL.md +13 -8
- package/skills/analyze-sibling-paper/SKILL.md.spec.ts +15 -7
- package/src/adapters/git/history.test.ts +63 -0
- package/src/adapters/git/index.test.ts +14 -5
- package/src/adapters/git/index.ts +54 -24
|
@@ -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, [
|
|
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
|
-
/**
|
|
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
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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.
|
|
3
|
+
"version": "13.0.2",
|
|
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",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"ts-essentials": "^10.2.1",
|
|
41
41
|
"typescript": "^6.0.3",
|
|
42
42
|
"unpdf": "^1.8.1",
|
|
43
|
-
"vigiles": "^
|
|
43
|
+
"vigiles": "^39.2.0",
|
|
44
44
|
"zod": "^4.6.5"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
name: analyze-sibling-paper
|
|
3
3
|
description: "Deep-analyze a specific related / competing / concurrent paper — reading the ACTUAL paper, not its abstract — to decide the \"isn't this just X?\" question before a reviewer asks it: what it really does, whether it's prior or concurrent work, the strongest scoop a reviewer could claim, whether your delta is REAL or merely cosmetic, and the exact cite + positioning clause (or a scoop escalation) to apply. Saves the analysis to the paper's research folder and surfaces action points. Use whenever a paper close enough to yours surfaces — via verify-citations' nearest-neighbor, a reviewer's \"this was already done\", your own lit search, or an arXiv alert. Distinct from verify-citations (confirms a cite is real + one-line delta) and study-accepted-papers (mines a venue's accepted corpus for levers); this one does the deep per-competitor overlap/scoop analysis. Compose with verify-citations (feeds it neighbors), harden-paper (its nearest-neighbor-scoop axis calls this), and extend-paper (a scooped angle can become the extension's pivot)."
|
|
4
4
|
context: fork
|
|
5
|
+
model: sonnet
|
|
5
6
|
allowed-tools: [WebSearch, WebFetch, Read, Write, Grep, Glob, Bash, Agent]
|
|
6
7
|
---
|
|
7
8
|
|
|
8
|
-
<!-- vigiles:sha256:
|
|
9
|
+
<!-- vigiles:sha256:17c3e0a4c03c6a33 compiled from skills/analyze-sibling-paper/SKILL.md.spec.ts -->
|
|
9
10
|
|
|
10
11
|
# analyze-sibling-paper — settle "isn't this just X?" before a reviewer does
|
|
11
12
|
|
|
@@ -26,13 +27,17 @@ far below the cost of a "novelty concern" reject.
|
|
|
26
27
|
|
|
27
28
|
## How to run it
|
|
28
29
|
|
|
29
|
-
1. **Deep-read the ACTUAL paper, not the abstract.** `WebFetch`
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
`
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
1. **Deep-read the ACTUAL paper, not the abstract.** `WebFetch` returns a model's summary of a page, not
|
|
31
|
+
its text: use it only to find the link (`arxiv.org/abs/<id>` for metadata and version), never as the
|
|
32
|
+
read, and never take absence from its output as proof. Save the full text to a file and read the file with
|
|
33
|
+
`Read`, abstract → intro → method → claims: the PDF (`arxiv.org/pdf/<id>`) fetched with `curl` into an
|
|
34
|
+
empty directory and extracted with plain `pdftotext <pdf> <txt>` (never `-layout`: it glues the two
|
|
35
|
+
page columns into one line and a quote is no longer contiguous), or the HTML (`arxiv.org/html/<id>v1`,
|
|
36
|
+
`ar5iv.labs.arxiv.org/html/<id>`) saved with `curl`. **A 403/404 on one route is not "inaccessible"**;
|
|
37
|
+
switch channel before concluding. An abstract hides exactly the overlap that matters. **Record what you
|
|
38
|
+
actually read** in the card's frontmatter as `read: full | abstract | none`: `full` only after the
|
|
39
|
+
saved file was read, and name in a `**Read:**` line the sections you covered and the ones you could not
|
|
40
|
+
— an abstract-only verdict is PROVISIONAL and must say so.
|
|
36
41
|
🔴 **Verify the abstract's numbers against the body.** They disagree more often than you would expect,
|
|
37
42
|
and a disagreement is itself a finding worth recording.
|
|
38
43
|
|
|
@@ -11,6 +11,10 @@ export default experimental_skill({
|
|
|
11
11
|
description:
|
|
12
12
|
"Deep-analyze a specific related / competing / concurrent paper — reading the ACTUAL paper, not its abstract — to decide the \"isn't this just X?\" question before a reviewer asks it: what it really does, whether it's prior or concurrent work, the strongest scoop a reviewer could claim, whether your delta is REAL or merely cosmetic, and the exact cite + positioning clause (or a scoop escalation) to apply. Saves the analysis to the paper's research folder and surfaces action points. Use whenever a paper close enough to yours surfaces — via verify-citations' nearest-neighbor, a reviewer's \"this was already done\", your own lit search, or an arXiv alert. Distinct from verify-citations (confirms a cite is real + one-line delta) and study-accepted-papers (mines a venue's accepted corpus for levers); this one does the deep per-competitor overlap/scoop analysis. Compose with verify-citations (feeds it neighbors), harden-paper (its nearest-neighbor-scoop axis calls this), and extend-paper (a scooped angle can become the extension's pivot).",
|
|
13
13
|
context: "fork",
|
|
14
|
+
// Reading one paper and copying verbatim quotes is a fixed spec, so the fork runs on Sonnet; without this
|
|
15
|
+
// line it runs as general-purpose on the caller's model (vigiles#401). Step 3's delta judgement still goes
|
|
16
|
+
// to an independent Fable subagent.
|
|
17
|
+
model: "sonnet",
|
|
14
18
|
tools: [
|
|
15
19
|
"WebSearch",
|
|
16
20
|
"WebFetch",
|
|
@@ -41,13 +45,17 @@ far below the cost of a "novelty concern" reject.
|
|
|
41
45
|
|
|
42
46
|
## How to run it
|
|
43
47
|
|
|
44
|
-
1. **Deep-read the ACTUAL paper, not the abstract.** \`WebFetch\`
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
\`
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
1. **Deep-read the ACTUAL paper, not the abstract.** \`WebFetch\` returns a model's summary of a page, not
|
|
49
|
+
its text: use it only to find the link (\`arxiv.org/abs/<id>\` for metadata and version), never as the
|
|
50
|
+
read, and never take absence from its output as proof. Save the full text to a file and read the file with
|
|
51
|
+
\`Read\`, abstract → intro → method → claims: the PDF (\`arxiv.org/pdf/<id>\`) fetched with \`curl\` into an
|
|
52
|
+
empty directory and extracted with plain \`pdftotext <pdf> <txt>\` (never \`-layout\`: it glues the two
|
|
53
|
+
page columns into one line and a quote is no longer contiguous), or the HTML (\`arxiv.org/html/<id>v1\`,
|
|
54
|
+
\`ar5iv.labs.arxiv.org/html/<id>\`) saved with \`curl\`. **A 403/404 on one route is not "inaccessible"**;
|
|
55
|
+
switch channel before concluding. An abstract hides exactly the overlap that matters. **Record what you
|
|
56
|
+
actually read** in the card's frontmatter as \`read: full | abstract | none\`: \`full\` only after the
|
|
57
|
+
saved file was read, and name in a \`**Read:**\` line the sections you covered and the ones you could not
|
|
58
|
+
— an abstract-only verdict is PROVISIONAL and must say so.
|
|
51
59
|
🔴 **Verify the abstract's numbers against the body.** They disagree more often than you would expect,
|
|
52
60
|
and a disagreement is itself a finding worth recording.
|
|
53
61
|
|
|
@@ -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
|
-
//
|
|
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(
|
|
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 = (
|
|
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
|
|
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
|
|
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, [
|
|
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(
|
|
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
|
-
/**
|
|
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
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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(
|