jev-agent-tools 0.1.4 → 0.2.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 (131) hide show
  1. package/CHANGELOG.md +70 -1
  2. package/CONTRIBUTING.md +40 -0
  3. package/README.md +42 -9
  4. package/SECURITY.md +27 -0
  5. package/dist/adapters/analysis-context.js +75 -0
  6. package/dist/adapters/ask-files.js +189 -0
  7. package/dist/adapters/ask-proof.js +144 -0
  8. package/dist/adapters/ask-syntax.js +385 -0
  9. package/dist/adapters/canonical-path.js +17 -0
  10. package/dist/adapters/command.js +181 -0
  11. package/dist/adapters/docs.js +172 -0
  12. package/dist/adapters/exec.js +207 -0
  13. package/dist/adapters/files.js +293 -0
  14. package/dist/adapters/find.js +122 -0
  15. package/dist/adapters/git-base.js +26 -0
  16. package/dist/adapters/git-inventory.js +71 -0
  17. package/dist/adapters/git.js +439 -0
  18. package/dist/adapters/locate-file.js +159 -0
  19. package/dist/adapters/output-lines.js +46 -0
  20. package/dist/adapters/private-storage.js +98 -0
  21. package/dist/adapters/risk-callers.js +426 -0
  22. package/dist/adapters/runner-version.js +78 -0
  23. package/dist/adapters/shell.js +76 -0
  24. package/dist/adapters/syntax.js +187 -0
  25. package/dist/adapters/test-inventory.js +131 -0
  26. package/dist/adapters/usage.js +20 -0
  27. package/dist/adapters/utf8.js +47 -0
  28. package/dist/configuration.js +257 -0
  29. package/dist/constants.js +119 -0
  30. package/dist/core/ask-closure.js +282 -0
  31. package/dist/core/ask-proof.js +1 -0
  32. package/dist/core/ask-references.js +194 -0
  33. package/dist/core/asks.js +436 -0
  34. package/dist/core/batches.js +65 -0
  35. package/dist/core/command-output.js +224 -0
  36. package/dist/core/diff.js +178 -0
  37. package/dist/core/docs.js +302 -0
  38. package/dist/core/find.js +108 -0
  39. package/dist/core/git.js +1 -0
  40. package/dist/core/imports.js +550 -0
  41. package/dist/core/integrity.js +45 -0
  42. package/dist/core/lexical.js +132 -0
  43. package/dist/core/locate.js +169 -0
  44. package/dist/core/output.js +120 -0
  45. package/dist/core/pointer.js +29 -0
  46. package/dist/core/risk-callers.js +851 -0
  47. package/dist/core/runner-version.js +45 -0
  48. package/dist/core/sections.js +230 -0
  49. package/dist/core/state.js +44 -0
  50. package/dist/core/syntax.js +1 -0
  51. package/dist/core/test-commands.js +334 -0
  52. package/dist/core/test-coverage.js +74 -0
  53. package/dist/core/test-discovery.js +1382 -0
  54. package/dist/core/test-evidence.js +527 -0
  55. package/dist/core/test-state.js +81 -0
  56. package/dist/core/truncate.js +12 -0
  57. package/dist/core/units.js +349 -0
  58. package/dist/describe.js +23 -0
  59. package/dist/guide.js +33 -0
  60. package/dist/host.js +24 -0
  61. package/dist/jev/client.js +434 -0
  62. package/dist/jev/pool.js +54 -0
  63. package/dist/jev/types.js +1 -0
  64. package/dist/mcp/main.js +124 -0
  65. package/dist/mcp/protocol.js +187 -0
  66. package/dist/mcp/tools.js +116 -0
  67. package/dist/presets/docs.js +62 -0
  68. package/dist/presets/risk.js +179 -0
  69. package/dist/presets/spec.js +81 -0
  70. package/dist/presets/witnesses.js +249 -0
  71. package/dist/render.js +42 -0
  72. package/dist/result.js +3 -0
  73. package/dist/runtime.js +1 -0
  74. package/dist/session.js +147 -0
  75. package/dist/texts/ask-files.js +1 -0
  76. package/dist/texts/ask.js +2 -0
  77. package/dist/texts/check-diff.js +17 -0
  78. package/dist/texts/configuration.js +1 -0
  79. package/dist/texts/find.js +14 -0
  80. package/dist/texts/guide.js +16 -0
  81. package/dist/texts/locate.js +10 -0
  82. package/dist/texts/select-tests.js +2 -0
  83. package/dist/tools/ask-files.js +217 -0
  84. package/dist/tools/ask-schema.js +70 -0
  85. package/dist/tools/ask.js +686 -0
  86. package/dist/tools/check-diff.js +402 -0
  87. package/dist/tools/docs-check.js +299 -0
  88. package/dist/tools/find.js +389 -0
  89. package/dist/tools/locate.js +303 -0
  90. package/dist/tools/select-tests.js +567 -0
  91. package/dist/tools/spec-check.js +166 -0
  92. package/docs/adr/0001-strict-typescript-pure-core-offline-tests.md +31 -0
  93. package/docs/adr/0002-one-http-protocol-across-hosts.md +17 -0
  94. package/docs/adr/0003-explicit-scope-conservative-automation.md +19 -0
  95. package/docs/adr/0004-compiled-typed-intents.md +19 -0
  96. package/docs/adr/0005-evidence-construction-before-judgment.md +19 -0
  97. package/docs/adr/0006-visible-uncertainty-constrained-controls.md +21 -0
  98. package/docs/adr/0007-bounded-evidence-visible-limits.md +21 -0
  99. package/docs/adr/0008-static-test-discovery-conservative-plans.md +19 -0
  100. package/docs/adr/0009-session-cache-requested-model-identity.md +17 -0
  101. package/docs/adr/0010-mcp-server-thin-host.md +23 -0
  102. package/docs/agent-instructions.md +91 -0
  103. package/docs/design.md +3 -3
  104. package/docs/mcp.md +231 -0
  105. package/package.json +19 -4
  106. package/server.json +57 -0
  107. package/src/adapters/canonical-path.ts +18 -0
  108. package/src/adapters/command.ts +7 -4
  109. package/src/adapters/exec.ts +226 -0
  110. package/src/adapters/private-storage.ts +143 -0
  111. package/src/adapters/risk-callers.ts +4 -2
  112. package/src/adapters/shell.ts +97 -0
  113. package/src/configuration.ts +39 -12
  114. package/src/constants.ts +11 -0
  115. package/src/core/command-output.ts +17 -1
  116. package/src/host.ts +11 -0
  117. package/src/jev/client.ts +12 -0
  118. package/src/jev/types.ts +6 -0
  119. package/src/mcp/main.ts +135 -0
  120. package/src/mcp/protocol.ts +282 -0
  121. package/src/mcp/tools.ts +166 -0
  122. package/src/session.ts +59 -0
  123. package/src/setup.ts +13 -5
  124. package/src/tools/ask-files.ts +5 -7
  125. package/src/tools/ask.ts +26 -22
  126. package/src/tools/check-diff.ts +8 -5
  127. package/src/tools/docs-check.ts +1 -0
  128. package/src/tools/find.ts +5 -2
  129. package/src/tools/locate.ts +5 -8
  130. package/src/tools/select-tests.ts +7 -4
  131. package/src/tools/spec-check.ts +1 -0
