@perpscope/percolator-adapter 0.7.0 → 0.9.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
@@ -63,11 +63,19 @@ The full field-level contract is documented in `../../docs/field-compatibility-m
63
63
 
64
64
  `compareCompatibilityReports(previous, current)` returns `perpscope.compatibility-diff` with score delta, status change, new/resolved fields, section drift, and merged alias suggestions.
65
65
 
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
+
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
+
66
72
  ## CLI
67
73
 
68
74
  ```bash
69
75
  perpscope compat report capture.json
70
76
  perpscope compat diff previous.json current.json
77
+ perpscope compat doctor capture.json
78
+ perpscope compat badge capture.json --json
71
79
  ```
72
80
 
73
81
  Try it locally with:
@@ -76,6 +84,10 @@ Try it locally with:
76
84
  perpscope compat diff ../../examples/fixture-pack-minimal-terminal.json ../../examples/fixture-pack-drifted-aliases.json
77
85
  ```
78
86
 
87
+ For the real-backed candidate path, try `../../examples/fixture-pack-real-sanitized-rpc-shape.json`.
88
+
89
+ For a copy-paste starter shape, use `../../examples/capture-template.json`.
90
+
79
91
  ## DTO Example
80
92
 
81
93
  ```js
package/bin/perpscope.mjs CHANGED
@@ -1,8 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFileSync } 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
@@ -15,6 +19,8 @@ function usage() {
15
19
  "Usage:",
16
20
  " perpscope compat report <capture.json>",
17
21
  " perpscope compat diff <previous.json> <current.json>",
22
+ " perpscope compat doctor <capture.json>",
23
+ " perpscope compat badge <capture.json> [--json|--markdown]",
18
24
  "",
19
25
  "Read-only only: the adapter rejects wallet, signer, transaction, instruction, order, private key, seed, mnemonic, and API key fields."
20
26
  ].join("\n");
@@ -26,10 +32,30 @@ function readCapture(path) {
26
32
  }
27
33
 
28
34
  function buildReport(input) {
29
- const snapshot = normalizePercolatorSnapshot(input);
35
+ const snapshot = detectPercolatorInputShape(input) === "read-only-rpc-fetch"
36
+ ? buildReadOnlyRpcSnapshot(input)
37
+ : normalizePercolatorSnapshot(input);
30
38
  return buildPercolatorCompatibilityReport(input, snapshot);
31
39
  }
32
40
 
41
+ function formatDoctor(doctor) {
42
+ const lines = [
43
+ `PerpScope compat doctor: ${doctor.pass ? "PASS" : "CHECK"}`,
44
+ `shape: ${doctor.shape}`,
45
+ `status: ${doctor.status} (${doctor.score}/100)`,
46
+ `safety: ${doctor.safety}`,
47
+ `required: ${doctor.required.label}`,
48
+ `useful: ${doctor.useful.label}`,
49
+ `unknown fields: ${doctor.unknownFields.length}`,
50
+ `alias suggestions: ${doctor.aliasSuggestions.length}`
51
+ ];
52
+ if (doctor.nextActions.length) {
53
+ lines.push("next actions:");
54
+ for (const action of doctor.nextActions) lines.push(`- ${action}`);
55
+ }
56
+ return lines.join("\n");
57
+ }
58
+
33
59
  function main() {
34
60
  const [scope, command, ...rest] = args;
35
61
  if (!scope || scope === "--help" || scope === "-h") {
@@ -48,6 +74,17 @@ function main() {
48
74
  console.log(JSON.stringify(compareCompatibilityReports(previous, current), null, 2));
49
75
  return;
50
76
  }
77
+ if (command === "doctor") {
78
+ const input = readCapture(rest[0]);
79
+ console.log(formatDoctor(buildCompatibilityDoctor(buildReport(input), { input })));
80
+ return;
81
+ }
82
+ if (command === "badge") {
83
+ const input = readCapture(rest[0]);
84
+ const badge = buildCompatibilityBadge(buildReport(input), { input });
85
+ console.log(rest.includes("--json") ? JSON.stringify(badge, null, 2) : badge.markdown);
86
+ return;
87
+ }
51
88
  throw new Error(`Unknown compat command: ${command || ""}`.trim());
52
89
  }
53
90
 
package/index.js CHANGED
@@ -1,5 +1,8 @@
1
1
  export {
2
2
  assertReadOnlySnapshot,
3
+ buildCompatibilityBadge,
4
+ buildCompatibilityDoctor,
5
+ buildCompatibilityRealityCheck,
3
6
  buildPercolatorCompatibilityReport,
4
7
  compareCompatibilityReports,
5
8
  detectPercolatorInputShape,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@perpscope/percolator-adapter",
3
- "version": "0.7.0",
3
+ "version": "0.9.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.7.0";
3
+ export const PERPSCOPE_ADAPTER_VERSION = "0.9.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([
@@ -231,6 +231,129 @@ export function exportCompatibilityReportFromReport(report, options = {}) {
231
231
  };
232
232
  }
233
233
 
234
+ export function buildCompatibilityRealityCheck(inputOrReport, options = {}) {
235
+ const hasReportShape = inputOrReport && typeof inputOrReport === "object" && Array.isArray(inputOrReport.recognizedSections);
236
+ const input = hasReportShape ? options.input : inputOrReport;
237
+ const report = hasReportShape ? normalizeCompatibilityReport(inputOrReport) : buildPercolatorCompatibilityReport(inputOrReport);
238
+ const requiredFields = COMPATIBILITY_FIELD_SPECS.filter((field) => field.severity === "danger");
239
+ const optionalFields = COMPATIBILITY_FIELD_SPECS.filter((field) => field.severity !== "danger");
240
+ const missingFieldSet = new Set((report.missingFields || []).map((field) => field.field));
241
+ const mappedRequired = requiredFields.filter((field) => !missingFieldSet.has(field.field));
242
+ const mappedOptional = optionalFields.filter((field) => !missingFieldSet.has(field.field));
243
+ const provenance = realityProvenance(input, report);
244
+ const unknownCount = report.summary?.ignoredCount ?? report.ignoredFields.length;
245
+ const aliasCount = report.summary?.suggestionCount ?? report.aliasSuggestions.length;
246
+ const dangerMissing = (report.missingFields || []).filter((field) => field.severity === "danger").length;
247
+ const warningMissing = (report.missingFields || []).length - dangerMissing;
248
+ const tone = dangerMissing
249
+ ? "danger"
250
+ : warningMissing || unknownCount || provenance.status === "candidate"
251
+ ? "warning"
252
+ : "good";
253
+
254
+ return {
255
+ schema: "perpscope.reality-check",
256
+ version: 1,
257
+ package: {
258
+ name: "@perpscope/percolator-adapter",
259
+ version: options.packageVersion || PERPSCOPE_ADAPTER_VERSION
260
+ },
261
+ generatedAt: options.generatedAt || new Date().toISOString(),
262
+ tone,
263
+ status: provenance.status,
264
+ sourceKind: provenance.kind,
265
+ provenance,
266
+ mapped: {
267
+ requiredCount: mappedRequired.length,
268
+ requiredTotal: requiredFields.length,
269
+ optionalCount: mappedOptional.length,
270
+ optionalTotal: optionalFields.length,
271
+ recognizedCount: report.summary?.recognizedCount ?? report.recognizedSections.length
272
+ },
273
+ gaps: {
274
+ dangerMissing,
275
+ warningMissing,
276
+ unknownCount,
277
+ aliasCount
278
+ },
279
+ lanes: [
280
+ realityLane("required", `${mappedRequired.length}/${requiredFields.length}`, dangerMissing ? "danger" : "good"),
281
+ realityLane("useful", `${mappedOptional.length}/${optionalFields.length}`, warningMissing ? "warning" : "good"),
282
+ realityLane("unknown", String(unknownCount), unknownCount ? "warning" : "good"),
283
+ realityLane("aliases", String(aliasCount), aliasCount ? "good" : "neutral")
284
+ ]
285
+ };
286
+ }
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
+
234
357
  const COMPATIBILITY_SECTION_SPECS = [
235
358
  {
236
359
  id: "safety",
@@ -659,6 +782,56 @@ function compatibilityDiffSummary(report) {
659
782
  };
660
783
  }
661
784
 
785
+ function realityLane(label, value, tone = "neutral") {
786
+ return { label, value, tone };
787
+ }
788
+
789
+ function realityProvenance(input, report) {
790
+ const source = report.source || {};
791
+ const inputSource = input && typeof input === "object" && !Array.isArray(input) ? input.source || {} : {};
792
+ const kind = stringOf(input, ["fixtureKind", "sourceKind"], inputSource.kind || source.shape || report.shape || "decoded-capture");
793
+ const candidate = /candidate|read-only-rpc|real/i.test(kind) || Boolean(inputSource.realBacked);
794
+ const submitted = Boolean(inputSource.submittedBy) || Boolean(inputSource.externalSubmission);
795
+ const status = submitted ? "submitted" : candidate ? "candidate" : "synthetic";
796
+ return {
797
+ status,
798
+ kind,
799
+ label: source.label || stringOf(input, ["label"], "decoded capture"),
800
+ cluster: source.cluster || stringOf(input, ["cluster"], "unknown"),
801
+ basis: inputSource.basis || inputSource.capture || "",
802
+ fixture: stringOf(input, ["fixture"], ""),
803
+ sanitized: inputSource.sanitized !== false,
804
+ submittedBy: inputSource.submittedBy || "",
805
+ note: inputSource.note || (status === "candidate" ? "real-backed candidate; still waiting on third-party decoded shape" : "")
806
+ };
807
+ }
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
+
662
835
  function differenceByField(left = [], right = []) {
663
836
  const rightFields = new Set((right || []).map((entry) => entry.field));
664
837
  return (left || []).filter((entry) => !rightFields.has(entry.field));
@@ -97,6 +97,10 @@ export function buildReadOnlyRpcSnapshot(request) {
97
97
  if (decoded.params) commands.push({ command: "slab:params", output: decoded.params });
98
98
  if (decoded.engine) commands.push({ command: "slab:engine", output: decoded.engine });
99
99
  if (decoded.bestPrice) commands.push({ command: "best-price", output: decoded.bestPrice });
100
+ if (decoded.receipts || decoded.executionReceipts) commands.push({ command: "execution:receipts", output: decoded.receipts || decoded.executionReceipts });
101
+ if (decoded.fundingSkew || decoded.fundingHistory || decoded.fundingSkewHistory) {
102
+ commands.push({ command: "funding-skew-history", output: decoded.fundingSkew || decoded.fundingHistory || decoded.fundingSkewHistory });
103
+ }
100
104
  if (decoded.accounts) commands.push({ command: "slab:accounts", output: decoded.accounts });
101
105
  if (decoded.bitmap) commands.push({ command: "slab:bitmap", output: decoded.bitmap });
102
106