@kernhq/module-tracker 0.10.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kernhq/module-tracker",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "Kern tracker module: projects, work item types, custom fields, workflows, issues, KQL, cycles, views, reports, intake, time tracking (server + client skeleton)",
5
5
  "homepage": "https://kernaio.com",
6
6
  "license": "AGPL-3.0-only",
@@ -0,0 +1,78 @@
1
+ /**
2
+ * The ordering invariants manual ranking depends on.
3
+ *
4
+ * There was no test for this file, which is how a loop that hangs the tab survived: dragging a
5
+ * sixth card to the top of one list was enough to reach it, and every other test passed.
6
+ */
7
+ import { describe, expect, it } from 'vitest'
8
+ import { initialRank, isValidRank, rankBetween, rankSequence } from './rank.js'
9
+
10
+ describe('rankBetween', () => {
11
+ it('lands strictly between its neighbours', () => {
12
+ const a = initialRank()
13
+ const c = rankBetween(a, null)
14
+ const b = rankBetween(a, c)
15
+ expect(a < b).toBe(true)
16
+ expect(b < c).toBe(true)
17
+ })
18
+
19
+ it('opens either end', () => {
20
+ const first = initialRank()
21
+ expect(rankBetween(null, first) < first).toBe(true)
22
+ expect(rankBetween(first, null) > first).toBe(true)
23
+ })
24
+
25
+ it('refuses neighbours that are the wrong way round', () => {
26
+ const a = initialRank()
27
+ const b = rankBetween(a, null)
28
+ expect(() => rankBetween(b, a)).toThrow()
29
+ })
30
+
31
+ it('keeps finding room when the same gap is split over and over', () => {
32
+ let lo = initialRank()
33
+ const hi = rankBetween(lo, null)
34
+ const seen = new Set<string>([lo, hi])
35
+ for (let i = 0; i < 200; i++) {
36
+ const mid = rankBetween(lo, hi)
37
+ expect(lo < mid, `${lo} < ${mid} failed at split ${i}`).toBe(true)
38
+ expect(mid < hi, `${mid} < ${hi} failed at split ${i}`).toBe(true)
39
+ expect(seen.has(mid), `collision at split ${i}`).toBe(false)
40
+ seen.add(mid)
41
+ lo = mid
42
+ }
43
+ })
44
+ })
45
+
46
+ describe('dragging to the top, over and over', () => {
47
+ it('keeps finding room instead of looping until the tab dies', () => {
48
+ // The regression: an absent lower bound was treated as "below digit 0", so the fifth insertion
49
+ // at the top minted '0' and the sixth searched below it for ever.
50
+ let first = initialRank()
51
+ const seen = new Set<string>([first])
52
+ for (let i = 0; i < 200; i++) {
53
+ const next = rankBetween(null, first)
54
+ expect(next < first, `${next} < ${first} failed at insertion ${i}`).toBe(true)
55
+ expect(isValidRank(next), `${next} is not canonical at insertion ${i}`).toBe(true)
56
+ expect(seen.has(next), `collision at insertion ${i}`).toBe(false)
57
+ seen.add(next)
58
+ first = next
59
+ }
60
+ })
61
+
62
+ it('never mints a key with no room below it', () => {
63
+ let first = initialRank()
64
+ for (let i = 0; i < 50; i++) {
65
+ first = rankBetween(null, first)
66
+ expect(first.endsWith('0'), `${first} ends in the lowest digit`).toBe(false)
67
+ }
68
+ })
69
+ })
70
+
71
+ describe('rankSequence', () => {
72
+ it('produces keys that already sort into the order they were asked for', () => {
73
+ const keys = rankSequence(25)
74
+ expect(keys).toHaveLength(25)
75
+ expect([...keys].sort()).toEqual(keys)
76
+ expect(new Set(keys).size).toBe(25)
77
+ })
78
+ })
@@ -45,10 +45,19 @@ export function rankBetween(before: string | null, after: string | null): string
45
45
  for (let i = 0; ; i++) {
46
46
  const da = i < lo.length ? indexOfDigit(lo[i] as string) : -1
47
47
  const db = free ? BASE : i < hi.length ? indexOfDigit(hi[i] as string) : 0
48
- if (db - da > 1) return prefix + DIGITS[Math.floor((da + db) / 2)]
49
- const kept = da >= 0 ? da : 0
50
- prefix += DIGITS[kept]
51
- if (!free && kept < db) free = true
48
+ /**
49
+ * An absent lower bound is digit 0, not "below 0".
50
+ *
51
+ * Digit 0 can never be the last digit of a key: nothing sorts strictly between `''` and `'0'`,
52
+ * so a key ending in 0 leaves no room to insert in front of it. Treating the absent bound as -1
53
+ * made the midpoint of (nothing, '2') come out as '0', and the next insertion at the front then
54
+ * searched for a key below '0' for ever, appending a digit each pass until the tab ran out of
55
+ * memory. Five cards dragged to the top of one list was enough to reach it.
56
+ */
57
+ const low = da >= 0 ? da : 0
58
+ if (db - low > 1) return prefix + DIGITS[Math.floor((low + db) / 2)]
59
+ prefix += DIGITS[low]
60
+ if (!free && low < db) free = true
52
61
  }
53
62
  }
54
63