Skip to content

Commit f53abef

Browse files
committed
feat: auto-generate SDK code samples, remove hand-written @example tags
Replace 78 hand-written @example-ts/@example-python JSDoc blocks with auto-generated examples derived from method signatures. Add x-sdk-constructors vendor extension to OpenAPI spec so downstream docs can generate per-exchange code samples without drift.
1 parent 06f2477 commit f53abef

12 files changed

Lines changed: 860 additions & 1652 deletions

File tree

‎changelog.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [2.27.7] - 2026-04-10
6+
7+
### Changed
8+
9+
- **Auto-generated SDK code samples replace hand-written `@example-ts`/`@example-python` JSDoc tags**: Removed all 78 hand-written example blocks from `BaseExchange.ts` and exchange files (`limitless`, `polymarket`, `probable`). These caused documentation drift — examples fell out of sync with the actual SDK API surface whenever method signatures changed. `extract-jsdoc.js` no longer parses or emits example tags. `generate-api-docs.js` now auto-generates idiomatic Python and TypeScript examples from method names and parameter signatures, using a shared `EXAMPLE_VALUES` lookup table for sensible defaults (with language-aware boolean formatting: `True`/`true`). The Handlebars templates are unchanged — they consume the same `{{this.example}}` slot, now populated by the auto-generator instead of hand-written JSDoc.
10+
11+
- **`generate-openapi.js` emits `x-sdk-constructors` vendor extension**: Parses `createExchange()` in `app.ts` (the same source of truth used by `generate-python-exchanges.js`) to extract per-exchange credential requirements, then embeds the result in the OpenAPI spec as `x-sdk-constructors`. Each entry maps an exchange wire key to its class name and constructor params (with Python snake_case names, TypeScript camelCase names, types, descriptions, and defaults). Adding a new exchange to `app.ts` automatically flows through to the spec — no manual metadata file to maintain. Consumed downstream by `hosted-pmxt/scripts/sync-docs.js` to generate per-exchange Mintlify code samples.
12+
513
## [2.27.6] - 2026-04-09
614

715
### Fixed

‎core/api-doc-config.generated.json‎

Lines changed: 33 additions & 591 deletions
Large diffs are not rendered by default.

‎core/scripts/generate-openapi.js‎

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const path = require('path');
1212
const yaml = require('js-yaml');
1313

1414
const BASE_EXCHANGE_PATH = path.join(__dirname, '../src/BaseExchange.ts');
15+
const APP_TS_PATH = path.join(__dirname, '../src/server/app.ts');
1516
const OPENAPI_OUT_PATH = path.join(__dirname, '../src/server/openapi.yaml');
1617
// Sidecar metadata consumed by the runtime server (app.ts) so the GET
1718
// handler knows which methods are safe to expose as GET and how to
@@ -961,6 +962,159 @@ function buildMethodVerbs(methodSpecs) {
961962
return out;
962963
}
963964

