@perpscope/percolator-adapter 0.5.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 +7 -0
- package/index.js +1 -0
- package/package.json +1 -1
- package/src/lib/percolator-adapter.js +233 -2
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
buildPercolatorCompatibilityReport,
|
|
12
12
|
buildReadOnlyRpcSnapshot,
|
|
13
13
|
buildWatchtowerSignals,
|
|
14
|
+
compareCompatibilityReports,
|
|
14
15
|
normalizeFundingSkewHistory,
|
|
15
16
|
normalizePercolatorSnapshot,
|
|
16
17
|
simulatePriceShock
|
|
@@ -20,6 +21,7 @@ const snapshot = normalizePercolatorSnapshot(decodedPercolatorJson);
|
|
|
20
21
|
const market = snapshot.markets[0];
|
|
21
22
|
const stress = simulatePriceShock(market, -5);
|
|
22
23
|
const compatibility = buildPercolatorCompatibilityReport(decodedPercolatorJson, snapshot);
|
|
24
|
+
const drift = compareCompatibilityReports(previousCompatibility, compatibility);
|
|
23
25
|
const watchtower = buildWatchtowerSignals(market, stress);
|
|
24
26
|
const carryHistory = normalizeFundingSkewHistory(market.history.fundingSkew, market);
|
|
25
27
|
```
|
|
@@ -30,6 +32,8 @@ The package exposes pure read-only helpers:
|
|
|
30
32
|
|
|
31
33
|
- `normalizePercolatorSnapshot()`
|
|
32
34
|
- `buildPercolatorCompatibilityReport()`
|
|
35
|
+
- `exportCompatibilityReport()`
|
|
36
|
+
- `compareCompatibilityReports()`
|
|
33
37
|
- `detectPercolatorInputShape()`
|
|
34
38
|
- `parsePercolatorJson()`
|
|
35
39
|
- `simulatePriceShock()`
|
|
@@ -53,9 +57,12 @@ It does not connect wallets, sign, send, route, place orders, or submit transact
|
|
|
53
57
|
- `recognizedSections`: mapped market, price, engine, account, execution, receipt, history, and provenance sections
|
|
54
58
|
- `missingFields`: useful fields the cockpit can render better when supplied
|
|
55
59
|
- `ignoredFields`: top-level fields or command names preserved as provenance but not mapped yet
|
|
60
|
+
- `aliasSuggestions`: candidate mappings such as `oraclePriceUsd -> price.mark`
|
|
56
61
|
|
|
57
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`.
|
|
58
63
|
|
|
64
|
+
`compareCompatibilityReports(previous, current)` returns `perpscope.compatibility-diff` with score delta, status change, new/resolved fields, section drift, and merged alias suggestions.
|
|
65
|
+
|
|
59
66
|
## DTO Example
|
|
60
67
|
|
|
61
68
|
```js
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { normalizeFundingSkewHistory } from "./funding-history.js";
|
|
2
2
|
|
|
3
|
-
export const PERPSCOPE_ADAPTER_VERSION = "0.
|
|
3
|
+
export const PERPSCOPE_ADAPTER_VERSION = "0.6.0";
|
|
4
4
|
|
|
5
5
|
const KEYPAIR_FIELD_PATTERN = /(^|_)(secret|private|keypair|mnemonic|seed|walletPath|wallet)(_|$)/i;
|
|
6
6
|
const HISTORY_COMMAND_KEYS = new Set([
|
|
@@ -98,6 +98,7 @@ export function buildPercolatorCompatibilityReport(input, normalizedSnapshot) {
|
|
|
98
98
|
detail: field.detail
|
|
99
99
|
}));
|
|
100
100
|
const ignoredFields = ignoredCompatibilityFields(input);
|
|
101
|
+
const aliasSuggestions = buildCompatibilityAliasSuggestions(missingFields, ignoredFields);
|
|
101
102
|
const dangerCount = missingFields.filter((field) => field.severity === "danger").length;
|
|
102
103
|
const warningCount = missingFields.length - dangerCount;
|
|
103
104
|
const recognizedDataCount = recognizedSections.filter((section) => section.id !== "safety").length;
|
|
@@ -119,6 +120,7 @@ export function buildPercolatorCompatibilityReport(input, normalizedSnapshot) {
|
|
|
119
120
|
recognizedSections,
|
|
120
121
|
missingFields,
|
|
121
122
|
ignoredFields,
|
|
123
|
+
aliasSuggestions,
|
|
122
124
|
source: {
|
|
123
125
|
label: source.label || stringOf(scope, ["label", "sourceLabel"], "decoded capture"),
|
|
124
126
|
mode: source.mode || "read-only",
|
|
@@ -133,12 +135,66 @@ export function buildPercolatorCompatibilityReport(input, normalizedSnapshot) {
|
|
|
133
135
|
recognizedCount: recognizedSections.length,
|
|
134
136
|
missingCount: missingFields.length,
|
|
135
137
|
ignoredCount: ignoredFields.length,
|
|
138
|
+
suggestionCount: aliasSuggestions.length,
|
|
136
139
|
marketCount: snapshot.markets.length,
|
|
137
140
|
commandCount: commandSet.length
|
|
138
141
|
}
|
|
139
142
|
};
|
|
140
143
|
}
|
|
141
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
|
+
|
|
142
198
|
export function exportCompatibilityReport(input, normalizedSnapshot, options = {}) {
|
|
143
199
|
const snapshot = normalizedSnapshot || normalizePercolatorSnapshot(input);
|
|
144
200
|
const report = buildPercolatorCompatibilityReport(input, snapshot);
|
|
@@ -166,8 +222,12 @@ export function exportCompatibilityReportFromReport(report, options = {}) {
|
|
|
166
222
|
recognizedSections: report.recognizedSections,
|
|
167
223
|
missingFields: report.missingFields,
|
|
168
224
|
ignoredFields: report.ignoredFields,
|
|
225
|
+
aliasSuggestions: report.aliasSuggestions || buildCompatibilityAliasSuggestions(report.missingFields, report.ignoredFields),
|
|
169
226
|
source: report.source,
|
|
170
|
-
summary:
|
|
227
|
+
summary: {
|
|
228
|
+
...report.summary,
|
|
229
|
+
suggestionCount: report.summary?.suggestionCount ?? (report.aliasSuggestions || []).length
|
|
230
|
+
}
|
|
171
231
|
};
|
|
172
232
|
}
|
|
173
233
|
|
|
@@ -443,6 +503,177 @@ function ignoredCompatibilityFields(input) {
|
|
|
443
503
|
return ignored.slice(0, 8);
|
|
444
504
|
}
|
|
445
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
|
+
|
|
446
677
|
function assertReadOnlyCompatibilityCapture(value, path = "capture") {
|
|
447
678
|
if (!value || typeof value !== "object") return;
|
|
448
679
|
for (const [key, child] of Object.entries(value)) {
|