@perpscope/percolator-adapter 0.4.0 → 0.6.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.
- package/README.md +11 -0
- package/index.js +4 -0
- package/package.json +1 -1
- package/src/lib/percolator-adapter.js +265 -0
package/README.md
CHANGED
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
Read-only adapter helpers for Solana perps terminals that want PerpScope DTOs without adopting the cockpit UI.
|
|
4
4
|
|
|
5
|
+
```bash
|
|
6
|
+
npm install @perpscope/percolator-adapter
|
|
7
|
+
```
|
|
8
|
+
|
|
5
9
|
```js
|
|
6
10
|
import {
|
|
7
11
|
buildPercolatorCompatibilityReport,
|
|
8
12
|
buildReadOnlyRpcSnapshot,
|
|
9
13
|
buildWatchtowerSignals,
|
|
14
|
+
compareCompatibilityReports,
|
|
10
15
|
normalizeFundingSkewHistory,
|
|
11
16
|
normalizePercolatorSnapshot,
|
|
12
17
|
simulatePriceShock
|
|
@@ -16,6 +21,7 @@ const snapshot = normalizePercolatorSnapshot(decodedPercolatorJson);
|
|
|
16
21
|
const market = snapshot.markets[0];
|
|
17
22
|
const stress = simulatePriceShock(market, -5);
|
|
18
23
|
const compatibility = buildPercolatorCompatibilityReport(decodedPercolatorJson, snapshot);
|
|
24
|
+
const drift = compareCompatibilityReports(previousCompatibility, compatibility);
|
|
19
25
|
const watchtower = buildWatchtowerSignals(market, stress);
|
|
20
26
|
const carryHistory = normalizeFundingSkewHistory(market.history.fundingSkew, market);
|
|
21
27
|
```
|
|
@@ -26,6 +32,8 @@ The package exposes pure read-only helpers:
|
|
|
26
32
|
|
|
27
33
|
- `normalizePercolatorSnapshot()`
|
|
28
34
|
- `buildPercolatorCompatibilityReport()`
|
|
35
|
+
- `exportCompatibilityReport()`
|
|
36
|
+
- `compareCompatibilityReports()`
|
|
29
37
|
- `detectPercolatorInputShape()`
|
|
30
38
|
- `parsePercolatorJson()`
|
|
31
39
|
- `simulatePriceShock()`
|
|
@@ -49,9 +57,12 @@ It does not connect wallets, sign, send, route, place orders, or submit transact
|
|
|
49
57
|
- `recognizedSections`: mapped market, price, engine, account, execution, receipt, history, and provenance sections
|
|
50
58
|
- `missingFields`: useful fields the cockpit can render better when supplied
|
|
51
59
|
- `ignoredFields`: top-level fields or command names preserved as provenance but not mapped yet
|
|
60
|
+
- `aliasSuggestions`: candidate mappings such as `oraclePriceUsd -> price.mark`
|
|
52
61
|
|
|
53
62
|
The full field-level contract is documented in `../../docs/field-compatibility-map.md`, with a machine-readable manifest at `../../examples/field-compatibility-map.json`.
|
|
54
63
|
|
|
64
|
+
`compareCompatibilityReports(previous, current)` returns `perpscope.compatibility-diff` with score delta, status change, new/resolved fields, section drift, and merged alias suggestions.
|
|
65
|
+
|
|
55
66
|
## DTO Example
|
|
56
67
|
|
|
57
68
|
```js
|
package/index.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
export {
|
|
2
2
|
assertReadOnlySnapshot,
|
|
3
3
|
buildPercolatorCompatibilityReport,
|
|
4
|
+
compareCompatibilityReports,
|
|
4
5
|
detectPercolatorInputShape,
|
|
6
|
+
exportCompatibilityReport,
|
|
7
|
+
exportCompatibilityReportFromReport,
|
|
5
8
|
normalizePercolatorCliBundle,
|
|
6
9
|
normalizePercolatorSnapshot,
|
|
10
|
+
PERPSCOPE_ADAPTER_VERSION,
|
|
7
11
|
parsePercolatorJson,
|
|
8
12
|
simulatePriceShock,
|
|
9
13
|
toTerminalMarketDto
|
package/package.json
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { normalizeFundingSkewHistory } from "./funding-history.js";
|
|
2
2
|
|
|
3
|
+
export const PERPSCOPE_ADAPTER_VERSION = "0.6.0";
|
|
4
|
+
|
|
3
5
|
const KEYPAIR_FIELD_PATTERN = /(^|_)(secret|private|keypair|mnemonic|seed|walletPath|wallet)(_|$)/i;
|
|
4
6
|
const HISTORY_COMMAND_KEYS = new Set([
|
|
5
7
|
"fundinghistory",
|
|
@@ -96,6 +98,7 @@ export function buildPercolatorCompatibilityReport(input, normalizedSnapshot) {
|
|
|
96
98
|
detail: field.detail
|
|
97
99
|
}));
|
|
98
100
|
const ignoredFields = ignoredCompatibilityFields(input);
|
|
101
|
+
const aliasSuggestions = buildCompatibilityAliasSuggestions(missingFields, ignoredFields);
|
|
99
102
|
const dangerCount = missingFields.filter((field) => field.severity === "danger").length;
|
|
100
103
|
const warningCount = missingFields.length - dangerCount;
|
|
101
104
|
const recognizedDataCount = recognizedSections.filter((section) => section.id !== "safety").length;
|
|
@@ -117,6 +120,7 @@ export function buildPercolatorCompatibilityReport(input, normalizedSnapshot) {
|
|
|
117
120
|
recognizedSections,
|
|
118
121
|
missingFields,
|
|
119
122
|
ignoredFields,
|
|
123
|
+
aliasSuggestions,
|
|
120
124
|
source: {
|
|
121
125
|
label: source.label || stringOf(scope, ["label", "sourceLabel"], "decoded capture"),
|
|
122
126
|
mode: source.mode || "read-only",
|
|
@@ -131,12 +135,102 @@ export function buildPercolatorCompatibilityReport(input, normalizedSnapshot) {
|
|
|
131
135
|
recognizedCount: recognizedSections.length,
|
|
132
136
|
missingCount: missingFields.length,
|
|
133
137
|
ignoredCount: ignoredFields.length,
|
|
138
|
+
suggestionCount: aliasSuggestions.length,
|
|
134
139
|
marketCount: snapshot.markets.length,
|
|
135
140
|
commandCount: commandSet.length
|
|
136
141
|
}
|
|
137
142
|
};
|
|
138
143
|
}
|
|
139
144
|
|
|
145
|
+
export function compareCompatibilityReports(previousReport, currentReport, options = {}) {
|
|
146
|
+
const previous = normalizeCompatibilityReport(previousReport);
|
|
147
|
+
const current = normalizeCompatibilityReport(currentReport);
|
|
148
|
+
const resolvedMissing = differenceByField(previous.missingFields, current.missingFields);
|
|
149
|
+
const newMissing = differenceByField(current.missingFields, previous.missingFields);
|
|
150
|
+
const resolvedIgnored = differenceByPath(previous.ignoredFields, current.ignoredFields);
|
|
151
|
+
const newIgnored = differenceByPath(current.ignoredFields, previous.ignoredFields);
|
|
152
|
+
const addedSections = differenceById(current.recognizedSections, previous.recognizedSections);
|
|
153
|
+
const removedSections = differenceById(previous.recognizedSections, current.recognizedSections);
|
|
154
|
+
const scoreDelta = Number(current.score || 0) - Number(previous.score || 0);
|
|
155
|
+
const statusChanged = previous.status !== current.status;
|
|
156
|
+
const suggestionSet = mergeAliasSuggestions(
|
|
157
|
+
current.aliasSuggestions || buildCompatibilityAliasSuggestions(current.missingFields, current.ignoredFields),
|
|
158
|
+
buildCompatibilityAliasSuggestions(newMissing, newIgnored)
|
|
159
|
+
);
|
|
160
|
+
const tone = current.status === "rejected" || newMissing.some((field) => field.severity === "danger")
|
|
161
|
+
? "danger"
|
|
162
|
+
: scoreDelta < 0 || newMissing.length || newIgnored.length || removedSections.length
|
|
163
|
+
? "warning"
|
|
164
|
+
: "good";
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
schema: "perpscope.compatibility-diff",
|
|
168
|
+
version: 1,
|
|
169
|
+
package: {
|
|
170
|
+
name: "@perpscope/percolator-adapter",
|
|
171
|
+
version: options.packageVersion || PERPSCOPE_ADAPTER_VERSION
|
|
172
|
+
},
|
|
173
|
+
generatedAt: options.generatedAt || new Date().toISOString(),
|
|
174
|
+
tone,
|
|
175
|
+
scoreDelta,
|
|
176
|
+
statusChanged,
|
|
177
|
+
previous: compatibilityDiffSummary(previous),
|
|
178
|
+
current: compatibilityDiffSummary(current),
|
|
179
|
+
resolvedMissing,
|
|
180
|
+
newMissing,
|
|
181
|
+
resolvedIgnored,
|
|
182
|
+
newIgnored,
|
|
183
|
+
addedSections,
|
|
184
|
+
removedSections,
|
|
185
|
+
aliasSuggestions: suggestionSet,
|
|
186
|
+
summary: {
|
|
187
|
+
resolvedMissingCount: resolvedMissing.length,
|
|
188
|
+
newMissingCount: newMissing.length,
|
|
189
|
+
resolvedIgnoredCount: resolvedIgnored.length,
|
|
190
|
+
newIgnoredCount: newIgnored.length,
|
|
191
|
+
addedSectionCount: addedSections.length,
|
|
192
|
+
removedSectionCount: removedSections.length,
|
|
193
|
+
suggestionCount: suggestionSet.length
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function exportCompatibilityReport(input, normalizedSnapshot, options = {}) {
|
|
199
|
+
const snapshot = normalizedSnapshot || normalizePercolatorSnapshot(input);
|
|
200
|
+
const report = buildPercolatorCompatibilityReport(input, snapshot);
|
|
201
|
+
return exportCompatibilityReportFromReport(report, options);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function exportCompatibilityReportFromReport(report, options = {}) {
|
|
205
|
+
return {
|
|
206
|
+
schema: "perpscope.compatibility-report",
|
|
207
|
+
version: 1,
|
|
208
|
+
package: {
|
|
209
|
+
name: "@perpscope/percolator-adapter",
|
|
210
|
+
version: options.packageVersion || PERPSCOPE_ADAPTER_VERSION
|
|
211
|
+
},
|
|
212
|
+
generatedAt: options.generatedAt || new Date().toISOString(),
|
|
213
|
+
safety: {
|
|
214
|
+
mode: "read-only",
|
|
215
|
+
rejected: report.status === "rejected"
|
|
216
|
+
},
|
|
217
|
+
shape: report.shape,
|
|
218
|
+
status: report.status,
|
|
219
|
+
compatible: report.compatible,
|
|
220
|
+
tone: report.tone,
|
|
221
|
+
score: report.score,
|
|
222
|
+
recognizedSections: report.recognizedSections,
|
|
223
|
+
missingFields: report.missingFields,
|
|
224
|
+
ignoredFields: report.ignoredFields,
|
|
225
|
+
aliasSuggestions: report.aliasSuggestions || buildCompatibilityAliasSuggestions(report.missingFields, report.ignoredFields),
|
|
226
|
+
source: report.source,
|
|
227
|
+
summary: {
|
|
228
|
+
...report.summary,
|
|
229
|
+
suggestionCount: report.summary?.suggestionCount ?? (report.aliasSuggestions || []).length
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
140
234
|
const COMPATIBILITY_SECTION_SPECS = [
|
|
141
235
|
{
|
|
142
236
|
id: "safety",
|
|
@@ -409,6 +503,177 @@ function ignoredCompatibilityFields(input) {
|
|
|
409
503
|
return ignored.slice(0, 8);
|
|
410
504
|
}
|
|
411
505
|
|
|
506
|
+
const COMPATIBILITY_ALIAS_HINTS = [
|
|
507
|
+
{
|
|
508
|
+
field: "market.slab",
|
|
509
|
+
tokens: ["slab", "marketaddress", "marketpubkey", "pubkey", "address"],
|
|
510
|
+
reason: "Use this as the market slab anchor."
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
field: "market.program",
|
|
514
|
+
tokens: ["program", "programid", "owner"],
|
|
515
|
+
reason: "Use this as the decoded account owner/program id."
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
field: "price.mark",
|
|
519
|
+
tokens: ["mark", "markprice", "oracleprice", "oraclepriceusd", "priceusd", "mid", "midprice"],
|
|
520
|
+
reason: "Map this into the mark/oracle price lane."
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
field: "price.publishAgeSec",
|
|
524
|
+
tokens: ["age", "agesec", "agesecs", "publishage", "publishagesec", "oracleage", "staleness"],
|
|
525
|
+
reason: "Map this into oracle freshness."
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
field: "crank.ageSlots",
|
|
529
|
+
tokens: ["crank", "crankage", "ageslots", "lastcrank", "lastmarketslot"],
|
|
530
|
+
reason: "Map this into crank freshness."
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
field: "funding.bpsPerHour",
|
|
534
|
+
tokens: ["funding", "fundingrate", "fundingbps", "premium", "carry"],
|
|
535
|
+
reason: "Map this into the carry-rate card."
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
field: "marketStructure.openInterestUsd",
|
|
539
|
+
tokens: ["openinterest", "oi", "oiusd", "longoi", "shortoi"],
|
|
540
|
+
reason: "Map this into OI, skew, and stress pressure."
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
field: "account.positionNotionalUsd",
|
|
544
|
+
tokens: ["notional", "positionnotional", "position", "basesize", "positionbase"],
|
|
545
|
+
reason: "Map this into account runway."
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
field: "execution.bestBid/bestAsk",
|
|
549
|
+
tokens: ["bestbid", "bestask", "bid", "ask", "quote", "book", "orderbook"],
|
|
550
|
+
reason: "Map this into spread and execution quality."
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
field: "execution.receipts",
|
|
554
|
+
tokens: ["receipt", "receipts", "fills", "fill", "execution"],
|
|
555
|
+
reason: "Map this into the fill receipt timeline."
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
field: "history.fundingSkew",
|
|
559
|
+
tokens: ["history", "fundinghistory", "skewhistory", "fundingskew", "rows"],
|
|
560
|
+
reason: "Map this into the carry-history sparklines."
|
|
561
|
+
}
|
|
562
|
+
];
|
|
563
|
+
|
|
564
|
+
function buildCompatibilityAliasSuggestions(missingFields = [], ignoredFields = []) {
|
|
565
|
+
const suggestions = [];
|
|
566
|
+
for (const missing of missingFields || []) {
|
|
567
|
+
const candidates = (ignoredFields || [])
|
|
568
|
+
.map((ignored) => aliasSuggestionFor(missing, ignored))
|
|
569
|
+
.filter(Boolean)
|
|
570
|
+
.sort((a, b) => b.score - a.score)
|
|
571
|
+
.slice(0, 2);
|
|
572
|
+
if (candidates.length) {
|
|
573
|
+
suggestions.push(...candidates);
|
|
574
|
+
} else if (missing.severity === "danger") {
|
|
575
|
+
suggestions.push({
|
|
576
|
+
field: missing.field,
|
|
577
|
+
candidatePath: "",
|
|
578
|
+
confidence: "needs-input",
|
|
579
|
+
score: 0,
|
|
580
|
+
action: "add-field",
|
|
581
|
+
reason: `${missing.label} is still required for a trusted terminal view.`
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return dedupeAliasSuggestions(suggestions).slice(0, 8);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function aliasSuggestionFor(missing, ignored) {
|
|
589
|
+
const hint = COMPATIBILITY_ALIAS_HINTS.find((entry) => entry.field === missing.field);
|
|
590
|
+
if (!hint || !ignored) return null;
|
|
591
|
+
const haystack = normalizeKey(`${ignored.path || ""} ${ignored.label || ""}`);
|
|
592
|
+
const score = hint.tokens.reduce((best, token) => {
|
|
593
|
+
const normalizedToken = normalizeKey(token);
|
|
594
|
+
if (haystack === normalizedToken) return Math.max(best, 100);
|
|
595
|
+
if (haystack.includes(normalizedToken) || normalizedToken.includes(haystack)) return Math.max(best, 86);
|
|
596
|
+
return best;
|
|
597
|
+
}, 0);
|
|
598
|
+
if (!score) return null;
|
|
599
|
+
return {
|
|
600
|
+
field: missing.field,
|
|
601
|
+
candidatePath: ignored.path,
|
|
602
|
+
confidence: score >= 90 ? "high" : "medium",
|
|
603
|
+
score,
|
|
604
|
+
action: "map-alias",
|
|
605
|
+
reason: hint.reason
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function dedupeAliasSuggestions(suggestions) {
|
|
610
|
+
const seen = new Set();
|
|
611
|
+
return suggestions.filter((suggestion) => {
|
|
612
|
+
const key = `${suggestion.field}:${suggestion.candidatePath}:${suggestion.action}`;
|
|
613
|
+
if (seen.has(key)) return false;
|
|
614
|
+
seen.add(key);
|
|
615
|
+
return true;
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function mergeAliasSuggestions(...groups) {
|
|
620
|
+
return dedupeAliasSuggestions(groups.flat().filter(Boolean))
|
|
621
|
+
.sort((a, b) => (b.score || 0) - (a.score || 0))
|
|
622
|
+
.slice(0, 8);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function normalizeCompatibilityReport(report = {}) {
|
|
626
|
+
return {
|
|
627
|
+
shape: report.shape || "unknown",
|
|
628
|
+
status: report.status || "unknown",
|
|
629
|
+
compatible: Boolean(report.compatible),
|
|
630
|
+
tone: report.tone || "neutral",
|
|
631
|
+
score: Number(report.score || 0),
|
|
632
|
+
recognizedSections: report.recognizedSections || [],
|
|
633
|
+
missingFields: report.missingFields || [],
|
|
634
|
+
ignoredFields: report.ignoredFields || [],
|
|
635
|
+
aliasSuggestions: report.aliasSuggestions || [],
|
|
636
|
+
source: report.source || {},
|
|
637
|
+
summary: report.summary || {}
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function compatibilityDiffSummary(report) {
|
|
642
|
+
return {
|
|
643
|
+
shape: report.shape,
|
|
644
|
+
status: report.status,
|
|
645
|
+
compatible: report.compatible,
|
|
646
|
+
score: report.score,
|
|
647
|
+
source: {
|
|
648
|
+
label: report.source?.label || "decoded capture",
|
|
649
|
+
cluster: report.source?.cluster || "unknown",
|
|
650
|
+
commandCount: Array.isArray(report.source?.commandSet) ? report.source.commandSet.length : 0,
|
|
651
|
+
marketCount: report.source?.marketCount || report.summary?.marketCount || 0
|
|
652
|
+
},
|
|
653
|
+
summary: {
|
|
654
|
+
recognizedCount: report.summary?.recognizedCount || report.recognizedSections.length,
|
|
655
|
+
missingCount: report.summary?.missingCount || report.missingFields.length,
|
|
656
|
+
ignoredCount: report.summary?.ignoredCount || report.ignoredFields.length,
|
|
657
|
+
suggestionCount: report.summary?.suggestionCount || report.aliasSuggestions.length
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function differenceByField(left = [], right = []) {
|
|
663
|
+
const rightFields = new Set((right || []).map((entry) => entry.field));
|
|
664
|
+
return (left || []).filter((entry) => !rightFields.has(entry.field));
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function differenceByPath(left = [], right = []) {
|
|
668
|
+
const rightPaths = new Set((right || []).map((entry) => entry.path));
|
|
669
|
+
return (left || []).filter((entry) => !rightPaths.has(entry.path));
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function differenceById(left = [], right = []) {
|
|
673
|
+
const rightIds = new Set((right || []).map((entry) => entry.id));
|
|
674
|
+
return (left || []).filter((entry) => !rightIds.has(entry.id));
|
|
675
|
+
}
|
|
676
|
+
|
|
412
677
|
function assertReadOnlyCompatibilityCapture(value, path = "capture") {
|
|
413
678
|
if (!value || typeof value !== "object") return;
|
|
414
679
|
for (const [key, child] of Object.entries(value)) {
|