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.
@@ -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":"AAsBA,MAAM,UAAU,GAAG,MAAM,CAAC;AAE1B,6FAA6F;AAC7F,MAAM,QAAQ,GAAG,CACf,CAAc,EACd,UAA6B,CAAC,CAAC,CAAC,EAGL,EAAE;IAC7B,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC;QACf,KAAK,QAAQ;YACX,OAAO,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC;gBAC/B,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE;gBACrC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,cAAc,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;QACnE,KAAK,WAAW;YACd,OAAO,EAAE,GAAG,EAAE,sBAAsB,EAAE,CAAC;QACzC,KAAK,cAAc;YACjB,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC;QAC5B,KAAK,WAAW,CAAC;QACjB,KAAK,WAAW;YACd,OAAO,EAAE,GAAG,EAAE,uBAAuB,CAAC,CAAC,IAAI,GAAG,EAAE,CAAC;IACrD,CAAC;AACH,CAAC,CAAC;AAEF,2EAA2E;AAC3E,MAAM,KAAK,GACT,CAAC,GAAe,EAAE,GAAqC,EAAE,EAAE,CAC3D,CAAC,GAAiB,EAAE,IAAuB,EAAE,OAA2B,EAAE,EAAE,CAC1E,QAAQ,CACN,GAAG,CAAC,GAAG,CAAC;IACN,IAAI,EAAE,KAAK;IACX,IAAI,EAAE,CAAC,IAAI,EAAE,sBAAsB,EAAE,GAAG,IAAI,CAAC;IAC7C,GAAG;IACH,SAAS,EAAE,UAAU;IACrB,GAAG;IACH,cAAc,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI;CACjC,CAAC,EACF,OAAO,CACR,CAAC;AAEN,mCAAmC;AACnC,MAAM,QAAQ,GAAG,CAAC,IAAY,EAAY,EAAE;IAC1C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC/B,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,CAAC;AACxD,CAAC,CAAC;AAEF,qDAAqD;AACrD,MAAM,GAAG,GAAG,CAAC,KAAwB,EAAE,CAAS,EAAU,EAAE,CAC1D,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AAEjC;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,GAAW,EAAiC,EAAE,CAC3E,GAAG;KACA,KAAK,CAAC,MAAM,CAAC;KACb,KAAK,CAAC,CAAC,CAAC;KACR,MAAM,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;IACtB,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAChC,MAAM,EAAE,GAAG,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;IACnC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC7B,OAAO,KAAK;SACT,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;SACvC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;AACtD,CAAC,EAAE,IAAI,GAAG,EAAoB,CAAC,CAAC;AAEpC,oCAAoC;AACpC,MAAM,UAAU,UAAU,CACxB,GAAe,EACf,GAAqC;IAErC,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IAC5B,OAAO;QACL,SAAS,EAAE,CAAC,KAAK,EAAE,IAAI,EAAa,EAAE;YACpC,MAAM,GAAG,GAAG,GAAG,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,oBAAoB,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;YACxE,IAAI,KAAK,IAAI,GAAG;gBAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,CAAC;YAC3D,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;YAClD,IAAI,MAAM,KAAK,EAAE;gBAAE,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,CAAC;YACpD,sFAAsF;YACtF,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;YACtC,IAAI,KAAK,IAAI,IAAI;gBAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC;YAC7D,IAAI,CAAC,IAAI,CAAC,OAAO;gBAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC;YACtD,MAAM,MAAM,GAAG,GAAG,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,gBAAgB,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;YACpE,IAAI,KAAK,IAAI,MAAM;gBAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC;YACjE,OAAO,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE;gBAC7B,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE;gBACrC,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC;QAClC,CAAC;QACD,KAAK,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,QAAQ,EAAE,QAAQ,CAAC;KAChE,CAAC;AACJ,CAAC;AAID,sGAAsG;AACtG,MAAM,MAAM,GAAG,CACb,GAAQ,EACR,GAAiB,EACjB,IAAkB,EACwC,EAAE;IAC5D,MAAM,CAAC,GAAG,GAAG,CAAC,GAAG,EAAE,CAAC,SAAS,EAAE,aAAa,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;IACnE,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;AAC3D,CAAC,CAAC;AAEF,mGAAmG;AACnG,MAAM,UAAU,GAAG,CACjB,GAAQ,EACR,QAAsB,EACtB,QAAsB,EAGK,EAAE;IAC7B,MAAM,CAAC,GAAG,GAAG,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC;IACvE,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC;IACzB,MAAM,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACvC,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;IACrD,IAAI,KAAK,CAAC,MAAM,KAAK,EAAE;QAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC7D,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC7C,OAAO,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;AACtE,CAAC,CAAC;AAEF,yEAAyE;AACzE,SAAS,OAAO,CACd,GAAQ,EACR,QAAsB,EACtB,QAAsB;IAEtB,MAAM,OAAO,GAAG,GAAG,CAAC,QAAQ,EAAE,CAAC,WAAW,EAAE,yBAAyB,CAAC,CAAC,CAAC;IACxE,IAAI,KAAK,IAAI,OAAO;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC;IACnE,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,MAAM;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;IAC9D,MAAM,CAAC,GAAG,UAAU,CAAC,GAAG,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC9C,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC;IACvD,MAAM,KAAK,GAAG,GAAG,CAAC,QAAQ,EAAE;QAC1B,KAAK;QACL,WAAW;QACX,wBAAwB;QACxB,aAAa;QACb,YAAY;QACZ,IAAI;QACJ,GAAG;KACJ,CAAC,CAAC;IACH,IAAI,KAAK,IAAI,KAAK;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,CAAC;IAC/D,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,cAAc,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IACzE,OAAO,KAAK,IAAI,OAAO;QACrB,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE;QACvC,CAAC,CAAC;YACE,IAAI,EAAE,MAAM;YACZ,QAAQ,EAAE,CAAC,CAAC,EAAE;YACd,eAAe,EAAE,CAAC,CAAC,OAAO;YAC1B,KAAK,EAAE,OAAO,CAAC,KAAK;SACrB,CAAC;AACR,CAAC;AAMD,MAAM,QAAQ,GAAG,CACf,CAAS,EAC+C,EAAE,CAAC,KAAK,IAAI,CAAC,CAAC;AACxE,MAAM,SAAS,GAAG,CAChB,CAAS,EACgD,EAAE,CAAC,OAAO,IAAI,CAAC,CAAC;AAE3E,+DAA+D;AAC/D,MAAM,SAAS,GAAG,CAChB,GAAQ,EACR,GAAiB,EACjB,QAAyB,EACzB,MAAc,EACN,EAAE;IACV,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC;IAC7D,IAAI,MAAM,KAAK,QAAQ,CAAC,MAAM;QAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IACjE,MAAM,CAAC,GAAG,GAAG,CACX,GAAG,EACH,CAAC,YAAY,EAAE,eAAe,EAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,EACxD,CAAC,CAAC,EAAE,CAAC,CAAC,CACP,CAAC;IACF,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC;IAC9C,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;AACnE,CAAC,CAAC;AAEF,wGAAwG;AACxG,SAAS,QAAQ,CACf,GAAQ,EACR,GAAiB,EACjB,QAAyB,EACzB,MAAqC;IAErC,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,CACzE,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC,CACxC,CAAC;IACF,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACtC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC;IACrD,OAAO;QACL,KAAK,EAAE,OAAO;aACX,MAAM,CAAC,SAAS,CAAC;aACjB,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,EAAE,CAC7B,CAAC,GAAG,MAAM,CAAC;aACR,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,MAAM,CAAC;aACtC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAC/C;KACJ,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/adapters/git/index.ts"],"names":[],"mappings":"AAsBA,MAAM,UAAU,GAAG,MAAM,CAAC;AAE1B,6FAA6F;AAC7F,MAAM,QAAQ,GAAG,CACf,CAAc,EACd,UAA6B,CAAC,CAAC,CAAC,EAGL,EAAE;IAC7B,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC;QACf,KAAK,QAAQ;YACX,OAAO,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC;gBAC/B,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE;gBACrC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,cAAc,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;QACnE,KAAK,WAAW;YACd,OAAO,EAAE,GAAG,EAAE,sBAAsB,EAAE,CAAC;QACzC,KAAK,cAAc;YACjB,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC;QAC5B,KAAK,WAAW,CAAC;QACjB,KAAK,WAAW;YACd,OAAO,EAAE,GAAG,EAAE,uBAAuB,CAAC,CAAC,IAAI,GAAG,EAAE,CAAC;IACrD,CAAC;AACH,CAAC,CAAC;AAEF,2EAA2E;AAC3E,MAAM,KAAK,GACT,CAAC,GAAe,EAAE,GAAqC,EAAE,EAAE,CAC3D,CAAC,GAAiB,EAAE,IAAuB,EAAE,OAA2B,EAAE,EAAE,CAC1E,QAAQ,CACN,GAAG,CAAC,GAAG,CAAC;IACN,IAAI,EAAE,KAAK;IACX,IAAI,EAAE,CAAC,IAAI,EAAE,sBAAsB,EAAE,GAAG,IAAI,CAAC;IAC7C,GAAG;IACH,SAAS,EAAE,UAAU;IACrB,GAAG;IACH,cAAc,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI;CACjC,CAAC,EACF,OAAO,CACR,CAAC;AAEN,mCAAmC;AACnC,MAAM,QAAQ,GAAG,CAAC,IAAY,EAAY,EAAE;IAC1C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC/B,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,CAAC;AACxD,CAAC,CAAC;AAEF,qDAAqD;AACrD,MAAM,GAAG,GAAG,CAAC,KAAwB,EAAE,CAAS,EAAU,EAAE,CAC1D,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AAEjC;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,GAAW,EAAiC,EAAE,CAC3E,GAAG;KACA,KAAK,CAAC,MAAM,CAAC;KACb,KAAK,CAAC,CAAC,CAAC;KACR,MAAM,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;IACtB,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAChC,MAAM,EAAE,GAAG,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;IACnC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC7B,OAAO,KAAK;SACT,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;SACvC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;AACtD,CAAC,EAAE,IAAI,GAAG,EAAoB,CAAC,CAAC;AAEpC,oCAAoC;AACpC,MAAM,UAAU,UAAU,CACxB,GAAe,EACf,GAAqC;IAErC,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IAC5B,OAAO;QACL,SAAS,EAAE,CAAC,KAAK,EAAE,IAAI,EAAa,EAAE;YACpC,MAAM,GAAG,GAAG,GAAG,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,oBAAoB,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;YACxE,IAAI,KAAK,IAAI,GAAG;gBAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,CAAC;YAC3D,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;YAClD,IAAI,MAAM,KAAK,EAAE;gBAAE,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,CAAC;YACpD,sFAAsF;YACtF,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;YACtC,IAAI,KAAK,IAAI,IAAI;gBAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC;YAC7D,IAAI,CAAC,IAAI,CAAC,OAAO;gBAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC;YACtD,MAAM,MAAM,GAAG,GAAG,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,gBAAgB,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;YACpE,IAAI,KAAK,IAAI,MAAM;gBAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC;YACjE,OAAO,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE;gBAC7B,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE;gBACrC,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC;QAClC,CAAC;QACD,KAAK,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,QAAQ,EAAE,QAAQ,CAAC;KAChE,CAAC;AACJ,CAAC;AAID,sGAAsG;AACtG,MAAM,MAAM,GAAG,CACb,GAAQ,EACR,GAAiB,EACjB,IAAkB,EACwC,EAAE;IAC5D,MAAM,CAAC,GAAG,GAAG,CAAC,GAAG,EAAE,CAAC,SAAS,EAAE,aAAa,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;IACnE,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;AAC3D,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,aAAa,GAAG,CAAC,gBAAgB,CAAU,CAAC;AAElD,iHAAiH;AACjH,MAAM,aAAa,GAAG;IACpB,KAAK;IACL,WAAW;IACX,GAAG,aAAa;IAChB,wBAAwB;IACxB,aAAa;IACb,YAAY;IACZ,IAAI;IACJ,GAAG;CACK,CAAC;AAEX,mGAAmG;AACnG,MAAM,UAAU,GAAG,CACjB,GAAQ,EACR,QAAsB,EACtB,QAAsB,EAGK,EAAE;IAC7B,MAAM,CAAC,GAAG,GAAG,CAAC,QAAQ,EAAE;QACtB,KAAK;QACL,GAAG,aAAa;QAChB,oBAAoB;QACpB,IAAI;QACJ,QAAQ;KACT,CAAC,CAAC;IACH,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC;IACzB,MAAM,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACvC,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;IACrD,IAAI,KAAK,CAAC,MAAM,KAAK,EAAE;QAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC7D,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC7C,OAAO,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;AACtE,CAAC,CAAC;AAEF,yEAAyE;AACzE,SAAS,OAAO,CACd,GAAQ,EACR,QAAsB,EACtB,QAAsB;IAEtB,MAAM,OAAO,GAAG,GAAG,CAAC,QAAQ,EAAE,CAAC,WAAW,EAAE,yBAAyB,CAAC,CAAC,CAAC;IACxE,IAAI,KAAK,IAAI,OAAO;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC;IACnE,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,MAAM;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;IAC9D,MAAM,CAAC,GAAG,UAAU,CAAC,GAAG,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC9C,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC;IACvD,MAAM,KAAK,GAAG,GAAG,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC;IAC3C,IAAI,KAAK,IAAI,KAAK;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,CAAC;IAC/D,MAAM,OAAO,GAAG,QAAQ,CACtB,GAAG,EACH,QAAQ,EACR,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAC5B,cAAc,CAAC,KAAK,CAAC,GAAG,CAAC,CAC1B,CAAC;IACF,OAAO,KAAK,IAAI,OAAO;QACrB,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE;QACvC,CAAC,CAAC;YACE,IAAI,EAAE,MAAM;YACZ,QAAQ,EAAE,CAAC,CAAC,EAAE;YACd,eAAe,EAAE,CAAC,CAAC,OAAO;YAC1B,KAAK,EAAE,OAAO,CAAC,KAAK;SACrB,CAAC;AACR,CAAC;AAMD,MAAM,QAAQ,GAAG,CACf,CAAS,EAC+C,EAAE,CAAC,KAAK,IAAI,CAAC,CAAC;AACxE,MAAM,SAAS,GAAG,CAChB,CAAS,EACgD,EAAE,CAAC,OAAO,IAAI,CAAC,CAAC;AAE3E;;;;GAIG;AACH,MAAM,SAAS,GAAG,CAChB,GAAQ,EACR,GAAiB,EACjB,QAAuE,EACvE,MAAc,EACN,EAAE;IACV,IAAI,QAAQ,CAAC,EAAE,KAAK,IAAI;QAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC;IAChE,IAAI,MAAM,KAAK,QAAQ,CAAC,EAAE,CAAC,MAAM;QAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IACpE,MAAM,CAAC,GAAG,GAAG,CAAC,GAAG,EAAE;QACjB,UAAU;QACV,GAAG,aAAa;QAChB,MAAM;QACN,IAAI;QACJ,QAAQ,CAAC,IAAI;KACd,CAAC,CAAC;IACH,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC;IAC9C,MAAM,OAAO,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,CAAC;IAC9D,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,MAAM,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;IAC7E,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;AACtE,CAAC,CAAC;AAEF,wGAAwG;AACxG,SAAS,QAAQ,CACf,GAAQ,EACR,GAAiB,EACjB,QAAuE,EACvE,MAAqC;IAErC,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,CACzE,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC,CACxC,CAAC;IACF,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACtC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC;IACrD,OAAO;QACL,KAAK,EAAE,OAAO;aACX,MAAM,CAAC,SAAS,CAAC;aACjB,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,EAAE,CAC7B,CAAC,GAAG,MAAM,CAAC;aACR,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,MAAM,CAAC;aACtC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAC/C;KACJ,CAAC;AACJ,CAAC"}
@@ -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.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": "^38.2.0",
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:834f128ace92288a compiled from skills/analyze-sibling-paper/SKILL.md.spec.ts -->
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` the arXiv/ar5iv/DOI page and read
30
- abstract → intro → method → claims (as much as is accessible). An abstract hides exactly the overlap
31
- that matters. Try the fetch routes in order — `arxiv.org/abs/<id>` for metadata, `arxiv.org/html/<id>v1`,
32
- `ar5iv.labs.arxiv.org/html/<id>`, then the PDF — and **a 403/404 on one route is not "inaccessible"**;
33
- switch channel before concluding. **Record what you actually read** in the card's frontmatter as
34
- `read: full | abstract | none`, and name in a `**Read:**` line the sections you covered and the
35
- ones you could not — an abstract-only verdict is PROVISIONAL and must say so.
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\` the arXiv/ar5iv/DOI page and read
45
- abstract → intro → method → claims (as much as is accessible). An abstract hides exactly the overlap
46
- that matters. Try the fetch routes in order — \`arxiv.org/abs/<id>\` for metadata, \`arxiv.org/html/<id>v1\`,
47
- \`ar5iv.labs.arxiv.org/html/<id>\`, then the PDF — and **a 403/404 on one route is not "inaccessible"**;
48
- switch channel before concluding. **Record what you actually read** in the card's frontmatter as
49
- \`read: full | abstract | none\`, and name in a \`**Read:**\` line the sections you covered and the
50
- ones you could not — an abstract-only verdict is PROVISIONAL and must say so.
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
- // 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(