@juangadm/pre-post 1.0.0 → 1.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 (103) hide show
  1. package/README.md +99 -20
  2. package/dist/baseline.d.ts +119 -0
  3. package/dist/baseline.d.ts.map +1 -0
  4. package/dist/baseline.js +316 -0
  5. package/dist/baseline.js.map +1 -0
  6. package/dist/bin/cli.js +80 -41
  7. package/dist/bin/cli.js.map +1 -1
  8. package/dist/browser.d.ts +31 -3
  9. package/dist/browser.d.ts.map +1 -1
  10. package/dist/browser.js +239 -97
  11. package/dist/browser.js.map +1 -1
  12. package/dist/commands/compare.d.ts.map +1 -1
  13. package/dist/commands/compare.js +21 -3
  14. package/dist/commands/compare.js.map +1 -1
  15. package/dist/commands/detect.d.ts +5 -0
  16. package/dist/commands/detect.d.ts.map +1 -1
  17. package/dist/commands/detect.js +4 -1
  18. package/dist/commands/detect.js.map +1 -1
  19. package/dist/commands/doctor.d.ts +32 -0
  20. package/dist/commands/doctor.d.ts.map +1 -1
  21. package/dist/commands/doctor.js +90 -9
  22. package/dist/commands/doctor.js.map +1 -1
  23. package/dist/commands/login.js +1 -1
  24. package/dist/commands/pr.d.ts +4 -0
  25. package/dist/commands/pr.d.ts.map +1 -1
  26. package/dist/commands/pr.js +150 -50
  27. package/dist/commands/pr.js.map +1 -1
  28. package/dist/commands/prune.js +1 -1
  29. package/dist/commands/prune.js.map +1 -1
  30. package/dist/comparison.d.ts +104 -0
  31. package/dist/comparison.d.ts.map +1 -0
  32. package/dist/comparison.js +236 -0
  33. package/dist/comparison.js.map +1 -0
  34. package/dist/config.d.ts +12 -2
  35. package/dist/config.d.ts.map +1 -1
  36. package/dist/config.js +25 -3
  37. package/dist/config.js.map +1 -1
  38. package/dist/deployments.d.ts +71 -0
  39. package/dist/deployments.d.ts.map +1 -0
  40. package/dist/deployments.js +204 -0
  41. package/dist/deployments.js.map +1 -0
  42. package/dist/diff-pool.d.ts.map +1 -1
  43. package/dist/diff-pool.js +5 -2
  44. package/dist/diff-pool.js.map +1 -1
  45. package/dist/diff-worker.d.ts +3 -0
  46. package/dist/diff-worker.d.ts.map +1 -1
  47. package/dist/diff-worker.js +1 -0
  48. package/dist/diff-worker.js.map +1 -1
  49. package/dist/diff.d.ts +10 -0
  50. package/dist/diff.d.ts.map +1 -1
  51. package/dist/diff.js +117 -7
  52. package/dist/diff.js.map +1 -1
  53. package/dist/doctor.d.ts +42 -1
  54. package/dist/doctor.d.ts.map +1 -1
  55. package/dist/doctor.js +73 -7
  56. package/dist/doctor.js.map +1 -1
  57. package/dist/git.d.ts +41 -3
  58. package/dist/git.d.ts.map +1 -1
  59. package/dist/git.js +103 -7
  60. package/dist/git.js.map +1 -1
  61. package/dist/github.d.ts +92 -2
  62. package/dist/github.d.ts.map +1 -1
  63. package/dist/github.js +191 -20
  64. package/dist/github.js.map +1 -1
  65. package/dist/landing.d.ts +48 -0
  66. package/dist/landing.d.ts.map +1 -0
  67. package/dist/landing.js +93 -0
  68. package/dist/landing.js.map +1 -0
  69. package/dist/pkg.d.ts +9 -0
  70. package/dist/pkg.d.ts.map +1 -1
  71. package/dist/pkg.js +14 -0
  72. package/dist/pkg.js.map +1 -1
  73. package/dist/report.d.ts.map +1 -1
  74. package/dist/report.js +49 -26
  75. package/dist/report.js.map +1 -1
  76. package/dist/routes.d.ts +27 -0
  77. package/dist/routes.d.ts.map +1 -1
  78. package/dist/routes.js +98 -23
  79. package/dist/routes.js.map +1 -1
  80. package/dist/run.d.ts +88 -8
  81. package/dist/run.d.ts.map +1 -1
  82. package/dist/run.js +214 -26
  83. package/dist/run.js.map +1 -1
  84. package/dist/sameness.d.ts +91 -0
  85. package/dist/sameness.d.ts.map +1 -0
  86. package/dist/sameness.js +157 -0
  87. package/dist/sameness.js.map +1 -0
  88. package/dist/sessions.d.ts.map +1 -1
  89. package/dist/sessions.js +7 -1
  90. package/dist/sessions.js.map +1 -1
  91. package/dist/shift.d.ts +67 -0
  92. package/dist/shift.d.ts.map +1 -0
  93. package/dist/shift.js +184 -0
  94. package/dist/shift.js.map +1 -0
  95. package/dist/types.d.ts +67 -5
  96. package/dist/types.d.ts.map +1 -1
  97. package/dist/types.js.map +1 -1
  98. package/dist/url.d.ts +12 -0
  99. package/dist/url.d.ts.map +1 -0
  100. package/dist/url.js +30 -0
  101. package/dist/url.js.map +1 -0
  102. package/package.json +1 -1
  103. package/skill/SKILL.md +40 -7