965+
// ---------------------------------------------------------------------------
966+
// Exchange constructor metadata (x-sdk-constructors)
967+
//
968+
// Parses createExchange() in app.ts to discover which exchanges exist and
969+
// which credentials each one requires — the same logic used by
970+
// generate-python-exchanges.js. The result is attached to the OpenAPI spec
971+
// as the `x-sdk-constructors` vendor extension so downstream consumers
972+
// (e.g. Mintlify docs sync) can auto-generate per-exchange SDK samples.
973+
// ---------------------------------------------------------------------------
974+
975+
// Overrides that cannot be derived from app.ts alone.
976+
const EXCHANGE_OVERRIDES = {
977+
polymarket: {
978+
defaults: { signature_type: 'gnosis-safe' },
979+
},
980+
myriad: {
981+
paramAliases: { private_key: 'wallet_address' },
982+
paramDocs: {
983+
wallet_address: 'Wallet address (required for positions and balance)',
984+
},
985+
},
986+
};
987+
988+
function toClassName(name) {
989+
return name
990+
.split(/[-_]/)
991+
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
992+
.join('');
993+
}
994+
995+
function parseExchanges(content) {
996+
const startIdx = content.indexOf('function createExchange(');
997+
if (startIdx === -1) throw new Error('createExchange not found in app.ts');
998+
999+
const tail = content.slice(startIdx);
1000+
let depth = 0;
1001+
let bodyEnd = 0;
1002+
for (let i = tail.indexOf('{'); i < tail.length; i++) {
1003+
if (tail[i] === '{') depth++;
1004+
else if (tail[i] === '}') {
1005+
depth--;
1006+
if (depth === 0) { bodyEnd = i + 1; break; }
1007+
}
1008+
}
1009+
const funcBody = tail.slice(0, bodyEnd);
1010+
1011+
const exchanges = [];
1012+
const lines = funcBody.split('\n');
1013+
let currentName = null;
1014+
let currentBlock = '';
1015+
1016+
for (const line of lines) {
1017+
const caseMatch = line.match(/^\s*case "([^"]+)":/);
1018+
if (caseMatch) {
1019+
if (currentName) exchanges.push(buildExchange(currentName, currentBlock));
1020+
currentName = caseMatch[1];
1021+
currentBlock = '';
1022+
continue;
1023+
}
1024+
if (/^\s*default:/.test(line) && currentName) {
1025+
exchanges.push(buildExchange(currentName, currentBlock));
1026+
currentName = null;
1027+
currentBlock = '';
1028+
continue;
1029+
}
1030+
if (currentName) currentBlock += line + '\n';
1031+
}
1032+
if (currentName) exchanges.push(buildExchange(currentName, currentBlock));
1033+
1034+
return exchanges;
1035+
}
1036+
1037+
function buildExchange(name, block) {
1038+
return {
1039+
name,
1040+
creds: {
1041+
apiKey: /credentials\?\.apiKey/.test(block),
1042+
apiToken: /credentials\?\.apiToken/.test(block),
1043+
apiSecret: /credentials\?\.apiSecret/.test(block),
1044+
passphrase: /credentials\?\.passphrase/.test(block),
1045+
privateKey: /credentials\?\.privateKey/.test(block),
1046+
funderAddress: /credentials\?\.funderAddress/.test(block),
1047+
signatureType: /credentials\?\.signatureType/.test(block),
1048+
},
1049+
};
1050+
}
1051+
1052+
// Credential flag → default param metadata
1053+
const CRED_PARAM_MAP = {
1054+
apiKey: { name: 'api_key', tsName: 'apiKey', description: 'API key for authentication' },
1055+
apiToken: { name: 'api_token', tsName: 'apiToken', description: 'API token for authentication' },
1056+
apiSecret: { name: 'api_secret', tsName: 'apiSecret', description: 'API secret for authentication' },
1057+
passphrase: { name: 'passphrase', tsName: 'passphrase', description: 'Passphrase for authentication' },
1058+
privateKey: { name: 'private_key', tsName: 'privateKey', description: 'Private key for authentication' },
1059+
funderAddress: { name: 'proxy_address', tsName: 'proxyAddress', description: 'Proxy/smart wallet address' },
1060+
signatureType: { name: 'signature_type', tsName: 'signatureType', description: 'Signature type' },
1061+
};
1062+
1063+
function camelCase(snakeName) {
1064+
return snakeName.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
1065+
}
1066+
1067+
function buildSdkConstructors(exchanges) {
1068+
const result = {};
1069+
1070+
for (const exchange of exchanges) {
1071+
const { name, creds } = exchange;
1072+
const ov = EXCHANGE_OVERRIDES[name] || {};
1073+
const aliases = ov.paramAliases || {};
1074+
const defaults = ov.defaults || {};
1075+
const paramDocs = ov.paramDocs || {};
1076+
1077+
// Every exchange gets the hosted API key param first
1078+
const params = [
1079+
{
1080+
name: 'pmxt_api_key',
1081+
tsName: 'pmxtApiKey',
1082+
type: 'string',
1083+
description: 'PMXT API key for hosted access',
1084+
},
1085+
];
1086+
1087+
for (const [credFlag, baseMeta] of Object.entries(CRED_PARAM_MAP)) {
1088+
if (!creds[credFlag]) continue;
1089+
1090+
const aliasedSnakeName = aliases[baseMeta.name] || baseMeta.name;
1091+
const aliasedTsName = camelCase(aliasedSnakeName);
1092+
const description = paramDocs[aliasedSnakeName] || baseMeta.description;
1093+
1094+
const param = {
1095+
name: aliasedSnakeName,
1096+
tsName: aliasedTsName,
1097+
type: 'string',
1098+
description,
1099+
};
1100+
1101+
const defaultVal = defaults[baseMeta.name];
1102+
if (defaultVal) {
1103+
param.default = defaultVal;
1104+
}
1105+
1106+
params.push(param);
1107+
}
1108+
1109+
result[name] = {
1110+
className: toClassName(name),
1111+
params,
1112+
};
1113+
}
1114+
1115+
return result;
1116+
}
1117+
9641118
function main() {
9651119
// Build the interface registry and the full component-schema map
9661120
// FIRST. `buildPathSpec` below consults the global SCHEMAS map via
@@ -981,6 +1135,11 @@ function main() {
9811135
const methodSpecs = methodNodes.map(m => buildPathSpec(m, sourceFile));
9821136
const spec = buildSpec(methodSpecs);
9831137

1138+
// Attach per-exchange SDK constructor metadata from app.ts
1139+
const appTsContent = fs.readFileSync(APP_TS_PATH, 'utf-8');
1140+
const exchanges = parseExchanges(appTsContent);
1141+
spec['x-sdk-constructors'] = buildSdkConstructors(exchanges);
1142+
9841143
const yamlStr = yaml.dump(spec, {
9851144
indent: 2,
9861145
lineWidth: 120,

0 commit comments

Comments
 (0)