@perpscope/percolator-adapter 0.8.0 → 1.0.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 CHANGED
@@ -65,13 +65,22 @@ The full field-level contract is documented in `../../docs/field-compatibility-m
65
65
 
66
66
  `buildCompatibilityRealityCheck(report, { input })` returns `perpscope.reality-check` with provenance, required/useful mapped counts, unknown fields, and alias counts. Use it when a terminal needs to show whether a capture is synthetic, real-backed candidate, or externally submitted.
67
67
 
68
+ `buildCompatibilityDoctor(report, { input })` returns `perpscope.compatibility-doctor` with pass/check status, shape, safety, required/useful mapped fields, unknown fields, alias suggestions, and next actions.
69
+
70
+ `buildCompatibilityBadge(reportOrDoctor)` returns `perpscope.compatibility-badge` with Markdown and JSON-friendly fields for READMEs, PRs, and capture handoffs.
71
+
68
72
  ## CLI
69
73
 
70
74
  ```bash
75
+ perpscope init perpscope.capture.json
71
76
  perpscope compat report capture.json
72
77
  perpscope compat diff previous.json current.json
78
+ perpscope compat doctor capture.json --strict
79
+ perpscope compat badge capture.json --json
73
80
  ```
74
81
 
82
+ Doctor exit codes are CI-ready: `0` means required fields pass, `1` means rejected or required fields missing, and `2` means strict mode found useful-field gaps, unknown fields, or alias suggestions.
83
+
75
84
  Try it locally with:
76
85
 
77
86
  ```bash
@@ -80,6 +89,8 @@ perpscope compat diff ../../examples/fixture-pack-minimal-terminal.json ../../ex
80
89
 
81
90
  For the real-backed candidate path, try `../../examples/fixture-pack-real-sanitized-rpc-shape.json`.
82
91
 
92
+ For a copy-paste starter shape, use `../../examples/capture-template.json`.
93
+
83
94
  ## DTO Example
84
95
 