package/dist/run.js CHANGED
@@ -3,36 +3,50 @@
3
3
  * Every route/viewport pair runs concurrently; the browser's page pool bounds it.
4
4
  */
5
5
  import fs from 'fs';
6
+ import { createHash } from 'crypto';
6
7
  import path from 'path';
7
8
  import { captureScreenshot } from './browser.js';
9
+ import { checkLanding, signInHint } from './landing.js';
10
+ import { differentSitesHint, looksLikeDifferentSites, textOverlap, titleOverlap } from './sameness.js';
8
11
  import { HttpStatusError, NavigationError } from './errors.js';
9
12
  import { DiffPool } from './diff-pool.js';
10
13
  import { authHint } from './doctor.js';
14
+ import { hostOf } from './url.js';
11
15
  export function routeSlug(route) {
12
16
  const slug = route.replace(/^\/+|\/+$/g, '').replace(/[^\w.-]+/g, '-').replace(/^-+|-+$/g, '');
13
17
  return slug || 'home';
14
18
  }
15
- export function joinUrl(base, route) {
16
- return base.replace(/\/+$/, '') + (route.startsWith('/') ? route : `/${route}`);
17
- }
18
- /** Add a scheme to a bare URL (http for loopback hosts, https otherwise) and drop trailing slashes. */
19
- export function normalizeUrl(url) {
20
- const withScheme = /^(https?|file):\/\//i.test(url)
21
- ? url
22
- : /^(localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(:|\/|$)/i.test(url) ? `http://${url}` : `https://${url}`;
23
- return withScheme.replace(/\/+$/, '');
24
- }
25
- export function hostOf(url) {
26
- try {
27
- return new URL(url).host;
28
- }
29
- catch {
30
- return url;
31
- }
19
+ /**
20
+ * Did this capture change?
21
+ *
22
+ * Two arms, because they cover different regimes and neither covers both:
23
+ *
24
+ * - Painted area, in CSS pixels² — the rule for pages. Measured against the
25
+ * fixture ladder, every deliberate change covers 242 CSS px² or more while
26
+ * every no-op covers exactly 0, so the floor sits in a wide empty band. CSS
27
+ * pixels rather than device pixels so it means the same at --scale 1 as at
28
+ * --scale 2; the old device-pixel constant was silently four times stricter
29
+ * at scale 1, and at ~10 CSS px² amounted to a third of one letter.
30
+ * - Share of the canvas — the rule for small images. One pixel of a 20x20 icon
31
+ * is a quarter of a percent of it and obviously a change, but it will never
32
+ * reach an area floor sized for pages.
33
+ *
34
+ * On a full page the ratio arm effectively never fires on its own (a recoloured
35
+ * 16px icon is 0.024% of one), and on a small image the area arm never does.
36
+ * Dropping either one loses real changes, which is the worse error: reporting
37
+ * "no visual changes" on a PR that changed something is the one answer that
38
+ * makes a reviewer stop looking.
39
+ */
40
+ export function isChanged(diff, opts) {
41
+ if (diff.changedPixels === 0)
42
+ return false;
43
+ return diff.changedPixels / (opts.scale * opts.scale) >= opts.minChangedArea
44
+ || diff.changedRatio >= opts.threshold;
32
45
  }
33
- /** The one rule for "did this capture change": a ratio for big pages, an absolute floor for small edits. */
34
- export function isChanged(diff, threshold, minChangedPixels) {
35
- return diff.changedPixels > 0 && (diff.changedRatio >= threshold || diff.changedPixels >= minChangedPixels);
46
+ /** "shifted down 48px" — the direction spelled out, because the sign is not. */
47
+ export function describeShift(px) {
48
+ const rounded = Math.round(Math.abs(px));
49
+ return `shifted ${px < 0 ? 'up' : 'down'} ${rounded}px`;
36
50
  }
37
51
  function describeError(err, side, url) {
38
52
  if (err instanceof HttpStatusError)
@@ -48,6 +62,11 @@ function describeError(err, side, url) {
48
62
  const msg = err?.message || String(err);
49
63
  return `${side}: ${msg.split('\n')[0].slice(0, 160)}`;
50
64
  }
65
+ /** Did this side reach the site, or a wall standing in front of it? */
66
+ function blockedSide(requested, result, side) {
67
+ const landing = checkLanding(requested, result.finalUrl ?? requested, result.title ?? '');
68
+ return landing.blocked ? { side, finalUrl: landing.finalUrl, vercel: Boolean(result.vercel) } : null;
69
+ }
51
70
  async function runTask(task, opts, pool) {
52
71
  const started = Date.now();
53
72
  const base = { route: task.route, resolvedRoute: task.resolvedRoute, viewport: task.viewport, status: 'error' };
@@ -73,12 +92,60 @@ async function runTask(task, opts, pool) {
73
92
  }
74
93
  const before = beforeRes.value;
75
94
  const after = afterRes.value;
95
+ // A sign-in wall answers with 200, so both captures "succeeded" — of a login
96
+ // page. Diffing those and calling the result a visual change is the failure
97
+ // this check exists to prevent, so nothing is compared when either side is
98
+ // walled.
99
+ const blocked = blockedSide(task.beforeUrl, before, 'before') ?? blockedSide(task.afterUrl, after, 'after');
100
+ if (blocked) {
101
+ const label = blocked.side === 'before' ? 'Pre' : 'Post';
102
+ return {
103
+ ...base,
104
+ blocked,
105
+ error: `${label} landed on a sign-in page (${blocked.finalUrl}), so the site was never captured`,
106
+ durationMs: Date.now() - started,
107
+ };
108
+ }
76
109
  if (before.status === 404 && after.status === 404) {
77
110
  return { ...base, error: `404 on both ${task.beforeUrl} and ${task.afterUrl} (wrong route or missing sample?)`, durationMs: Date.now() - started };
78
111
  }
79
112
  const prefix = `${routeSlug(task.route)}-${task.viewport}`;
80
113
  const file = (kind) => path.join(opts.outputDir, `${prefix}-${kind}.png`);
81
114
  const outputs = { before: file('pre'), after: file('post'), diff: file('diff'), cropBefore: file('pre-crop'), cropAfter: file('post-crop') };
115
+ // A route that exists on only one side is not a comparison, and the moment to
116
+ // notice is here — before the diff, not after it in a footnote.
117
+ //
118
+ // The old path diffed anyway. For a page this branch adds, "Pre" was a
119
+ // screenshot of the baseline's 404 and the percentage measured the distance
120
+ // between an error page and a new one: 10.81% for a coloured box on white,
121
+ // 0.05% for a light-grey one, neither a fact about the change. Worse, both
122
+ // numbers were published as a Pre/Post pair, so a reviewer was shown a
123
+ // comparison that never existed and four images where one was true.
124
+ //
125
+ // Nothing can be recovered by trying harder — a page the baseline does not
126
+ // serve has no "before" to find — so the honest move is to keep the side that
127
+ // rendered, name why the other is missing, and skip the diff entirely.
128
+ const absent = absentSide(before, after);
129
+ if (absent) {
130
+ const present = absent === 'before' ? after : before;
131
+ const target = absent === 'before' ? outputs.after : outputs.before;
132
+ fs.writeFileSync(target, Buffer.from(present.image));
133
+ const status = absent === 'before' ? 'added' : 'removed';
134
+ const missingHost = hostOf(absent === 'before' ? task.beforeUrl : task.afterUrl);
135
+ opts.log?.(` ${status === 'added' ? 'new ' : 'removed '} ${task.route} @ ${task.viewport} (no ${absent === 'before' ? 'Pre' : 'Post'}, ${Date.now() - started}ms)`);
136
+ return {
137
+ ...base,
138
+ status,
139
+ files: absent === 'before' ? { after: outputs.after } : { before: outputs.before },
140
+ // Deliberately no changedRatio: there is nothing to take a ratio of.
141
+ // Deliberately no textOverlap either — a 404's wording is evidence about
142
+ // an error page, not about whether the two sides are the same site.
143
+ note: absent === 'before'
144
+ ? `new page — no baseline (${missingHost} returned 404)`
145
+ : `page removed — ${missingHost} returned 404`,
146
+ durationMs: Date.now() - started,
147
+ };
148
+ }
82
149
  // Copy into standalone buffers so they can be transferred to the worker.
83
150
  const diff = await pool.run({
84
151
  before: new Uint8Array(before.image),
@@ -88,22 +155,44 @@ async function runTask(task, opts, pool) {
88
155
  padding: 40 * opts.scale,
89
156
  minCrop: { width: 400 * opts.scale, height: 200 * opts.scale },
90
157
  highlightDownscale: opts.scale >= 2 ? 2 : 1,
158
+ highlight: false,
91
159
  },
92
160
  });
93
161
  const notes = [];
94
- if (before.status === 404 && after.status && after.status < 400)
95
- notes.push('new page (404 on production)');
96
- else if (before.status && before.status >= 400)
162
+ // The 404-on-one-side cases returned above; what is left is a page both sides
163
+ // served with an unhappy status, which is still worth diffing and flagging.
164
+ if (before.status && before.status >= 400)
97
165
  notes.push(`production returned ${before.status}`);
98
166
  if (after.status && after.status >= 400)
99
167
  notes.push(`local returned ${after.status}`);
100
- const changed = isChanged(diff, opts.threshold, opts.minChangedPixels);
101
- opts.log?.(` ${changed ? 'changed ' : 'same '} ${task.route} @ ${task.viewport} (${(diff.changedRatio * 100).toFixed(2)}%, ${Date.now() - started}ms)`);
168
+ const changed = isChanged(diff, opts);
169
+ // Cheap fingerprint of the baseline, kept so the run can notice a host that
170
+ // answers every route with the same page. See softNotFoundWarning.
171
+ const baselineHash = createHash('sha1').update(Buffer.from(before.image)).digest('hex');
172
+ // Recorded on every outcome, judged only across the whole run: one route
173
+ // sharing no words is a rewrite, every route sharing none is the wrong site.
174
+ const overlap = textOverlap(before.text ?? '', after.text ?? '');
175
+ const titles = titleOverlap(before.title ?? '', after.title ?? '');
176
+ // A shift is measured in device pixels; everything a reader sees is in CSS
177
+ // pixels. Whether anything changed besides the move is judged by the same
178
+ // rule as any other capture, against the aligned numbers.
179
+ const shift = diff.shift
180
+ ? {
181
+ px: diff.shift.dy / opts.scale,
182
+ otherChange: isChanged({ changedPixels: diff.shift.alignedChangedPixels, changedRatio: diff.shift.alignedChangedRatio }, opts),
183
+ }
184
+ : undefined;
185
+ const shiftNote = shift ? `${describeShift(shift.px)}, ` : '';
186
+ opts.log?.(` ${changed ? 'changed ' : 'same '} ${task.route} @ ${task.viewport} (${shiftNote}${(diff.changedRatio * 100).toFixed(2)}%, ${Date.now() - started}ms)`);
102
187
  return {
103
188
  ...base,
104
189
  status: changed ? 'changed' : 'unchanged',
190
+ baselineHash,
105
191
  changedRatio: diff.changedRatio,
106
192
  sizeChanged: diff.sizeChanged,
193
+ textOverlap: overlap,
194
+ titleOverlap: titles,
195
+ shift,
107
196
  files: {
108
197
  before: outputs.before,
109
198
  after: outputs.after,
@@ -115,14 +204,113 @@ async function runTask(task, opts, pool) {
115
204
  durationMs: Date.now() - started,
116
205
  };
117
206
  }
207
+ /**
208
+ * Did this run compare the two sites, or something standing in front of them?
209
+ *
210
+ * This lives beside the code that produces the evidence, not in the command
211
+ * that happens to want it. Both checks used to be written out in `pr`, and
212
+ * `compare` — the other caller of this pipeline — therefore had neither, and
213
+ * would print a confident percentage for two unrelated sites. Returning the
214
+ * verdict with the outcomes makes that omission impossible to repeat: a caller
215
+ * cannot get the numbers without also being handed the reason not to trust
216
+ * them.
217
+ *
218
+ * Order matters. A wall is the more specific diagnosis and has the more
219
+ * actionable fix, and a walled run also looks like two different sites — the
220
+ * sign-in page shares no words with the site — so it is answered first.
221
+ */
222
+ /**
223
+ * Which side, if either, does not have this page at all.
224
+ *
225
+ * The signal is the HTTP status, not the page's wording. A 404 status is a fact
226
+ * the server states about the route; the string "404" in the body is just text,
227
+ * and it is present on pages that are merely *about* error codes and absent
228
+ * from every custom or localised not-found page ever shipped. Sniffing for it
229
+ * would mean this tool's own marketing page — which shows a 404 in a mockup —
230
+ * could be mistaken for a missing route.
231
+ *
232
+ * Soft 404s (a not-found view served with status 200, the usual SPA shape) are
233
+ * therefore invisible here by design; they are caught at the run level instead,
234
+ * where identical baselines across unrelated routes give them away.
235
+ */
236
+ function absentSide(before, after) {
237
+ const gone = (c) => c.status === 404;
238
+ const served = (c) => !c.status || c.status < 400;
239
+ if (gone(before) && served(after))
240
+ return 'before';
241
+ if (gone(after) && served(before))
242
+ return 'after';
243
+ return null;
244
+ }
245
+ /**
246
+ * Did the baseline answer unrelated routes with the same page?
247
+ *
248
+ * This is the soft 404 — a not-found view served with status 200, which no
249
+ * status check can see and which a keyword hunt for "404" would catch only in
250
+ * English, only when the page says it, and only when no real page happens to
251
+ * mention it. The durable signal is not the wording but the sameness: a host
252
+ * that returns pixel-identical bytes for several different routes is serving a
253
+ * catch-all, and every comparison against it is measuring the distance from
254
+ * that catch-all rather than from the page.
255
+ *
256
+ * It warns rather than reclassifies. Two routes can legitimately render the
257
+ * same page, and silently turning a real diff into "new page" on a heuristic
258
+ * would hide exactly the change this tool exists to show. The authoritative
259
+ * signal (a 404 status) acts; this one only tells the reader what it sees.
260
+ */
261
+ export function softNotFoundWarning(outcomes, beforeUrl) {
262
+ const routesByHash = new Map();
263
+ for (const o of outcomes) {
264
+ if (!o.baselineHash)
265
+ continue;
266
+ const seen = routesByHash.get(o.baselineHash) ?? new Set();
267
+ seen.add(o.route);
268
+ routesByHash.set(o.baselineHash, seen);
269
+ }
270
+ let worst = null;
271
+ for (const routes of routesByHash.values()) {
272
+ if (routes.size > 1 && (!worst || routes.size > worst.size))
273
+ worst = routes;
274
+ }
275
+ if (!worst)
276
+ return null;
277
+ const shown = [...worst].sort().slice(0, 4).join(', ');
278
+ const more = worst.size > 4 ? `, and ${worst.size - 4} more` : '';
279
+ return `Note: ${hostOf(beforeUrl)} served an identical page for ${worst.size} routes (${shown}${more}). `
280
+ + 'If that is a catch-all or a not-found page, those comparisons are against it, not against the real baseline.';
281
+ }
282
+ export function verdictFor(outcomes, sides) {
283
+ // Every route walled means the run never saw the site. Publishing anything
284
+ // from that, "no visual changes" included, would be a confident lie.
285
+ const walled = outcomes.filter(o => o.blocked);
286
+ if (walled.length > 0 && walled.length === outcomes.length) {
287
+ const { side, vercel } = walled[0].blocked;
288
+ return { kind: 'walled', hint: signInHint(side === 'before' ? sides.before.url : sides.after.url, vercel) };
289
+ }
290
+ if (looksLikeDifferentSites(outcomes)) {
291
+ return { kind: 'different-sites', hint: differentSitesHint(sides.before.url, sides.before.detail, sides.after.url, sides.fix) };
292
+ }
293
+ return null;
294
+ }
118
295
  export async function runTasks(tasks, opts) {
119
296
  fs.mkdirSync(opts.outputDir, { recursive: true });
120
297
  const pool = new DiffPool();
298
+ let outcomes;
121
299
  try {
122
- return await Promise.all(tasks.map(t => runTask(t, opts, pool)));
300
+ outcomes = await Promise.all(tasks.map(t => runTask(t, opts, pool)));
123
301
  }
124
302
  finally {
125
303
  await pool.close();
126
304
  }
305
+ // Falling back to the first task's URLs keeps the verdict available to a
306
+ // caller that never resolved a pair — `compare`, given two URLs by hand.
307
+ const sides = opts.sides ?? {
308
+ before: { url: tasks[0]?.beforeUrl ?? '' },
309
+ after: { url: tasks[0]?.afterUrl ?? '' },
310
+ };
311
+ const softNotFound = softNotFoundWarning(outcomes, sides.before.url);
312
+ if (softNotFound)
313
+ opts.log?.(softNotFound);
314
+ return { outcomes, verdict: verdictFor(outcomes, sides) };
127
315
  }
128
316
  //# sourceMappingURL=run.js.map
package/dist/run.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"run.js","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"run.js","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Are the two sides the same site at all?
3
+ *
4
+ * The backstop this file exists to be was first specified as "a diff near 100%
5
+ * on every route means the two sides are different sites". Measured, that rule
6
+ * is wrong in both directions, and it is worth writing down why so it is not
7
+ * proposed again:
8
+ *
9
+ * | pair | changed |
10
+ * |-------------------------------------------------|---------|
11
+ * | the marketing site vs an unrelated real site | 2.66% desktop / 5.02% mobile |
12
+ * | fixture pricing page vs an unrelated fixture | 0.72% – 7.99% |
13
+ * | the same site with a full theme change (fixture) | 99.14% |
14
+ * | the same site redesigned (fixture rung-7) | 99.60% |
15
+ *
16
+ * Web pages are mostly background, and a pixel diff counts pixels that differ,
17
+ * so two *unrelated* pages agree on most of the canvas and read as a small
18
+ * change — the same 1-8% band as the login-page run that started all of this
19
+ * (4.53% / 1.99%). Meanwhile a legitimate redesign, the PR most worth
20
+ * screenshotting, reads ~100%. A threshold at 100% would have missed every
21
+ * real occurrence and fired on the best ones.
22
+ *
23
+ * What does separate them is the words on the page. A redesign changes how a
24
+ * page looks, not what it says; a different site says something else entirely.
25
+ * Measured over the same pairs, with common English words removed:
26
+ *
27
+ * different sites 0.000, 0.000, 0.000, 0.071
28
+ * same site 0.200 (the weakest fixture), 0.417+, 1.000 for real pages
29
+ *
30
+ * The floor below sits in that empty band.
31
+ */
32
+ /** The distinctive words a page shows a reader. */
33
+ export declare function pageWords(text: string): Set<string>;
34
+ /**
35
+ * A page with almost nothing to say cannot answer this question: an image or a
36
+ * canvas with six words on it scores near zero against anything, including
37
+ * itself one version later. Below this many distinct words, the answer is "do
38
+ * not know" rather than "different".
39
+ */
40
+ export declare const MIN_WORDS = 10;
41
+ /** The floor, from the measurements above: 0.071 was different, 0.200 was not. */
42
+ export declare const SAME_SITE_FLOOR = 0.1;
43
+ /** Overlap of two pages' body text, or null when either is too text-poor to judge. */
44
+ export declare function textOverlap(before: string, after: string): number | null;
45
+ /**
46
+ * Overlap of two pages' titles, or null when either says nothing distinctive.
47
+ *
48
+ * No `MIN_WORDS` here: a title is a handful of words by nature, and it is the
49
+ * one place a site almost always names itself. That makes it the corroborating
50
+ * signal for a run with only one route to go on — a branch that rewrites a
51
+ * page's copy keeps the site's name in the tab, a different site does not.
52
+ */
53
+ export declare function titleOverlap(before: string, after: string): number | null;
54
+ /** What this needs from each capture: which page it was, and how the two sides compared. */
55
+ export interface RouteEvidence {
56
+ route?: string;
57
+ textOverlap?: number | null;
58
+ titleOverlap?: number | null;
59
+ }
60
+ /**
61
+ * Do the captures, taken together, say these are two different sites?
62
+ *
63
+ * Every judgeable page has to disagree, and the evidence has to be about
64
+ * *pages*, not captures. One route shot at three viewports is one page's word
65
+ * list three times over; counting it as three agreeing witnesses is how a run
66
+ * that detected a single route — an explicit `--routes /`, or the `/` fallback
67
+ * when the diff names none — could reject the one thing this function's own
68
+ * rule permits: a branch that rewrote a page.
69
+ *
70
+ * So captures are folded per route, keeping each route's *strongest* showing of
71
+ * shared words, and a run with only one page to go on asks the title as well. A
72
+ * title is where a site names itself, so a rewritten page keeps it and a
73
+ * different site does not. Without a usable title there is no second witness
74
+ * and the run is published, which is the right way to be wrong here: publishing
75
+ * a diff someone can look at beats refusing to on one page's say-so.
76
+ */
77
+ export declare function looksLikeDifferentSites(routes: RouteEvidence[]): boolean;
78
+ /**
79
+ * The one sentence a human needs when the two sides are not the same site.
80
+ *
81
+ * `beforeDetail` is how Pre was chosen ("production deployment for 26aa6cd"),
82
+ * absent when a caller was handed both URLs by hand and has no provenance to
83
+ * report. `fix` is the remedy, which is the caller's to name: telling someone
84
+ * to pass `--before` is right when the tool picked the baseline and wrong in
85
+ * the two-URL mode, where both sides came in as positional arguments and that
86
+ * flag is not how the command is invoked. Advice that does not apply to the
87
+ * mode you are in is the failure this project keeps finding — it is what sent
88
+ * someone to sign in to a macOS system service.
89
+ */
90
+ export declare function differentSitesHint(beforeUrl: string, beforeDetail: string | undefined, afterUrl: string, fix?: string): string;
91
+ //# sourceMappingURL=sameness.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sameness.d.ts","sourceRoot":"","sources":["../src/sameness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAeH,mDAAmD;AACnD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAGnD;AAED;;;;;GAKG;AACH,eAAO,MAAM,SAAS,KAAK,CAAC;AAE5B,kFAAkF;AAClF,eAAO,MAAM,eAAe,MAAM,CAAC;AAgBnC,sFAAsF;AACtF,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAKxE;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAKzE;AAED,4FAA4F;AAC5F,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,OAAO,CAkBxE;AAYD;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,QAAQ,EAAE,MAAM,EAChB,GAAG,GAAE,MAAoC,GACxC,MAAM,CAGR"}
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Are the two sides the same site at all?
3
+ *
4
+ * The backstop this file exists to be was first specified as "a diff near 100%
5
+ * on every route means the two sides are different sites". Measured, that rule
6
+ * is wrong in both directions, and it is worth writing down why so it is not
7
+ * proposed again:
8
+ *
9
+ * | pair | changed |
10
+ * |-------------------------------------------------|---------|
11
+ * | the marketing site vs an unrelated real site | 2.66% desktop / 5.02% mobile |
12
+ * | fixture pricing page vs an unrelated fixture | 0.72% – 7.99% |
13
+ * | the same site with a full theme change (fixture) | 99.14% |
14
+ * | the same site redesigned (fixture rung-7) | 99.60% |
15
+ *
16
+ * Web pages are mostly background, and a pixel diff counts pixels that differ,
17
+ * so two *unrelated* pages agree on most of the canvas and read as a small
18
+ * change — the same 1-8% band as the login-page run that started all of this
19
+ * (4.53% / 1.99%). Meanwhile a legitimate redesign, the PR most worth
20
+ * screenshotting, reads ~100%. A threshold at 100% would have missed every
21
+ * real occurrence and fired on the best ones.
22
+ *
23
+ * What does separate them is the words on the page. A redesign changes how a
24
+ * page looks, not what it says; a different site says something else entirely.
25
+ * Measured over the same pairs, with common English words removed:
26
+ *
27
+ * different sites 0.000, 0.000, 0.000, 0.071
28
+ * same site 0.200 (the weakest fixture), 0.417+, 1.000 for real pages
29
+ *
30
+ * The floor below sits in that empty band.
31
+ */
32
+ /**
33
+ * Words too common to say anything about which site you are on. Without this,
34
+ * two unrelated English pages score 0.18 on shared articles and prepositions
35
+ * alone, which is close enough to the weakest same-site pair (0.20) to make the
36
+ * test unusable.
37
+ */
38
+ const STOP_WORDS = new Set(('the and for you your are with that this from all not our but can has have was were will more than then ' +
39
+ 'when who how why into out off over under new get set use using its about here there they them their ' +
40
+ 'his her she him one two any some each own too very just also only same such per via yes non')
41
+ .split(' '));
42
+ /** The distinctive words a page shows a reader. */
43
+ export function pageWords(text) {
44
+ const found = text.toLowerCase().match(/[a-z0-9']{3,}/g) ?? [];
45
+ return new Set(found.filter(w => !STOP_WORDS.has(w)));
46
+ }
47
+ /**
48
+ * A page with almost nothing to say cannot answer this question: an image or a
49
+ * canvas with six words on it scores near zero against anything, including
50
+ * itself one version later. Below this many distinct words, the answer is "do
51
+ * not know" rather than "different".
52
+ */
53
+ export const MIN_WORDS = 10;
54
+ /** The floor, from the measurements above: 0.071 was different, 0.200 was not. */
55
+ export const SAME_SITE_FLOOR = 0.1;
56
+ /**
57
+ * How much of the smaller vocabulary the larger one also has.
58
+ *
59
+ * Containment rather than Jaccard because the two sides legitimately differ in
60
+ * length — a branch that adds a section should not read as a different site for
61
+ * having more words than the baseline.
62
+ */
63
+ function containment(a, b) {
64
+ const [small, large] = a.size <= b.size ? [a, b] : [b, a];
65
+ let shared = 0;
66
+ for (const word of small)
67
+ if (large.has(word))
68
+ shared++;
69
+ return shared / small.size;
70
+ }
71
+ /** Overlap of two pages' body text, or null when either is too text-poor to judge. */
72
+ export function textOverlap(before, after) {
73
+ const a = pageWords(before);
74
+ const b = pageWords(after);
75
+ if (a.size < MIN_WORDS || b.size < MIN_WORDS)
76
+ return null;
77
+ return containment(a, b);
78
+ }
79
+ /**
80
+ * Overlap of two pages' titles, or null when either says nothing distinctive.
81
+ *
82
+ * No `MIN_WORDS` here: a title is a handful of words by nature, and it is the
83
+ * one place a site almost always names itself. That makes it the corroborating
84
+ * signal for a run with only one route to go on — a branch that rewrites a
85
+ * page's copy keeps the site's name in the tab, a different site does not.
86
+ */
87
+ export function titleOverlap(before, after) {
88
+ const a = pageWords(before);
89
+ const b = pageWords(after);
90
+ if (!a.size || !b.size)
91
+ return null;
92
+ return containment(a, b);
93
+ }
94
+ /**
95
+ * Do the captures, taken together, say these are two different sites?
96
+ *
97
+ * Every judgeable page has to disagree, and the evidence has to be about
98
+ * *pages*, not captures. One route shot at three viewports is one page's word
99
+ * list three times over; counting it as three agreeing witnesses is how a run
100
+ * that detected a single route — an explicit `--routes /`, or the `/` fallback
101
+ * when the diff names none — could reject the one thing this function's own
102
+ * rule permits: a branch that rewrote a page.
103
+ *
104
+ * So captures are folded per route, keeping each route's *strongest* showing of
105
+ * shared words, and a run with only one page to go on asks the title as well. A
106
+ * title is where a site names itself, so a rewritten page keeps it and a
107
+ * different site does not. Without a usable title there is no second witness
108
+ * and the run is published, which is the right way to be wrong here: publishing
109
+ * a diff someone can look at beats refusing to on one page's say-so.
110
+ */
111
+ export function looksLikeDifferentSites(routes) {
112
+ const pages = new Map();
113
+ for (const [i, r] of routes.entries()) {
114
+ const key = r.route ?? `#${i}`;
115
+ const seen = pages.get(key);
116
+ // The best showing across viewports: any viewport that found shared words
117
+ // clears the page, so a single narrow layout cannot condemn it alone.
118
+ pages.set(key, {
119
+ text: strongest(seen?.text, r.textOverlap),
120
+ title: strongest(seen?.title, r.titleOverlap),
121
+ });
122
+ }
123
+ const judged = [...pages.values()].filter(p => p.text !== null);
124
+ if (judged.length === 0 || !judged.every(p => p.text < SAME_SITE_FLOOR))
125
+ return false;
126
+ if (judged.length > 1)
127
+ return true;
128
+ const { title } = judged[0];
129
+ return title !== null && title < SAME_SITE_FLOOR;
130
+ }
131
+ /** The higher of two overlaps, treating "could not judge" as no evidence. */
132
+ function strongest(a, b) {
133
+ if (typeof a !== 'number')
134
+ return typeof b === 'number' ? b : null;
135
+ if (typeof b !== 'number')
136
+ return a;
137
+ return Math.max(a, b);
138
+ }
139
+ /** What to do about it, when the caller says nothing: the `pr` path, which chose Pre itself. */
140
+ const DEFAULT_DIFFERENT_SITES_FIX = "re-run with --before pointing at this site's own deployment";
141
+ /**
142
+ * The one sentence a human needs when the two sides are not the same site.
143
+ *
144
+ * `beforeDetail` is how Pre was chosen ("production deployment for 26aa6cd"),
145
+ * absent when a caller was handed both URLs by hand and has no provenance to
146
+ * report. `fix` is the remedy, which is the caller's to name: telling someone
147
+ * to pass `--before` is right when the tool picked the baseline and wrong in
148
+ * the two-URL mode, where both sides came in as positional arguments and that
149
+ * flag is not how the command is invoked. Advice that does not apply to the
150
+ * mode you are in is the failure this project keeps finding — it is what sent
151
+ * someone to sign in to a macOS system service.
152
+ */
153
+ export function differentSitesHint(beforeUrl, beforeDetail, afterUrl, fix = DEFAULT_DIFFERENT_SITES_FIX) {
154
+ const pre = beforeDetail ? `${beforeUrl} — ${beforeDetail}` : beforeUrl;
155
+ return `Pre (${pre}) and Post (${afterUrl}) share no wording on any route, so they are not the same site: ${fix}.`;
156
+ }
157
+ //# sourceMappingURL=sameness.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sameness.js","sourceRoot":"","sources":["../src/sameness.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEzC,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,KAAK,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAC1D,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;KAC1F,CAAC,CAAC;IACH,OAAO,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,KAAK,CAAC;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE,CAAC,CAAC;CAC1F;AAED,wBAAgB,WAAW,IAAI,MAAM,CAKpC;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,MAAM,CAKrE;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAQ7D;AAED,2EAA2E;AAC3E,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,YAAY,CAAC,SAAS,CAAC,CAkBtE;AAED,MAAM,WAAW,gBAAgB;IAC/B,kCAAkC;IAClC,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,oCAAoC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,4DAA4D;IAC5D,OAAO,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,WAAW,GAAG,SAAS,CAY5E"}
1
+ {"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEzC,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,KAAK,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAC1D,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;KAC1F,CAAC,CAAC;IACH,OAAO,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,KAAK,CAAC;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE,CAAC,CAAC;CAC1F;AAED,wBAAgB,WAAW,IAAI,MAAM,CAKpC;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,MAAM,CAKrE;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAQ7D;AAED,2EAA2E;AAC3E,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,YAAY,CAAC,SAAS,CAAC,CAkBtE;AAED,MAAM,WAAW,gBAAgB;IAC/B,kCAAkC;IAClC,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,oCAAoC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,4DAA4D;IAC5D,OAAO,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,WAAW,GAAG,SAAS,CAkB5E"}
package/dist/sessions.js CHANGED
@@ -61,9 +61,15 @@ export function cookiesForUrls(urls) {
61
61
  export function resolveAuth(input) {
62
62
  const headers = { ...(input.configHeaders || {}), ...(input.headers || {}) };
63
63
  const bypass = process.env.VERCEL_AUTOMATION_BYPASS_SECRET;
64
+ // The bypass header alone is enough, and it is all we send. Asking for the
65
+ // cookie as well (`x-vercel-set-bypass-cookie: true`) makes the host set it
66
+ // through a redirect, which a client that keeps no cookie jar can never
67
+ // satisfy: the reachability probe uses plain `fetch`, so it looped until it
68
+ // threw and every protected preview was reported as unreachable. Measured
69
+ // against a live protected deployment on 2026-09-05 — with the one header,
70
+ // 200 and no redirect; with both, 307 until curl gave up at 50.
64
71
  if (bypass && !headers['x-vercel-protection-bypass']) {
65
72
  headers['x-vercel-protection-bypass'] = bypass;
66
- headers['x-vercel-set-bypass-cookie'] = 'true';
67
73
  }
68
74
  const cookies = [
69
75
  ...cookiesForUrls(input.urls),