@@ -0,0 +1,132 @@
1
+ /** A lexical pass excludes comments and keeps literals separate from identifiers. */
2
+ export function tokenize(text, python = false) {
3
+ const tokens = [];
4
+ let i = 0;
5
+ let line = 1;
6
+ while (i < text.length) {
7
+ const c = text[i] ?? "";
8
+ if (/\s/.test(c)) {
9
+ if (c === "\n")
10
+ line++;
11
+ i++;
12
+ continue;
13
+ }
14
+ if (python ? c === "#" : text.slice(i, i + 2) === "//") {
15
+ while (i < text.length && text[i] !== "\n")
16
+ i++;
17
+ continue;
18
+ }
19
+ if (!python && text.slice(i, i + 2) === "/*") {
20
+ i += 2;
21
+ while (i < text.length && text.slice(i, i + 2) !== "*/") {
22
+ if (text[i] === "\n")
23
+ line++;
24
+ i++;
25
+ }
26
+ i += 2;
27
+ continue;
28
+ }
29
+ const start = i;
30
+ const firstLine = line;
31
+ const previous = tokens[tokens.length - 1];
32
+ // A slash can begin a regex only where an expression can begin, not after
33
+ // an operand (where it is division). Keep the entire literal opaque.
34
+ if (!python &&
35
+ c === "/" &&
36
+ (!previous ||
37
+ (!previous.string &&
38
+ (/^[([{,:;=!?&|+*%~^<>-]$/.test(previous.value) ||
39
+ [
40
+ "return",
41
+ "throw",
42
+ "case",
43
+ "delete",
44
+ "void",
45
+ "typeof",
46
+ "yield",
47
+ "await",
48
+ "in",
49
+ "of",
50
+ ].includes(previous.value))))) {
51
+ let end = i + 1;
52
+ let characterClass = false;
53
+ while (end < text.length && text[end] !== "\n" && text[end] !== "\r") {
54
+ const next = text[end];
55
+ if (next === "\\") {
56
+ end += 2;
57
+ continue;
58
+ }
59
+ if (next === "[")
60
+ characterClass = true;
61
+ else if (next === "]")
62
+ characterClass = false;
63
+ else if (next === "/" && !characterClass)
64
+ break;
65
+ end++;
66
+ }
67
+ if (text[end] === "/") {
68
+ end++;
69
+ while (/[a-z]/i.test(text[end] ?? ""))
70
+ end++;
71
+ tokens.push({
72
+ value: text.slice(i, end),
73
+ line,
74
+ offset: start,
75
+ string: false,
76
+ });
77
+ i = end;
78
+ continue;
79
+ }
80
+ }
81
+ if (c === "'" || c === '"' || c === "`") {
82
+ const quote = python && text.slice(i, i + 3) === c.repeat(3) ? c.repeat(3) : c;
83
+ i += quote.length;
84
+ let value = "";
85
+ let dynamic = false;
86
+ while (i < text.length && text.slice(i, i + quote.length) !== quote) {
87
+ if (text[i] === "\n")
88
+ line++;
89
+ if (c === "`" && text.slice(i, i + 2) === "${")
90
+ dynamic = true;
91
+ if (text[i] === "\\") {
92
+ i++;
93
+ const escaped = text[i++] ?? "";
94
+ value +=
95
+ escaped === "n"
96
+ ? "\n"
97
+ : escaped === "t"
98
+ ? "\t"
99
+ : escaped === "r"
100
+ ? "\r"
101
+ : escaped;
102
+ }
103
+ else
104
+ value += text[i++];
105
+ }
106
+ const closed = text.slice(i, i + quote.length) === quote;
107
+ if (closed)
108
+ i += quote.length;
109
+ tokens.push({
110
+ value,
111
+ line: firstLine,
112
+ offset: start,
113
+ string: closed && !dynamic,
114
+ });
115
+ }
116
+ else if (/[\w$]/.test(c)) {
117
+ while (i < text.length && /[\w$]/.test(text[i] ?? ""))
118
+ i++;
119
+ tokens.push({
120
+ value: text.slice(start, i),
121
+ line,
122
+ offset: start,
123
+ string: false,
124
+ });
125
+ }
126
+ else {
127
+ i++;
128
+ tokens.push({ value: c, line, offset: start, string: false });
129
+ }
130
+ }
131
+ return tokens;
132
+ }
@@ -0,0 +1,169 @@
1
+ import { CHOICE_MAX_OPTIONS, LOCATE_VERDICT_MIN, STATE_MAX_CHARS, } from "../constants.js";
2
+ import { truncate } from "./truncate.js";
3
+ export function sectionState(path, goal, sections, outline = false) {
4
+ return {
5
+ path,
6
+ goal,
7
+ sections: sections.map((section) => ({
8
+ id: section.id,
9
+ lines: `${section.start}-${section.end}`,
10
+ text: outline ? section.label : section.text,
11
+ })),
12
+ };
13
+ }
14
+ export function sectionStateFits(path, goal, sections, outline = false) {
15
+ return (sections.length < CHOICE_MAX_OPTIONS &&
16
+ JSON.stringify(sectionState(path, goal, sections, outline)).length <=
17
+ STATE_MAX_CHARS);
18
+ }
19
+ /** Every planned block has at least two sections and fits its full-text refinement budget. */
20
+ export function sectionOutline(path, goal, sections) {
21
+ const base = JSON.stringify(sectionState(path, goal, [])).length;
22
+ const costs = new Map(sections.map((section) => [
23
+ section,
24
+ JSON.stringify({
25
+ id: section.id,
26
+ lines: `${section.start}-${section.end}`,
27
+ text: "",
28
+ }).length +
29
+ (section.serializedTextChars ?? JSON.stringify(section.text).length) -
30
+ 2,
31
+ ]));
32
+ const fits = (items) => items.length < CHOICE_MAX_OPTIONS &&
33
+ base + items.reduce((sum, item) => sum + (costs.get(item) ?? 0) + 1, 0) <=
34
+ STATE_MAX_CHARS;
35
+ const groups = [];
36
+ let current = [], currentSize = base;
37
+ for (const section of sections) {
38
+ const length = (costs.get(section) ?? 0) + 1;
39
+ if (base + length > STATE_MAX_CHARS)
40
+ return [];
41
+ const owner = section.label.split(".")[0], previousOwner = current.at(-1)?.label.split(".")[0];
42
+ if (current.length &&
43
+ (currentSize + length > STATE_MAX_CHARS ||
44
+ current.length + 1 >= CHOICE_MAX_OPTIONS ||
45
+ (current.length >= 2 &&
46
+ section.label.includes(".") &&
47
+ owner !== previousOwner))) {
48
+ groups.push(current);
49
+ current = [];
50
+ currentSize = base;
51
+ }
52
+ current.push(section);
53
+ currentSize += length;
54
+ }
55
+ if (current.length)
56
+ groups.push(current);
57
+ for (let index = 0; index < groups.length; index++) {
58
+ const group = groups[index];
59
+ if (!group || group.length >= 2)
60
+ continue;
61
+ const previous = groups[index - 1], next = groups[index + 1];
62
+ if (previous && fits([...previous, ...group])) {
63
+ previous.push(...group);
64
+ groups.splice(index, 1);
65
+ index--;
66
+ continue;
67
+ }
68
+ if (next && fits([...group, ...next])) {
69
+ next.unshift(...group);
70
+ groups.splice(index, 1);
71
+ index--;
72
+ continue;
73
+ }
74
+ if (previous && previous.length > 2) {
75
+ const moved = previous.at(-1);
76
+ if (moved && fits([moved, ...group])) {
77
+ previous.pop();
78
+ group.unshift(moved);
79
+ continue;
80
+ }
81
+ }
82
+ if (next && next.length > 2) {
83
+ const moved = next[0];
84
+ if (moved && fits([...group, moved])) {
85
+ next.shift();
86
+ group.push(moved);
87
+ continue;
88
+ }
89
+ }
90
+ return [];
91
+ }
92
+ return groups.map((group, index) => ({
93
+ id: `B${index + 1}`,
94
+ start: group[0]?.start ?? 0,
95
+ end: group.at(-1)?.end ?? 0,
96
+ label: group.map((s) => s.label).join(" / "),
97
+ text: group.map((s) => s.text).join("\n"),
98
+ }));
99
+ }
100
+ /** JSON-escaped budget, allocated equally then redistributed to larger blocks. */
101
+ export function outlineEvidence(path, goal, blocks) {
102
+ const overhead = JSON.stringify(sectionState(path, goal, blocks.map((block) => ({ ...block, text: "" })))).length;
103
+ let remaining = Math.max(0, STATE_MAX_CHARS - overhead);
104
+ const allocations = blocks.map(() => 0);
105
+ let active = blocks.map((_, index) => index);
106
+ while (remaining > 0 && active.length) {
107
+ const share = Math.max(1, Math.floor(remaining / active.length));
108
+ const next = [];
109
+ for (const index of active) {
110
+ const block = blocks[index];
111
+ if (!block)
112
+ continue;
113
+ const need = JSON.stringify(block.text).length - 2 - (allocations[index] ?? 0);
114
+ const granted = Math.min(need, share, remaining);
115
+ allocations[index] = (allocations[index] ?? 0) + granted;
116
+ remaining -= granted;
117
+ if (need > granted)
118
+ next.push(index);
119
+ }
120
+ active = next;
121
+ }
122
+ return blocks.map((block, index) => {
123
+ const budget = allocations[index] ?? 0;
124
+ if (JSON.stringify(block.text).length - 2 <= budget)
125
+ return { ...block };
126
+ const excerpt = (chars) => {
127
+ const part = Math.floor(chars / 3);
128
+ const slice = (start) => {
129
+ if (start > 0 && /[\uDC00-\uDFFF]/.test(block.text[start] ?? ""))
130
+ start++;
131
+ return truncate(block.text.slice(start), part);
132
+ };
133
+ return `${slice(0)}\n…\n${slice(Math.max(0, Math.floor(block.text.length / 2) - Math.floor(part / 2)))}\n…\n${slice(Math.max(0, block.text.length - part))}`;
134
+ };
135
+ let low = 0, high = Math.min(block.text.length, budget), text = "";
136
+ while (low <= high) {
137
+ const middle = Math.floor((low + high) / 2), candidate = excerpt(middle);
138
+ if (JSON.stringify(candidate).length - 2 <= budget) {
139
+ text = candidate;
140
+ low = middle + 1;
141
+ }
142
+ else
143
+ high = middle - 1;
144
+ }
145
+ return { ...block, text };
146
+ });
147
+ }
148
+ export function locateDisplay(path, sections, ranked, forceUnsure) {
149
+ const head = ranked[0];
150
+ if (!head)
151
+ return undefined;
152
+ const display = (id, p) => {
153
+ const section = sections.find((s) => s.id === id);
154
+ return {
155
+ label: section ? `${path}:${section.start}-${section.end}` : "none",
156
+ value: { head: section?.label ?? "no section fits", p },
157
+ };
158
+ };
159
+ const band = forceUnsure || head.p < LOCATE_VERDICT_MIN
160
+ ? "unsure"
161
+ : "verdict";
162
+ return {
163
+ primary: display(head.id, head.p),
164
+ band,
165
+ candidates: band === "unsure"
166
+ ? ranked.slice(1, 2).map((s) => display(s.id, s.p))
167
+ : undefined,
168
+ };
169
+ }
@@ -0,0 +1,120 @@
1
+ import { BAND_BOOL_GRAY_A, BAND_BOOL_YES_MIN, BAND_CHOICE_VERDICT_MIN, CANNOT_TELL_MIN, LIMIT_DISPLAY_MAX_PATHS, } from "../constants.js";
2
+ export function askBand(value, input) {
3
+ const answer = input.answer;
4
+ if (answer?.type === "bool")
5
+ return value.p <= BAND_BOOL_GRAY_A || value.p >= BAND_BOOL_YES_MIN
6
+ ? "verdict"
7
+ : "unsure";
8
+ if (answer && (answer.probabilities.cannot_tell ?? 0) >= CANNOT_TELL_MIN)
9
+ return "abstain";
10
+ return value.p >= BAND_CHOICE_VERDICT_MIN ? "verdict" : "unsure";
11
+ }
12
+ function displayValue(input) {
13
+ if (input.value)
14
+ return input.value;
15
+ const answer = input.answer;
16
+ if (!answer)
17
+ throw new Error("An answer requires a typed value or a Jev answer");
18
+ if (answer.type === "bool")
19
+ return { head: answer.p >= 0.5 ? "yes" : "no (not shown)", p: answer.p };
20
+ const p = input.verify
21
+ ? Math.max(answer.probabilities.holds ?? 0, answer.probabilities.contradicted ?? 0, (answer.probabilities.not_addressed ?? 0) +
22
+ (answer.probabilities.cannot_tell ?? 0))
23
+ : Math.max(...Object.values(answer.probabilities));
24
+ return { head: answer.type === "choice" ? answer.choice : answer.score, p };
25
+ }
26
+ export function buildEnvelope(input) {
27
+ const lines = [];
28
+ if (input.budget?.kind === "session" && input.yield.calls === 0)
29
+ return {
30
+ lines: [
31
+ { type: "refusal", message: input.budget.message },
32
+ { type: "yield", value: input.yield },
33
+ ],
34
+ };
35
+ if (input.state)
36
+ lines.push({ type: "state", ...input.state });
37
+ const failed = (input.controls ?? [])
38
+ .map((control) => control.fact)
39
+ .join("; ");
40
+ for (const item of input.answers ?? []) {
41
+ const value = displayValue(item);
42
+ const initial = item.band ?? (input.policy ?? askBand)(value, item);
43
+ const band = initial === "verdict" && failed ? "unsure" : initial;
44
+ const reason = band === "abstain"
45
+ ? (item.reason ??
46
+ item.missing ??
47
+ "proof missing; add decisive evidence to paths or command")
48
+ : failed && initial === "verdict"
49
+ ? failed
50
+ : item.reason;
51
+ lines.push({
52
+ type: "answer",
53
+ label: item.label,
54
+ value,
55
+ band,
56
+ uncalibrated: item.uncalibrated ?? false,
57
+ ...(reason ? { reason } : {}),
58
+ ...(item.orderDependent !== undefined
59
+ ? { orderDependent: item.orderDependent }
60
+ : {}),
61
+ ...(item.candidates ? { candidates: item.candidates } : {}),
62
+ });
63
+ }
64
+ for (const item of input.unjudged ?? [])
65
+ lines.push({ type: "unjudged", ...item });
66
+ for (const line of input.lines ?? [])
67
+ lines.push(line.type === "entry" && line.band === "verdict" && failed
68
+ ? { ...line, band: "unsure" }
69
+ : line);
70
+ if (input.refusal)
71
+ lines.push({ type: "refusal", message: input.refusal });
72
+ if (input.budget?.kind === "session")
73
+ lines.push({ type: "refusal", message: input.budget.message });
74
+ for (const fact of input.controls ?? [])
75
+ lines.push({ type: "fact", ...fact });
76
+ const groups = new Map();
77
+ for (const limit of input.limitations ?? []) {
78
+ if (!limit.cause ||
79
+ !limit.path ||
80
+ input.limitationPriorityPaths === undefined) {
81
+ lines.push({ type: "fact", fact: limit.fact, next: limit.next });
82
+ continue;
83
+ }
84
+ const priority = input.limitationPriorityPaths?.includes(limit.path) ?? false;
85
+ const key = JSON.stringify([priority, limit.cause, limit.next]);
86
+ const group = groups.get(key) ?? {
87
+ type: "limit-group",
88
+ cause: limit.cause,
89
+ count: 0,
90
+ priority,
91
+ examples: [],
92
+ remaining: 0,
93
+ next: limit.next,
94
+ };
95
+ group.count += limit.count ?? 1;
96
+ if (priority || group.examples.length < LIMIT_DISPLAY_MAX_PATHS)
97
+ group.examples.push(`${limit.path}${limit.dependency ? ` (${limit.dependency})` : ""}`);
98
+ else
99
+ group.remaining++;
100
+ groups.set(key, group);
101
+ }
102
+ lines.push(...[...groups.values()].sort((a, b) => Number(b.priority) - Number(a.priority)));
103
+ if (input.budget?.kind === "max_calls")
104
+ lines.push({
105
+ type: "fact",
106
+ fact: input.budget.message,
107
+ next: "raise max_calls",
108
+ });
109
+ if (input.unchecked?.length)
110
+ lines.push({ type: "unchecked", items: input.unchecked });
111
+ const gray = lines.reduce((count, line) => count + Number(line.type === "answer" && line.band === "unsure"), 0);
112
+ if (gray > 0)
113
+ lines.push({
114
+ type: "fact",
115
+ fact: `${gray}/${input.answers?.length ?? 0} answers unsure`,
116
+ next: "read the decisive passage or add its file to paths",
117
+ });
118
+ lines.push({ type: "yield", value: input.yield });
119
+ return { lines };
120
+ }
@@ -0,0 +1,29 @@
1
+ import { ORDER_DISAGREE_MIN, ORDER_REVERSE_BELOW } from "../constants.js";
2
+ export function pointerQuestion({ instructions, noneLabel, candidates, reverse = false, }) {
3
+ const ordered = reverse ? [...candidates].reverse() : candidates;
4
+ return {
5
+ type: "choice",
6
+ instructions,
7
+ criteria: Object.fromEntries([
8
+ ...ordered.map((candidate) => [candidate.id, candidate.label]),
9
+ ["none", noneLabel],
10
+ ]),
11
+ };
12
+ }
13
+ export function needsReverse(probabilities, candidateCount) {
14
+ return (candidateCount > 1 &&
15
+ Math.max(...Object.values(probabilities)) < ORDER_REVERSE_BELOW);
16
+ }
17
+ export function readPointer(first, second) {
18
+ const ranked = Object.entries(first)
19
+ .map(([id, p]) => ({ id, p: second ? (p + (second[id] ?? 0)) / 2 : p }))
20
+ .sort((a, b) => b.p - a.p);
21
+ const head = ranked[0];
22
+ return {
23
+ ranked,
24
+ orderDependent: !!(second &&
25
+ head &&
26
+ Math.abs((first[head.id] ?? 0) - (second[head.id] ?? 0)) >
27
+ ORDER_DISAGREE_MIN),
28
+ };
29
+ }