85
96
  ```js
package/bin/perpscope.mjs CHANGED
@@ -1,41 +1,168 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync } from "node:fs";
2
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import {
4
+ buildCompatibilityBadge,
5
+ buildCompatibilityDoctor,
6
+ buildReadOnlyRpcSnapshot,
4
7
  buildPercolatorCompatibilityReport,
5
8
  compareCompatibilityReports,
9
+ detectPercolatorInputShape,
6
10
  exportCompatibilityReport,
7
11
  normalizePercolatorSnapshot,
8
12
  parsePercolatorJson
9
13
  } from "../index.js";
10
14
 
11
15
  const [, , ...args] = process.argv;
16
+ const CAPTURE_TEMPLATE = {
17
+ label: "My terminal read-only capture",
18
+ cluster: "mainnet-beta",
19
+ market: {
20
+ symbol: "SOL-PERP",
21
+ base: "SOL",
22
+ quote: "USDC",
23
+ slab: "PERCOLAT_SOL_...",
24
+ program: "Perco1ator111111111111111111111111111111111"
25
+ },
26
+ oracle: {
27
+ priceUsd: 181.61,
28
+ ageSecs: 2
29
+ },
30
+ engine: {
31
+ currentSlot: 346892118,
32
+ lastMarketSlot: 346892090,
33
+ fundingRateBpsPerHour: 0.82,
34
+ openInterestUsd: 2430000,
35
+ longOpenInterestUsd: 1320000,
36
+ shortOpenInterestUsd: 1110000,
37
+ insuranceUsd: 148000,
38
+ stressConsumedBps: 118,
39
+ stressLimitBps: 500
40
+ },
41
+ execution: {
42
+ bestBid: 181.52,
43
+ bestAsk: 181.71,
44
+ receipts: [
45
+ {
46
+ label: "latest fill",
47
+ sourceTimestamp: "2026-06-20T13:24:12Z",
48
+ spreadBps: 10.5,
49
+ impactBps: 8.4,
50
+ markout1mBps: 4.2,
51
+ markout5mBps: -1.7,
52
+ routeLatencyMs: 132,
53
+ priorityFeeMicrolamports: 2200
54
+ }
55
+ ]
56
+ },
57
+ account: {
58
+ side: "long",
59
+ positionSize: 420,
60
+ positionNotionalUsd: 76276.2,
61
+ collateralUsd: 8400,
62
+ unrealizedPnlUsd: 3067.2,
63
+ liquidationPrice: 162.94
64
+ },
65
+ history: {
66
+ fundingSkew: [
67
+ {
68
+ sourceTimestamp: "2026-06-20T13:24:00Z",
69
+ slot: 346892086,
70
+ fundingBpsPerHour: 0.82,
71
+ longOpenInterestUsd: 1320000,
72
+ shortOpenInterestUsd: 1110000,
73
+ stressConsumedBps: 118,
74
+ stressLimitBps: 500,
75
+ oracleAgeSec: 2.1
76
+ }
77
+ ]
78
+ }
79
+ };
12
80
 
13
81
  function usage() {
14
82
  return [
15
83
  "Usage:",
84
+ " perpscope init [output.json] [--force]",
16
85
  " perpscope compat report <capture.json>",
17
86
  " perpscope compat diff <previous.json> <current.json>",
87
+ " perpscope compat doctor <capture.json> [--strict|--json]",
88
+ " perpscope compat badge <capture.json> [--json|--markdown]",
18
89
  "",
19
90
  "Read-only only: the adapter rejects wallet, signer, transaction, instruction, order, private key, seed, mnemonic, and API key fields."
20
91
  ].join("\n");
21
92
  }
22
93
 
94
+ function initMessage(path) {
95
+ return [
96
+ `Created ${path}`,
97
+ "",
98
+ "Next:",
99
+ ` perpscope compat doctor ${path}`,
100
+ ` perpscope compat badge ${path}`,
101
+ "",
102
+ "Edit the capture with sanitized read-only decoded state before sharing it."
103
+ ].join("\n");
104
+ }
105
+
106
+ function initCapture(path = "perpscope.capture.json", options = {}) {
107
+ if (existsSync(path) && !options.force) {
108
+ throw new Error(`${path} already exists. Use --force to overwrite.`);
109
+ }
110
+ writeFileSync(path, `${JSON.stringify(CAPTURE_TEMPLATE, null, 2)}\n`);
111
+ return path;
112
+ }
113
+
23
114
  function readCapture(path) {
24
115
  if (!path) throw new Error("Missing capture path.");
25
116
  return parsePercolatorJson(readFileSync(path, "utf8"));
26
117
  }
27
118
 
28
119
  function buildReport(input) {
29
- const snapshot = normalizePercolatorSnapshot(input);
120
+ const snapshot = detectPercolatorInputShape(input) === "read-only-rpc-fetch"
121
+ ? buildReadOnlyRpcSnapshot(input)
122
+ : normalizePercolatorSnapshot(input);
30
123
  return buildPercolatorCompatibilityReport(input, snapshot);
31
124
  }
32
125
 
126
+ function formatDoctor(doctor) {
127
+ const lines = [
128
+ `PerpScope compat doctor: ${doctor.pass ? "PASS" : "CHECK"}`,
129
+ `shape: ${doctor.shape}`,
130
+ `status: ${doctor.status} (${doctor.score}/100)`,
131
+ `safety: ${doctor.safety}`,
132
+ `required: ${doctor.required.label}`,
133
+ `useful: ${doctor.useful.label}`,
134
+ `unknown fields: ${doctor.unknownFields.length}`,
135
+ `alias suggestions: ${doctor.aliasSuggestions.length}`
136
+ ];
137
+ if (doctor.nextActions.length) {
138
+ lines.push("next actions:");
139
+ for (const action of doctor.nextActions) lines.push(`- ${action}`);
140
+ }
141
+ return lines.join("\n");
142
+ }
143
+
144
+ function doctorExitCode(doctor, options = {}) {
145
+ if (doctor.status === "rejected" || doctor.required.mapped < doctor.required.total) return 1;
146
+ if (options.strict && (
147
+ doctor.useful.mapped < doctor.useful.total ||
148
+ doctor.unknownFields.length ||
149
+ doctor.aliasSuggestions.length
150
+ )) return 2;
151
+ return 0;
152
+ }
153
+
33
154
  function main() {
34
155
  const [scope, command, ...rest] = args;
35
156
  if (!scope || scope === "--help" || scope === "-h") {
36
157
  console.log(usage());
37
158
  return;
38
159
  }
160
+ if (scope === "init") {
161
+ const output = command && !command.startsWith("--") ? command : "perpscope.capture.json";
162
+ const flags = [command, ...rest].filter(Boolean);
163
+ console.log(initMessage(initCapture(output, { force: flags.includes("--force") })));
164
+ return;
165
+ }
39
166
  if (scope !== "compat") throw new Error(`Unknown scope: ${scope}`);
40
167
  if (command === "report") {
41
168
  const input = readCapture(rest[0]);
@@ -48,6 +175,19 @@ function main() {
48
175
  console.log(JSON.stringify(compareCompatibilityReports(previous, current), null, 2));
49
176
  return;
50
177
  }
178
+ if (command === "doctor") {
179
+ const input = readCapture(rest[0]);
180
+ const doctor = buildCompatibilityDoctor(buildReport(input), { input });
181
+ console.log(rest.includes("--json") ? JSON.stringify(doctor, null, 2) : formatDoctor(doctor));
182
+ process.exitCode = doctorExitCode(doctor, { strict: rest.includes("--strict") });
183
+ return;
184
+ }
185
+ if (command === "badge") {
186
+ const input = readCapture(rest[0]);
187
+ const badge = buildCompatibilityBadge(buildReport(input), { input });
188
+ console.log(rest.includes("--json") ? JSON.stringify(badge, null, 2) : badge.markdown);
189
+ return;
190
+ }
51
191
  throw new Error(`Unknown compat command: ${command || ""}`.trim());
52
192
  }
53
193
 
package/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  export {
2
2
  assertReadOnlySnapshot,
3
+ buildCompatibilityBadge,
4
+ buildCompatibilityDoctor,
3
5
  buildCompatibilityRealityCheck,
4
6
  buildPercolatorCompatibilityReport,
5
7
  compareCompatibilityReports,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@perpscope/percolator-adapter",
3
- "version": "0.8.0",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
5
  "description": "Read-only Percolator adapter helpers for Solana perps terminals.",
6
6
  "main": "./index.js",
@@ -1,6 +1,6 @@
1
1
  import { normalizeFundingSkewHistory } from "./funding-history.js";
2
2
 
3
- export const PERPSCOPE_ADAPTER_VERSION = "0.8.0";
3
+ export const PERPSCOPE_ADAPTER_VERSION = "1.0.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([
@@ -285,6 +285,75 @@ export function buildCompatibilityRealityCheck(inputOrReport, options = {}) {
285
285
  };
286
286
  }
287
287
 
288
+ export function buildCompatibilityDoctor(inputOrReport, options = {}) {
289
+ const hasReportShape = inputOrReport && typeof inputOrReport === "object" && Array.isArray(inputOrReport.recognizedSections);
290
+ const report = hasReportShape ? normalizeCompatibilityReport(inputOrReport) : buildPercolatorCompatibilityReport(inputOrReport);
291
+ const reality = buildCompatibilityRealityCheck(report, {
292
+ input: hasReportShape ? options.input : inputOrReport,
293
+ generatedAt: options.generatedAt,
294
+ packageVersion: options.packageVersion
295
+ });
296
+ const requiredLane = reality.lanes.find((lane) => lane.label === "required") || {};
297
+ const usefulLane = reality.lanes.find((lane) => lane.label === "useful") || {};
298
+ const pass = report.status === "compatible" || (reality.gaps.dangerMissing === 0 && report.status === "partial");
299
+ const safety = report.status === "rejected" ? "rejected" : "read-only";
300
+
301
+ return {
302
+ schema: "perpscope.compatibility-doctor",
303
+ version: 1,
304
+ package: {
305
+ name: "@perpscope/percolator-adapter",
306
+ version: options.packageVersion || PERPSCOPE_ADAPTER_VERSION
307
+ },
308
+ generatedAt: options.generatedAt || new Date().toISOString(),
309
+ pass,
310
+ tone: report.status === "rejected" || reality.gaps.dangerMissing ? "danger" : report.summary.ignoredCount || report.summary.suggestionCount ? "warning" : "good",
311
+ status: report.status,
312
+ shape: report.shape,
313
+ score: report.score,
314
+ safety,
315
+ source: report.source,
316
+ required: {
317
+ mapped: reality.mapped.requiredCount,
318
+ total: reality.mapped.requiredTotal,
319
+ label: requiredLane.value || `${reality.mapped.requiredCount}/${reality.mapped.requiredTotal}`
320
+ },
321
+ useful: {
322
+ mapped: reality.mapped.optionalCount,
323
+ total: reality.mapped.optionalTotal,
324
+ label: usefulLane.value || `${reality.mapped.optionalCount}/${reality.mapped.optionalTotal}`
325
+ },
326
+ unknownFields: report.ignoredFields || [],
327
+ aliasSuggestions: report.aliasSuggestions || [],
328
+ missingFields: report.missingFields || [],
329
+ nextActions: compatibilityDoctorActions(report, reality)
330
+ };
331
+ }
332
+
333
+ export function buildCompatibilityBadge(inputOrReport, options = {}) {
334
+ const doctor = inputOrReport?.schema === "perpscope.compatibility-doctor"
335
+ ? inputOrReport
336
+ : buildCompatibilityDoctor(inputOrReport, options);
337
+ const label = options.label || "PerpScope compatible";
338
+ const summary = `${doctor.status}, ${doctor.score}/100, ${doctor.aliasSuggestions.length} alias suggestions`;
339
+ return {
340
+ schema: "perpscope.compatibility-badge",
341
+ version: 1,
342
+ package: doctor.package,
343
+ generatedAt: options.generatedAt || doctor.generatedAt || new Date().toISOString(),
344
+ label,
345
+ status: doctor.status,
346
+ score: doctor.score,
347
+ tone: doctor.tone,
348
+ aliasSuggestionCount: doctor.aliasSuggestions.length,
349
+ unknownFieldCount: doctor.unknownFields.length,
350
+ required: doctor.required,
351
+ useful: doctor.useful,
352
+ markdown: `**${label}:** ${summary}`,
353
+ text: `${label}: ${summary}`
354
+ };
355
+ }
356
+
288
357
  const COMPATIBILITY_SECTION_SPECS = [
289
358
  {
290
359
  id: "safety",
@@ -737,6 +806,32 @@ function realityProvenance(input, report) {
737
806
  };
738
807
  }
739
808
 
809
+ function compatibilityDoctorActions(report, reality) {
810
+ const actions = [];
811
+ if (report.status === "rejected") {
812
+ actions.push("Remove wallet, signer, transaction, instruction, order, secret, private key, seed, mnemonic, or API key fields.");
813
+ return actions;
814
+ }
815
+ const dangerMissing = (report.missingFields || []).filter((field) => field.severity === "danger");
816
+ if (dangerMissing.length) {
817
+ actions.push(`Map required fields: ${dangerMissing.map((field) => field.field).join(", ")}.`);
818
+ }
819
+ const warningMissing = (report.missingFields || []).filter((field) => field.severity !== "danger").slice(0, 3);
820
+ if (warningMissing.length) {
821
+ actions.push(`Add useful trader fields: ${warningMissing.map((field) => field.field).join(", ")}.`);
822
+ }
823
+ if ((report.aliasSuggestions || []).length) {
824
+ actions.push(`Apply alias suggestions: ${(report.aliasSuggestions || []).slice(0, 3).map((suggestion) => `${suggestion.candidatePath || suggestion.action} -> ${suggestion.field}`).join(", ")}.`);
825
+ }
826
+ if ((report.ignoredFields || []).length) {
827
+ actions.push(`${report.ignoredFields.length} unknown field${report.ignoredFields.length === 1 ? "" : "s"} can be mapped or intentionally ignored.`);
828
+ }
829
+ if (!actions.length && reality.gaps.dangerMissing === 0) {
830
+ actions.push("Ready for read-only terminal display.");
831
+ }
832
+ return actions;
833
+ }
834
+
740
835
  function differenceByField(left = [], right = []) {
741
836
  const rightFields = new Set((right || []).map((entry) => entry.field));
742
837
  return (left || []).filter((entry) => !rightFields.has(entry.field));