@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 +1 -1
- package/src/client/rank.test.ts +78 -0
- package/src/client/rank.ts +13 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kernhq/module-tracker",
|
|
3
|
-
"version": "0.10.
|
|
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
|
+
})
|
package/src/client/rank.ts
CHANGED
|
@@ -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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|