@manifest-network/manifest-agent-core 0.10.0 → 0.12.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/dist/close-lease.d.ts +3 -2
- package/dist/close-lease.d.ts.map +1 -1
- package/dist/close-lease.js +4 -3
- package/dist/close-lease.js.map +1 -1
- package/dist/deploy-app.d.ts +3 -2
- package/dist/deploy-app.d.ts.map +1 -1
- package/dist/deploy-app.js +245 -77
- package/dist/deploy-app.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/internals/build-fred-input.d.ts +38 -0
- package/dist/internals/build-fred-input.d.ts.map +1 -0
- package/dist/internals/build-fred-input.js +147 -0
- package/dist/internals/build-fred-input.js.map +1 -0
- package/dist/internals/classify-deploy-error.d.ts +13 -9
- package/dist/internals/classify-deploy-error.d.ts.map +1 -1
- package/dist/internals/classify-deploy-error.js +15 -11
- package/dist/internals/classify-deploy-error.js.map +1 -1
- package/dist/internals/classify-deploy-response.d.ts.map +1 -1
- package/dist/internals/classify-deploy-response.js.map +1 -1
- package/dist/internals/connection.d.ts.map +1 -1
- package/dist/internals/connection.js.map +1 -1
- package/dist/internals/evaluate-readiness-from-fred.d.ts +28 -0
- package/dist/internals/evaluate-readiness-from-fred.d.ts.map +1 -0
- package/dist/internals/evaluate-readiness-from-fred.js +94 -0
- package/dist/internals/evaluate-readiness-from-fred.js.map +1 -0
- package/dist/internals/evaluate-readiness.d.ts.map +1 -1
- package/dist/internals/evaluate-readiness.js.map +1 -1
- package/dist/internals/find-sku-uuid.d.ts.map +1 -1
- package/dist/internals/find-sku-uuid.js.map +1 -1
- package/dist/internals/format-success.d.ts.map +1 -1
- package/dist/internals/format-success.js.map +1 -1
- package/dist/internals/guarded-fetch.d.ts +2 -138
- package/dist/internals/guarded-fetch.js +1 -241
- package/dist/internals/humanize-denom.d.ts.map +1 -1
- package/dist/internals/humanize-denom.js.map +1 -1
- package/dist/internals/inspect-image.d.ts.map +1 -1
- package/dist/internals/inspect-image.js.map +1 -1
- package/dist/internals/lease-items.d.ts.map +1 -1
- package/dist/internals/lease-items.js +1 -4
- package/dist/internals/lease-items.js.map +1 -1
- package/dist/internals/lease-state.d.ts.map +1 -1
- package/dist/internals/lease-state.js.map +1 -1
- package/dist/internals/render-deployment-plan.d.ts.map +1 -1
- package/dist/internals/render-deployment-plan.js.map +1 -1
- package/dist/internals/render-intent-recap.d.ts.map +1 -1
- package/dist/internals/render-intent-recap.js.map +1 -1
- package/dist/internals/render-partial-success-prompt.d.ts.map +1 -1
- package/dist/internals/render-partial-success-prompt.js.map +1 -1
- package/dist/internals/save-manifest.d.ts.map +1 -1
- package/dist/internals/save-manifest.js.map +1 -1
- package/dist/internals/secret-denylist.d.ts.map +1 -1
- package/dist/internals/secret-denylist.js.map +1 -1
- package/dist/internals/spec-normalize.d.ts.map +1 -1
- package/dist/internals/spec-normalize.js.map +1 -1
- package/dist/internals/verify-domain-state.d.ts.map +1 -1
- package/dist/internals/verify-domain-state.js.map +1 -1
- package/dist/internals/verify-recover.d.ts.map +1 -1
- package/dist/internals/verify-recover.js.map +1 -1
- package/dist/manage-domain.d.ts +3 -2
- package/dist/manage-domain.d.ts.map +1 -1
- package/dist/manage-domain.js +4 -3
- package/dist/manage-domain.js.map +1 -1
- package/dist/troubleshoot.js.map +1 -1
- package/dist/types.d.ts +19 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -7
- package/dist/internals/guarded-fetch.d.ts.map +0 -1
- package/dist/internals/guarded-fetch.js.map +0 -1
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Companion to `classify-deploy-response.ts`: that file handles the RETURN
|
|
7
7
|
* path; this file handles the THROW path. The split exists because
|
|
8
|
-
* `manifest-mcp-fred`
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* `manifest-mcp-fred`'s `deployApp` throws `ManifestMCPError` with
|
|
9
|
+
* `details.lease_uuid` populated when create-lease succeeded but something
|
|
10
|
+
* downstream (set-domain, manifest upload, readiness poll) fell over. The
|
|
11
|
+
* partial-success signal is carried structurally as `details.partial === true`
|
|
12
|
+
* (ENG-280); the legacy `Deploy partially succeeded: lease ${uuid} was created
|
|
13
|
+
* but subsequent steps failed.` message prefix is retained as a cross-version
|
|
14
|
+
* fallback for fred builds that predate the structured flag.
|
|
13
15
|
*
|
|
14
16
|
* Recognised input envelope shapes:
|
|
15
17
|
* - `{ message, details?, code? }`
|
|
@@ -19,10 +21,12 @@
|
|
|
19
21
|
* classified as `outcome: 'failed'` with a stable `reason`, so the
|
|
20
22
|
* orchestrator can branch on the JSON without an outer try/catch.
|
|
21
23
|
*
|
|
22
|
-
* `outcome: 'partially_succeeded'` triggers
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
24
|
+
* `outcome: 'partially_succeeded'` triggers primarily on the structured
|
|
25
|
+
* `details.partial === true` discriminant. As a fallback (for fred builds
|
|
26
|
+
* without the flag) it also triggers when `err.message` starts with the exact
|
|
27
|
+
* prefix `Deploy partially succeeded:`. The prefix match is EXACT-start only:
|
|
28
|
+
* looser matching would risk false positives on wrapper errors that happen to
|
|
29
|
+
* contain the phrase nested inside other text.
|
|
26
30
|
*/
|
|
27
31
|
/** Permissive UUID pattern (RFC-4122 8-4-4-4-12, version byte lenient). */
|
|
28
32
|
const UUID_PATTERN = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
|
|
@@ -48,7 +52,7 @@ function classifyDeployError(err, opts = {}) {
|
|
|
48
52
|
const envelope = e;
|
|
49
53
|
const message = typeof envelope.message === "string" ? envelope.message : "";
|
|
50
54
|
const details = envelope.details !== null && typeof envelope.details === "object" && !Array.isArray(envelope.details) ? envelope.details : {};
|
|
51
|
-
if (message.startsWith(PARTIAL_PREFIX)) {
|
|
55
|
+
if (details.partial === true || message.startsWith(PARTIAL_PREFIX)) {
|
|
52
56
|
let leaseUuid;
|
|
53
57
|
if (typeof details.lease_uuid === "string") leaseUuid = details.lease_uuid;
|
|
54
58
|
else {
|
|
@@ -58,7 +62,7 @@ function classifyDeployError(err, opts = {}) {
|
|
|
58
62
|
return finalize({
|
|
59
63
|
outcome: "partially_succeeded",
|
|
60
64
|
...leaseUuid !== void 0 && { leaseUuid },
|
|
61
|
-
reason: message
|
|
65
|
+
reason: message || "deploy partially succeeded; lease was created"
|
|
62
66
|
}, expectedCustomDomain);
|
|
63
67
|
}
|
|
64
68
|
return finalize({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"classify-deploy-error.js","names":[],"sources":["../../src/internals/classify-deploy-error.ts"],"sourcesContent":["/**\n * Classify the MCP error envelope thrown by `mcp__manifest-fred__deploy_app`\n * when the call fails AFTER the create-lease tx already confirmed.\n *\n * Companion to `classify-deploy-response.ts`: that file handles the RETURN\n * path; this file handles the THROW path. The split exists because\n * `manifest-mcp-fred`
|
|
1
|
+
{"version":3,"file":"classify-deploy-error.js","names":[],"sources":["../../src/internals/classify-deploy-error.ts"],"sourcesContent":["/**\n * Classify the MCP error envelope thrown by `mcp__manifest-fred__deploy_app`\n * when the call fails AFTER the create-lease tx already confirmed.\n *\n * Companion to `classify-deploy-response.ts`: that file handles the RETURN\n * path; this file handles the THROW path. The split exists because\n * `manifest-mcp-fred`'s `deployApp` throws `ManifestMCPError` with\n * `details.lease_uuid` populated when create-lease succeeded but something\n * downstream (set-domain, manifest upload, readiness poll) fell over. The\n * partial-success signal is carried structurally as `details.partial === true`\n * (ENG-280); the legacy `Deploy partially succeeded: lease ${uuid} was created\n * but subsequent steps failed.` message prefix is retained as a cross-version\n * fallback for fred builds that predate the structured flag.\n *\n * Recognised input envelope shapes:\n * - `{ message, details?, code? }`\n * - `{ error: { message, details?, code? } }`\n *\n * Returns deterministically — never throws. A malformed envelope is\n * classified as `outcome: 'failed'` with a stable `reason`, so the\n * orchestrator can branch on the JSON without an outer try/catch.\n *\n * `outcome: 'partially_succeeded'` triggers primarily on the structured\n * `details.partial === true` discriminant. As a fallback (for fred builds\n * without the flag) it also triggers when `err.message` starts with the exact\n * prefix `Deploy partially succeeded:`. The prefix match is EXACT-start only:\n * looser matching would risk false positives on wrapper errors that happen to\n * contain the phrase nested inside other text.\n */\n\n/** Permissive UUID pattern (RFC-4122 8-4-4-4-12, version byte lenient). */\nconst UUID_PATTERN =\n /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;\n\nconst PARTIAL_PREFIX = 'Deploy partially succeeded:';\n\nexport interface DeployErrorClassification {\n outcome: 'partially_succeeded' | 'failed';\n /** Present when create-lease confirmed (outcome partially_succeeded), or when a UUID was extractable from a non-partial error. */\n leaseUuid?: string;\n /** Echoed from `opts.expectedCustomDomain` so downstream prompts can name the FQDN. */\n requestedCustomDomain?: string;\n /** Human-readable summary — the raw error message (or a stable placeholder if missing). */\n reason: string;\n}\n\n/**\n * Pick the inner envelope when the error is wrapped as `{ error: {...} }`.\n * `JSON.stringify(err)` produces this shape in some SDKs.\n */\nfunction pickEnvelope(raw: unknown): unknown {\n if (raw !== null && typeof raw === 'object') {\n const r = raw as { error?: unknown };\n if (r.error !== null && typeof r.error === 'object') return r.error;\n }\n return raw;\n}\n\nexport function classifyDeployError(\n err: unknown,\n opts: { expectedCustomDomain?: string } = {},\n): DeployErrorClassification {\n const expectedCustomDomain = opts.expectedCustomDomain;\n const e = pickEnvelope(err);\n\n if (e === null || typeof e !== 'object') {\n return finalize(\n {\n outcome: 'failed',\n reason: 'stdin envelope is not an object',\n },\n expectedCustomDomain,\n );\n }\n\n const envelope = e as { message?: unknown; details?: unknown };\n const message = typeof envelope.message === 'string' ? envelope.message : '';\n const details =\n envelope.details !== null &&\n typeof envelope.details === 'object' &&\n !Array.isArray(envelope.details)\n ? (envelope.details as { lease_uuid?: unknown })\n : {};\n\n // Partial-success trigger: structured `details.partial === true`\n // discriminant (the ENG-280 split surfaces it on the partial-success wrap),\n // OR the EXACT legacy upstream prefix as a fallback. The prefix path stays\n // EXACT-start: anything looser risks mis-classifying wrapper errors whose\n // message merely contains the phrase as a substring (defended by case #5 in\n // the CJS test). `=== true` is strict to preserve the anti-false-positive\n // discipline (a truthy-but-non-`true` flag must not trigger cleanup).\n const partialFlag = (details as { partial?: unknown }).partial === true;\n if (partialFlag || message.startsWith(PARTIAL_PREFIX)) {\n let leaseUuid: string | undefined;\n if (typeof details.lease_uuid === 'string') {\n leaseUuid = details.lease_uuid;\n } else {\n const m = message.match(UUID_PATTERN);\n if (m) leaseUuid = m[0];\n }\n return finalize(\n {\n outcome: 'partially_succeeded',\n ...(leaseUuid !== undefined && { leaseUuid }),\n // `details.partial` can trigger this path with an empty envelope\n // message; fall back to a stable placeholder, matching the\n // failed-path contract (\"raw error message, or a placeholder if missing\").\n reason: message || 'deploy partially succeeded; lease was created',\n },\n expectedCustomDomain,\n );\n }\n\n // Anything else: terminal failure — the create-lease tx didn't confirm,\n // or the error happened before broadcast.\n return finalize(\n {\n outcome: 'failed',\n reason: message || 'deploy_app threw an empty error',\n },\n expectedCustomDomain,\n );\n}\n\nfunction finalize(\n base: DeployErrorClassification,\n expectedCustomDomain: string | undefined,\n): DeployErrorClassification {\n if (expectedCustomDomain !== undefined) {\n return { ...base, requestedCustomDomain: expectedCustomDomain };\n }\n return base;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,MAAM,eACJ;AAEF,MAAM,iBAAiB;;;;;AAgBvB,SAAS,aAAa,KAAuB;CAC3C,IAAI,QAAQ,QAAQ,OAAO,QAAQ,UAAU;EAC3C,MAAM,IAAI;EACV,IAAI,EAAE,UAAU,QAAQ,OAAO,EAAE,UAAU,UAAU,OAAO,EAAE;CAChE;CACA,OAAO;AACT;AAEA,SAAgB,oBACd,KACA,OAA0C,CAAC,GAChB;CAC3B,MAAM,uBAAuB,KAAK;CAClC,MAAM,IAAI,aAAa,GAAG;CAE1B,IAAI,MAAM,QAAQ,OAAO,MAAM,UAC7B,OAAO,SACL;EACE,SAAS;EACT,QAAQ;CACV,GACA,oBACF;CAGF,MAAM,WAAW;CACjB,MAAM,UAAU,OAAO,SAAS,YAAY,WAAW,SAAS,UAAU;CAC1E,MAAM,UACJ,SAAS,YAAY,QACrB,OAAO,SAAS,YAAY,YAC5B,CAAC,MAAM,QAAQ,SAAS,OAAO,IAC1B,SAAS,UACV,CAAC;CAUP,IADqB,QAAkC,YAAY,QAChD,QAAQ,WAAW,cAAc,GAAG;EACrD,IAAI;EACJ,IAAI,OAAO,QAAQ,eAAe,UAChC,YAAY,QAAQ;OACf;GACL,MAAM,IAAI,QAAQ,MAAM,YAAY;GACpC,IAAI,GAAG,YAAY,EAAE;EACvB;EACA,OAAO,SACL;GACE,SAAS;GACT,GAAI,cAAc,KAAA,KAAa,EAAE,UAAU;GAI3C,QAAQ,WAAW;EACrB,GACA,oBACF;CACF;CAIA,OAAO,SACL;EACE,SAAS;EACT,QAAQ,WAAW;CACrB,GACA,oBACF;AACF;AAEA,SAAS,SACP,MACA,sBAC2B;CAC3B,IAAI,yBAAyB,KAAA,GAC3B,OAAO;EAAE,GAAG;EAAM,uBAAuB;CAAqB;CAEhE,OAAO;AACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"classify-deploy-response.d.ts","names":[],"sources":["../../src/internals/classify-deploy-response.ts"],"mappings":";;AA0CA
|
|
1
|
+
{"version":3,"file":"classify-deploy-response.d.ts","names":[],"sources":["../../src/internals/classify-deploy-response.ts"],"mappings":";;AA0CA;;;;;;;;;;;;AAOiB;AAGjB;;;;;;;;;;;;AAOc;AAGd;;;;;;UApBiB,mBAAA;EACf,UAAA;EACA,aAAA;EACA,YAAA;EACA,KAAA;EACA,GAAA;EACA,UAAA;EACA,eAAA;AAAA;AAAA,UAGe,4BAAA;EACf,OAAA;EACA,SAAA;EACA,YAAA;EACA,WAAA;EACA,IAAA;EACA,SAAA;EACA,YAAA;AAAA;AAAA,iBAGc,sBAAA,CACd,QAAA,EAAU,mBAAA,GACT,4BAA4B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"classify-deploy-response.js","names":["decodeLeaseState"],"sources":["../../src/internals/classify-deploy-response.ts"],"sourcesContent":["import {\n extractRunningEndpoints,\n formatEndpointAsUrl,\n hasRunningInstances,\n normalizeFredUrl,\n} from './connection.js';\nimport { decode as decodeLeaseState, isTerminal } from './lease-state.js';\n\n/**\n * Classify the RETURN envelope of `mcp__manifest-fred__deploy_app` into one\n * of three outcomes for the orchestrator to branch on.\n *\n * Companion to `classify-deploy-error.ts` (which handles the THROW path).\n *\n * Outcomes:\n * - `'active'` — state is LEASE_STATE_ACTIVE AND at least one\n * running instance exists. Internal-only deploys\n * (every port `ingress: false`) have running\n * instances but no FQDN, so the URL count alone\n * can't gate this; `hasRunningInstances` covers it.\n * Orchestrator can skip `wait_for_app_ready`.\n * - `'needs_wait'` — lease created but not yet active, OR no running\n * instances yet (provider hasn't started the\n * container). Orchestrator polls `wait_for_app_ready`.\n * - `'failed'` — no `lease_uuid` present, OR state is a terminal\n * failure state (CLOSED / REJECTED / EXPIRED, plus\n * the legacy INSUFFICIENT_FUNDS defense-in-depth).\n * Orchestrator routes to troubleshoot/cleanup.\n *\n * Terminal-state set is the union of `TERMINAL_STATES` from `lease-state.ts`\n * — extended from the CJS's `{CLOSED, INSUFFICIENT_FUNDS}` to also cover\n * `REJECTED` and `EXPIRED` since those are emitted by manifestjs 2.4.1\n * (chain v2.1.0) as terminal states. INSUFFICIENT_FUNDS is unreachable\n * from `decode()` on the current chain but retained as defense-in-depth.\n * See `lease-state.ts` for the divergence rationale.\n *\n * Error summary format (qa-engineer's parity pin):\n * - Lease present + terminal: `Lease ${leaseUuid} reached terminal state ${stateName || 'UNKNOWN'}`\n * - Lease missing: `deploy_app returned no lease_uuid`\n * - `connectionError` string present: passed through verbatim\n */\n\nexport interface DeployResponseShape {\n lease_uuid?: unknown;\n provider_uuid?: unknown;\n provider_url?: unknown;\n state?: unknown;\n url?: unknown;\n connection?: unknown;\n connectionError?: unknown;\n}\n\nexport interface DeployResponseClassification {\n outcome: 'active' | 'needs_wait' | 'failed';\n leaseUuid?: string;\n providerUuid?: string;\n providerUrl?: string;\n urls: string[];\n stateName?: string;\n errorSummary?: string;\n}\n\nexport function classifyDeployResponse(\n response: DeployResponseShape,\n): DeployResponseClassification {\n const stateName = decodeLeaseState(\n typeof response.state === 'number' || typeof response.state === 'string'\n ? response.state\n : undefined,\n );\n\n // URL synthesis from connection payload + optional top-level `url`.\n const urls: string[] = extractRunningEndpoints(response.connection).map(\n formatEndpointAsUrl,\n );\n if (typeof response.url === 'string' && response.url.length > 0) {\n const u = normalizeFredUrl(response.url);\n if (u.length > 0 && !urls.includes(u)) urls.unshift(u);\n }\n\n const leaseUuid =\n typeof response.lease_uuid === 'string' ? response.lease_uuid : undefined;\n\n let outcome: DeployResponseClassification['outcome'];\n if (!leaseUuid) {\n outcome = 'failed';\n } else if (\n stateName === 'LEASE_STATE_ACTIVE' &&\n (urls.length > 0 || hasRunningInstances(response.connection))\n ) {\n outcome = 'active';\n } else if (stateName !== undefined && isTerminal(stateName)) {\n outcome = 'failed';\n } else {\n // Pending / unspecified / active-without-any-running-instance / unknown.\n outcome = 'needs_wait';\n }\n\n const out: DeployResponseClassification = {\n outcome,\n ...(leaseUuid !== undefined && { leaseUuid }),\n ...(typeof response.provider_uuid === 'string' && {\n providerUuid: response.provider_uuid,\n }),\n ...(typeof response.provider_url === 'string' && {\n providerUrl: response.provider_url,\n }),\n urls,\n ...(stateName !== undefined && { stateName }),\n };\n\n if (outcome === 'failed') {\n if (typeof response.connectionError === 'string') {\n out.errorSummary = response.connectionError;\n } else if (!leaseUuid) {\n out.errorSummary = 'deploy_app returned no lease_uuid';\n } else {\n // Byte-exact format (qa-engineer's parity pin):\n // `Lease ${lease_uuid} reached terminal state ${stateName || 'UNKNOWN'}`\n out.errorSummary = `Lease ${leaseUuid} reached terminal state ${\n stateName || 'UNKNOWN'\n }`;\n }\n }\n\n return out;\n}\n"],"mappings":";;;AA8DA,SAAgB,uBACd,UAC8B;CAC9B,MAAM,YAAYA,OAChB,OAAO,SAAS,UAAU,YAAY,OAAO,SAAS,UAAU,WAC5D,SAAS,QACT,KAAA,
|
|
1
|
+
{"version":3,"file":"classify-deploy-response.js","names":["decodeLeaseState"],"sources":["../../src/internals/classify-deploy-response.ts"],"sourcesContent":["import {\n extractRunningEndpoints,\n formatEndpointAsUrl,\n hasRunningInstances,\n normalizeFredUrl,\n} from './connection.js';\nimport { decode as decodeLeaseState, isTerminal } from './lease-state.js';\n\n/**\n * Classify the RETURN envelope of `mcp__manifest-fred__deploy_app` into one\n * of three outcomes for the orchestrator to branch on.\n *\n * Companion to `classify-deploy-error.ts` (which handles the THROW path).\n *\n * Outcomes:\n * - `'active'` — state is LEASE_STATE_ACTIVE AND at least one\n * running instance exists. Internal-only deploys\n * (every port `ingress: false`) have running\n * instances but no FQDN, so the URL count alone\n * can't gate this; `hasRunningInstances` covers it.\n * Orchestrator can skip `wait_for_app_ready`.\n * - `'needs_wait'` — lease created but not yet active, OR no running\n * instances yet (provider hasn't started the\n * container). Orchestrator polls `wait_for_app_ready`.\n * - `'failed'` — no `lease_uuid` present, OR state is a terminal\n * failure state (CLOSED / REJECTED / EXPIRED, plus\n * the legacy INSUFFICIENT_FUNDS defense-in-depth).\n * Orchestrator routes to troubleshoot/cleanup.\n *\n * Terminal-state set is the union of `TERMINAL_STATES` from `lease-state.ts`\n * — extended from the CJS's `{CLOSED, INSUFFICIENT_FUNDS}` to also cover\n * `REJECTED` and `EXPIRED` since those are emitted by manifestjs 2.4.1\n * (chain v2.1.0) as terminal states. INSUFFICIENT_FUNDS is unreachable\n * from `decode()` on the current chain but retained as defense-in-depth.\n * See `lease-state.ts` for the divergence rationale.\n *\n * Error summary format (qa-engineer's parity pin):\n * - Lease present + terminal: `Lease ${leaseUuid} reached terminal state ${stateName || 'UNKNOWN'}`\n * - Lease missing: `deploy_app returned no lease_uuid`\n * - `connectionError` string present: passed through verbatim\n */\n\nexport interface DeployResponseShape {\n lease_uuid?: unknown;\n provider_uuid?: unknown;\n provider_url?: unknown;\n state?: unknown;\n url?: unknown;\n connection?: unknown;\n connectionError?: unknown;\n}\n\nexport interface DeployResponseClassification {\n outcome: 'active' | 'needs_wait' | 'failed';\n leaseUuid?: string;\n providerUuid?: string;\n providerUrl?: string;\n urls: string[];\n stateName?: string;\n errorSummary?: string;\n}\n\nexport function classifyDeployResponse(\n response: DeployResponseShape,\n): DeployResponseClassification {\n const stateName = decodeLeaseState(\n typeof response.state === 'number' || typeof response.state === 'string'\n ? response.state\n : undefined,\n );\n\n // URL synthesis from connection payload + optional top-level `url`.\n const urls: string[] = extractRunningEndpoints(response.connection).map(\n formatEndpointAsUrl,\n );\n if (typeof response.url === 'string' && response.url.length > 0) {\n const u = normalizeFredUrl(response.url);\n if (u.length > 0 && !urls.includes(u)) urls.unshift(u);\n }\n\n const leaseUuid =\n typeof response.lease_uuid === 'string' ? response.lease_uuid : undefined;\n\n let outcome: DeployResponseClassification['outcome'];\n if (!leaseUuid) {\n outcome = 'failed';\n } else if (\n stateName === 'LEASE_STATE_ACTIVE' &&\n (urls.length > 0 || hasRunningInstances(response.connection))\n ) {\n outcome = 'active';\n } else if (stateName !== undefined && isTerminal(stateName)) {\n outcome = 'failed';\n } else {\n // Pending / unspecified / active-without-any-running-instance / unknown.\n outcome = 'needs_wait';\n }\n\n const out: DeployResponseClassification = {\n outcome,\n ...(leaseUuid !== undefined && { leaseUuid }),\n ...(typeof response.provider_uuid === 'string' && {\n providerUuid: response.provider_uuid,\n }),\n ...(typeof response.provider_url === 'string' && {\n providerUrl: response.provider_url,\n }),\n urls,\n ...(stateName !== undefined && { stateName }),\n };\n\n if (outcome === 'failed') {\n if (typeof response.connectionError === 'string') {\n out.errorSummary = response.connectionError;\n } else if (!leaseUuid) {\n out.errorSummary = 'deploy_app returned no lease_uuid';\n } else {\n // Byte-exact format (qa-engineer's parity pin):\n // `Lease ${lease_uuid} reached terminal state ${stateName || 'UNKNOWN'}`\n out.errorSummary = `Lease ${leaseUuid} reached terminal state ${\n stateName || 'UNKNOWN'\n }`;\n }\n }\n\n return out;\n}\n"],"mappings":";;;AA8DA,SAAgB,uBACd,UAC8B;CAC9B,MAAM,YAAYA,OAChB,OAAO,SAAS,UAAU,YAAY,OAAO,SAAS,UAAU,WAC5D,SAAS,QACT,KAAA,CACN;CAGA,MAAM,OAAiB,wBAAwB,SAAS,UAAU,EAAE,IAClE,mBACF;CACA,IAAI,OAAO,SAAS,QAAQ,YAAY,SAAS,IAAI,SAAS,GAAG;EAC/D,MAAM,IAAI,iBAAiB,SAAS,GAAG;EACvC,IAAI,EAAE,SAAS,KAAK,CAAC,KAAK,SAAS,CAAC,GAAG,KAAK,QAAQ,CAAC;CACvD;CAEA,MAAM,YACJ,OAAO,SAAS,eAAe,WAAW,SAAS,aAAa,KAAA;CAElE,IAAI;CACJ,IAAI,CAAC,WACH,UAAU;MACL,IACL,cAAc,yBACb,KAAK,SAAS,KAAK,oBAAoB,SAAS,UAAU,IAE3D,UAAU;MACL,IAAI,cAAc,KAAA,KAAa,WAAW,SAAS,GACxD,UAAU;MAGV,UAAU;CAGZ,MAAM,MAAoC;EACxC;EACA,GAAI,cAAc,KAAA,KAAa,EAAE,UAAU;EAC3C,GAAI,OAAO,SAAS,kBAAkB,YAAY,EAChD,cAAc,SAAS,cACzB;EACA,GAAI,OAAO,SAAS,iBAAiB,YAAY,EAC/C,aAAa,SAAS,aACxB;EACA;EACA,GAAI,cAAc,KAAA,KAAa,EAAE,UAAU;CAC7C;CAEA,IAAI,YAAY,UACd,IAAI,OAAO,SAAS,oBAAoB,UACtC,IAAI,eAAe,SAAS;MACvB,IAAI,CAAC,WACV,IAAI,eAAe;MAInB,IAAI,eAAe,SAAS,UAAU,0BACpC,aAAa;CAKnB,OAAO;AACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection.d.ts","names":[],"sources":["../../src/internals/connection.ts"],"mappings":";;AAsBA
|
|
1
|
+
{"version":3,"file":"connection.d.ts","names":[],"sources":["../../src/internals/connection.ts"],"mappings":";;AAsBA;;;;AACe;AAGf;;;;AAU0B;AAQ1B;;;;;;;;;AAGkB;UAzBD,eAAA;EAAA,SACN,IAAI;AAAA;AAAA,UAGE,qBAAA;EAuE0C;AAK3D;;;;AAAuD;AAqBvD;;;EAvFE,MAAA,IAAU,MAAc;AAAA;AAmG1B;;;;AAAuD;AAAvD,iBA3FgB,uBAAA,CACd,UAAA,WACA,IAAA,GAAM,qBAAA,GACL,eAAe;;iBAkDF,uBAAA,CAAwB,EAAmB,EAAf,eAAe;;iBAK3C,mBAAA,CAAoB,EAAmB,EAAf,eAAe;;;;;;;;;;;;;;;;;;iBAqBvC,gBAAA,CAAiB,GAAW;;;;;;;;iBAY5B,mBAAA,CAAoB,UAAmB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection.js","names":[],"sources":["../../src/internals/connection.ts"],"sourcesContent":["/**\n * Helpers for walking the provider's `connection` payload returned by\n * `mcp__manifest-fred__deploy_app` / `app_status` / `wait_for_app_ready`.\n *\n * The provider emits instance lists in one or both of:\n * - top-level `connection.instances[]` (single-service / non-services-map shape)\n * - per-service `connection.services.<name>.instances[]` (stack /\n * services-map shape — emitted whenever the spec uses services-map form,\n * which `build_manifest_preview` does even for single-service deploys to\n * enable per-port `ingress: bool`)\n *\n * Subdomain-based routing on the provider: port is NOT part of the URL.\n * One user-facing URL per FQDN regardless of container port count.\n *\n * Unrecognized payload shape: returns `[]` and invokes the logger\n * (defaults to `console.warn`; override via `opts.logger`) so a future\n * provider-shape divergence is loud rather than silent. The CJS uses\n * `process.stderr.write`; the TS port surfaces via an injectable logger\n * to keep agent-core platform-neutral while preserving the always-loud\n * CJS posture by default.\n */\n\nexport interface RunningEndpoint {\n readonly fqdn: string;\n}\n\nexport interface ConnectionWalkOptions {\n /**\n * Sink for warnings about unrecognized connection shapes. Defaults to\n * `console.warn` (Web Standard; platform-neutral across Node, browsers,\n * Deno, Bun) so a future provider-shape divergence is loud rather than\n * silent. Host surfaces that want to route elsewhere (structured\n * stderr, UI toast, log file) can override; surfaces that want to\n * suppress entirely can pass `() => {}` explicitly — silence becomes a\n * consumer-controlled opt-out instead of the easy-to-forget default.\n */\n logger?: (reason: string) => void;\n}\n\n/**\n * Returns a deduped list of running instances (status === 'running' AND\n * `fqdn` populated) found anywhere under `connection.instances` or\n * `connection.services.<name>.instances`.\n */\nexport function extractRunningEndpoints(\n connection: unknown,\n opts: ConnectionWalkOptions = {},\n): RunningEndpoint[] {\n if (!isPlainObject(connection)) return [];\n const seen = new Set<string>();\n const endpoints: RunningEndpoint[] = [];\n\n const pushFromInstances = (instances: unknown): void => {\n if (!Array.isArray(instances)) return;\n for (const inst of instances) {\n if (!isPlainObject(inst)) continue;\n if (inst.status !== 'running') continue;\n if (typeof inst.fqdn !== 'string' || inst.fqdn.length === 0) continue;\n if (seen.has(inst.fqdn)) continue;\n seen.add(inst.fqdn);\n endpoints.push({ fqdn: inst.fqdn });\n }\n };\n\n pushFromInstances(connection.instances);\n\n const services = connection.services;\n if (isPlainObject(services)) {\n for (const svc of Object.values(services)) {\n if (isPlainObject(svc)) pushFromInstances(svc.instances);\n }\n }\n\n // Only warn when neither `instances` nor `services` is present at all.\n // The empty-but-present case (no instance has status 'running') is a\n // legitimate \"lease pending, wait_for_app_ready hasn't returned yet\" state\n // — returning [] there is the correct, non-warning behavior.\n // Object.keys + includes avoids both ES2022's `Object.hasOwn` (base\n // tsconfig targets ES2020) and biome's `noPrototypeBuiltins` rule on\n // `Object.prototype.hasOwnProperty.call`.\n const ownKeys = Object.keys(connection);\n const hasModernShape =\n ownKeys.includes('instances') || ownKeys.includes('services');\n if (!hasModernShape) {\n const keys = Object.keys(connection).slice(0, 8).join(', ') || '(empty)';\n const logger = opts.logger ?? defaultLogger;\n logger(\n `connection: unrecognized shape (no 'instances' or 'services' key found; keys present: ${keys}). ` +\n 'Returning empty endpoints — the orchestrator will report no ingresses for this lease. ' +\n 'Provider may have shipped a new shape; check manifest-mcp-fred ConnectionDetails.',\n );\n }\n\n return endpoints;\n}\n\n/** Render an endpoint as a bare FQDN string (for ingress lists). */\nexport function formatEndpointAsIngress(ep: RunningEndpoint): string {\n return ep.fqdn;\n}\n\n/** Render an endpoint as a full `https://<fqdn>/` URL. */\nexport function formatEndpointAsUrl(ep: RunningEndpoint): string {\n return `https://${ep.fqdn}/`;\n}\n\n/**\n * Normalize fred's top-level `url` field to a full `http(s)://...`\n * string. Defensive fallback for the legacy `connection.host` / `ports`\n * shape: fred surfaces a top-level `url` when no `connection.instances`\n * FQDN is available, and the value may or may not carry a scheme.\n *\n * Mirrors the inline logic that lived in three call sites\n * (`classify-deploy-response.ts:76-80`, `format-success.ts` ingress\n * fallback, `deploy-app.ts` `DeployResult.urls` fallback) — factored\n * here so all three share one source of truth.\n *\n * - Returns `''` for empty input (caller branches into a different\n * render path if needed).\n * - Passes through unchanged if already prefixed `http://` or\n * `https://` (case-insensitive).\n * - Otherwise wraps as `https://${raw}/`.\n */\nexport function normalizeFredUrl(raw: string): string {\n if (raw.length === 0) return '';\n return /^https?:\\/\\//i.test(raw) ? raw : `https://${raw}/`;\n}\n\n/**\n * True iff any instance anywhere in the connection payload has\n * `status === 'running'`, regardless of `fqdn`. Used by\n * `classify-deploy-response.ts` to recognize internal-only deploys\n * (every port `ingress: false`) as `active` rather than misclassifying\n * them as `needs_wait` because they have no public URLs to surface.\n */\nexport function hasRunningInstances(connection: unknown): boolean {\n if (!isPlainObject(connection)) return false;\n const runs = (instances: unknown): boolean =>\n Array.isArray(instances) &&\n instances.some((i) => isPlainObject(i) && i.status === 'running');\n if (runs(connection.instances)) return true;\n const services = connection.services;\n if (isPlainObject(services)) {\n for (const svc of Object.values(services)) {\n if (isPlainObject(svc) && runs(svc.instances)) return true;\n }\n }\n return false;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n\n/**\n * Default logger — `console.warn`. Defined as a module-level constant so\n * test code can spy on it (`vi.spyOn(console, 'warn')`) without races\n * around the import order of the binding.\n */\nconst defaultLogger: (reason: string) => void = (reason) => {\n console.warn(reason);\n};\n"],"mappings":";;;;;;AA4CA,SAAgB,wBACd,YACA,OAA8B,
|
|
1
|
+
{"version":3,"file":"connection.js","names":[],"sources":["../../src/internals/connection.ts"],"sourcesContent":["/**\n * Helpers for walking the provider's `connection` payload returned by\n * `mcp__manifest-fred__deploy_app` / `app_status` / `wait_for_app_ready`.\n *\n * The provider emits instance lists in one or both of:\n * - top-level `connection.instances[]` (single-service / non-services-map shape)\n * - per-service `connection.services.<name>.instances[]` (stack /\n * services-map shape — emitted whenever the spec uses services-map form,\n * which `build_manifest_preview` does even for single-service deploys to\n * enable per-port `ingress: bool`)\n *\n * Subdomain-based routing on the provider: port is NOT part of the URL.\n * One user-facing URL per FQDN regardless of container port count.\n *\n * Unrecognized payload shape: returns `[]` and invokes the logger\n * (defaults to `console.warn`; override via `opts.logger`) so a future\n * provider-shape divergence is loud rather than silent. The CJS uses\n * `process.stderr.write`; the TS port surfaces via an injectable logger\n * to keep agent-core platform-neutral while preserving the always-loud\n * CJS posture by default.\n */\n\nexport interface RunningEndpoint {\n readonly fqdn: string;\n}\n\nexport interface ConnectionWalkOptions {\n /**\n * Sink for warnings about unrecognized connection shapes. Defaults to\n * `console.warn` (Web Standard; platform-neutral across Node, browsers,\n * Deno, Bun) so a future provider-shape divergence is loud rather than\n * silent. Host surfaces that want to route elsewhere (structured\n * stderr, UI toast, log file) can override; surfaces that want to\n * suppress entirely can pass `() => {}` explicitly — silence becomes a\n * consumer-controlled opt-out instead of the easy-to-forget default.\n */\n logger?: (reason: string) => void;\n}\n\n/**\n * Returns a deduped list of running instances (status === 'running' AND\n * `fqdn` populated) found anywhere under `connection.instances` or\n * `connection.services.<name>.instances`.\n */\nexport function extractRunningEndpoints(\n connection: unknown,\n opts: ConnectionWalkOptions = {},\n): RunningEndpoint[] {\n if (!isPlainObject(connection)) return [];\n const seen = new Set<string>();\n const endpoints: RunningEndpoint[] = [];\n\n const pushFromInstances = (instances: unknown): void => {\n if (!Array.isArray(instances)) return;\n for (const inst of instances) {\n if (!isPlainObject(inst)) continue;\n if (inst.status !== 'running') continue;\n if (typeof inst.fqdn !== 'string' || inst.fqdn.length === 0) continue;\n if (seen.has(inst.fqdn)) continue;\n seen.add(inst.fqdn);\n endpoints.push({ fqdn: inst.fqdn });\n }\n };\n\n pushFromInstances(connection.instances);\n\n const services = connection.services;\n if (isPlainObject(services)) {\n for (const svc of Object.values(services)) {\n if (isPlainObject(svc)) pushFromInstances(svc.instances);\n }\n }\n\n // Only warn when neither `instances` nor `services` is present at all.\n // The empty-but-present case (no instance has status 'running') is a\n // legitimate \"lease pending, wait_for_app_ready hasn't returned yet\" state\n // — returning [] there is the correct, non-warning behavior.\n // Object.keys + includes avoids both ES2022's `Object.hasOwn` (base\n // tsconfig targets ES2020) and biome's `noPrototypeBuiltins` rule on\n // `Object.prototype.hasOwnProperty.call`.\n const ownKeys = Object.keys(connection);\n const hasModernShape =\n ownKeys.includes('instances') || ownKeys.includes('services');\n if (!hasModernShape) {\n const keys = Object.keys(connection).slice(0, 8).join(', ') || '(empty)';\n const logger = opts.logger ?? defaultLogger;\n logger(\n `connection: unrecognized shape (no 'instances' or 'services' key found; keys present: ${keys}). ` +\n 'Returning empty endpoints — the orchestrator will report no ingresses for this lease. ' +\n 'Provider may have shipped a new shape; check manifest-mcp-fred ConnectionDetails.',\n );\n }\n\n return endpoints;\n}\n\n/** Render an endpoint as a bare FQDN string (for ingress lists). */\nexport function formatEndpointAsIngress(ep: RunningEndpoint): string {\n return ep.fqdn;\n}\n\n/** Render an endpoint as a full `https://<fqdn>/` URL. */\nexport function formatEndpointAsUrl(ep: RunningEndpoint): string {\n return `https://${ep.fqdn}/`;\n}\n\n/**\n * Normalize fred's top-level `url` field to a full `http(s)://...`\n * string. Defensive fallback for the legacy `connection.host` / `ports`\n * shape: fred surfaces a top-level `url` when no `connection.instances`\n * FQDN is available, and the value may or may not carry a scheme.\n *\n * Mirrors the inline logic that lived in three call sites\n * (`classify-deploy-response.ts:76-80`, `format-success.ts` ingress\n * fallback, `deploy-app.ts` `DeployResult.urls` fallback) — factored\n * here so all three share one source of truth.\n *\n * - Returns `''` for empty input (caller branches into a different\n * render path if needed).\n * - Passes through unchanged if already prefixed `http://` or\n * `https://` (case-insensitive).\n * - Otherwise wraps as `https://${raw}/`.\n */\nexport function normalizeFredUrl(raw: string): string {\n if (raw.length === 0) return '';\n return /^https?:\\/\\//i.test(raw) ? raw : `https://${raw}/`;\n}\n\n/**\n * True iff any instance anywhere in the connection payload has\n * `status === 'running'`, regardless of `fqdn`. Used by\n * `classify-deploy-response.ts` to recognize internal-only deploys\n * (every port `ingress: false`) as `active` rather than misclassifying\n * them as `needs_wait` because they have no public URLs to surface.\n */\nexport function hasRunningInstances(connection: unknown): boolean {\n if (!isPlainObject(connection)) return false;\n const runs = (instances: unknown): boolean =>\n Array.isArray(instances) &&\n instances.some((i) => isPlainObject(i) && i.status === 'running');\n if (runs(connection.instances)) return true;\n const services = connection.services;\n if (isPlainObject(services)) {\n for (const svc of Object.values(services)) {\n if (isPlainObject(svc) && runs(svc.instances)) return true;\n }\n }\n return false;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n\n/**\n * Default logger — `console.warn`. Defined as a module-level constant so\n * test code can spy on it (`vi.spyOn(console, 'warn')`) without races\n * around the import order of the binding.\n */\nconst defaultLogger: (reason: string) => void = (reason) => {\n console.warn(reason);\n};\n"],"mappings":";;;;;;AA4CA,SAAgB,wBACd,YACA,OAA8B,CAAC,GACZ;CACnB,IAAI,CAAC,cAAc,UAAU,GAAG,OAAO,CAAC;CACxC,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,YAA+B,CAAC;CAEtC,MAAM,qBAAqB,cAA6B;EACtD,IAAI,CAAC,MAAM,QAAQ,SAAS,GAAG;EAC/B,KAAK,MAAM,QAAQ,WAAW;GAC5B,IAAI,CAAC,cAAc,IAAI,GAAG;GAC1B,IAAI,KAAK,WAAW,WAAW;GAC/B,IAAI,OAAO,KAAK,SAAS,YAAY,KAAK,KAAK,WAAW,GAAG;GAC7D,IAAI,KAAK,IAAI,KAAK,IAAI,GAAG;GACzB,KAAK,IAAI,KAAK,IAAI;GAClB,UAAU,KAAK,EAAE,MAAM,KAAK,KAAK,CAAC;EACpC;CACF;CAEA,kBAAkB,WAAW,SAAS;CAEtC,MAAM,WAAW,WAAW;CAC5B,IAAI,cAAc,QAAQ;OACnB,MAAM,OAAO,OAAO,OAAO,QAAQ,GACtC,IAAI,cAAc,GAAG,GAAG,kBAAkB,IAAI,SAAS;CAAA;CAW3D,MAAM,UAAU,OAAO,KAAK,UAAU;CAGtC,IAAI,EADF,QAAQ,SAAS,WAAW,KAAK,QAAQ,SAAS,UAAU,IACzC;EACnB,MAAM,OAAO,OAAO,KAAK,UAAU,EAAE,MAAM,GAAG,CAAC,EAAE,KAAK,IAAI,KAAK;EAE/D,CADe,KAAK,UAAU,eAE5B,yFAAyF,KAAK,2KAGhG;CACF;CAEA,OAAO;AACT;;AAGA,SAAgB,wBAAwB,IAA6B;CACnE,OAAO,GAAG;AACZ;;AAGA,SAAgB,oBAAoB,IAA6B;CAC/D,OAAO,WAAW,GAAG,KAAK;AAC5B;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,iBAAiB,KAAqB;CACpD,IAAI,IAAI,WAAW,GAAG,OAAO;CAC7B,OAAO,gBAAgB,KAAK,GAAG,IAAI,MAAM,WAAW,IAAI;AAC1D;;;;;;;;AASA,SAAgB,oBAAoB,YAA8B;CAChE,IAAI,CAAC,cAAc,UAAU,GAAG,OAAO;CACvC,MAAM,QAAQ,cACZ,MAAM,QAAQ,SAAS,KACvB,UAAU,MAAM,MAAM,cAAc,CAAC,KAAK,EAAE,WAAW,SAAS;CAClE,IAAI,KAAK,WAAW,SAAS,GAAG,OAAO;CACvC,MAAM,WAAW,WAAW;CAC5B,IAAI,cAAc,QAAQ;OACnB,MAAM,OAAO,OAAO,OAAO,QAAQ,GACtC,IAAI,cAAc,GAAG,KAAK,KAAK,IAAI,SAAS,GAAG,OAAO;CAAA;CAG1D,OAAO;AACT;AAEA,SAAS,cAAc,OAAkD;CACvE,OAAO,UAAU,QAAQ,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK;AAC5E;;;;;;AAOA,MAAM,iBAA2C,WAAW;CAC1D,QAAQ,KAAK,MAAM;AACrB"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { DenomMap, Readiness } from "../types.js";
|
|
2
|
+
import { CheckDeploymentReadinessResult } from "@manifest-network/manifest-mcp-fred";
|
|
3
|
+
|
|
4
|
+
//#region src/internals/evaluate-readiness-from-fred.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Translate fred's snake_case `CheckDeploymentReadinessResult` into
|
|
7
|
+
* the canonical `EvaluateReadinessInputs` (camelCase + context) and
|
|
8
|
+
* invoke `evaluateReadiness`. Returns the typed `Readiness` verdict
|
|
9
|
+
* the orchestrator gates on (`status === 'block'` → INVALID_CONFIG).
|
|
10
|
+
*
|
|
11
|
+
* @param raw fred's wire response (snake_case, readonly).
|
|
12
|
+
* @param gasPrice Gas-price string from `clientManager.getConfig().gasPrice`
|
|
13
|
+
* (e.g. `'1umfx'`). Required by the evaluator's
|
|
14
|
+
* wallet-gas check; defaulted upstream when absent.
|
|
15
|
+
* @param denomMap Pre-loaded `DenomMap` for humanization. Pass
|
|
16
|
+
* `EMPTY_DENOM_MAP` when no chain-data file is
|
|
17
|
+
* configured.
|
|
18
|
+
* @param tenantAddress Canonical tenant address from the orchestrator's
|
|
19
|
+
* address-source consistency guard. PREFERRED over
|
|
20
|
+
* `raw.tenant` so a fred response whose `tenant`
|
|
21
|
+
* differs (configuration drift / replayed mock)
|
|
22
|
+
* does NOT silently route the verdict against a
|
|
23
|
+
* different wallet.
|
|
24
|
+
*/
|
|
25
|
+
declare function evaluateReadinessFromFredResponse(raw: CheckDeploymentReadinessResult, gasPrice: string, denomMap: DenomMap, tenantAddress: string): Readiness;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { evaluateReadinessFromFredResponse };
|
|
28
|
+
//# sourceMappingURL=evaluate-readiness-from-fred.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evaluate-readiness-from-fred.d.ts","names":[],"sources":["../../src/internals/evaluate-readiness-from-fred.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;iBAuEgB,iCAAA,CACd,GAAA,EAAK,8BAAA,EACL,QAAA,UACA,QAAA,EAAU,QAAA,EACV,aAAA,WACC,SAAA"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { evaluateReadiness } from "./evaluate-readiness.js";
|
|
2
|
+
//#region src/internals/evaluate-readiness-from-fred.ts
|
|
3
|
+
/**
|
|
4
|
+
* Translate fred's snake_case `CheckDeploymentReadinessResult` into
|
|
5
|
+
* the canonical `EvaluateReadinessInputs` (camelCase + context) and
|
|
6
|
+
* invoke `evaluateReadiness`. Returns the typed `Readiness` verdict
|
|
7
|
+
* the orchestrator gates on (`status === 'block'` → INVALID_CONFIG).
|
|
8
|
+
*
|
|
9
|
+
* @param raw fred's wire response (snake_case, readonly).
|
|
10
|
+
* @param gasPrice Gas-price string from `clientManager.getConfig().gasPrice`
|
|
11
|
+
* (e.g. `'1umfx'`). Required by the evaluator's
|
|
12
|
+
* wallet-gas check; defaulted upstream when absent.
|
|
13
|
+
* @param denomMap Pre-loaded `DenomMap` for humanization. Pass
|
|
14
|
+
* `EMPTY_DENOM_MAP` when no chain-data file is
|
|
15
|
+
* configured.
|
|
16
|
+
* @param tenantAddress Canonical tenant address from the orchestrator's
|
|
17
|
+
* address-source consistency guard. PREFERRED over
|
|
18
|
+
* `raw.tenant` so a fred response whose `tenant`
|
|
19
|
+
* differs (configuration drift / replayed mock)
|
|
20
|
+
* does NOT silently route the verdict against a
|
|
21
|
+
* different wallet.
|
|
22
|
+
*/
|
|
23
|
+
function evaluateReadinessFromFredResponse(raw, gasPrice, denomMap, tenantAddress) {
|
|
24
|
+
const skuNames = new Set(raw.available_sku_names);
|
|
25
|
+
if (raw.sku !== null) skuNames.add(raw.sku.name);
|
|
26
|
+
return evaluateReadiness({
|
|
27
|
+
tenant: tenantAddress,
|
|
28
|
+
image: raw.image,
|
|
29
|
+
size: raw.size,
|
|
30
|
+
walletBalances: toCoinArray(raw.wallet_balances),
|
|
31
|
+
credits: translateCredits(raw),
|
|
32
|
+
sku: translateSku(raw.sku),
|
|
33
|
+
availableSkuNames: [...skuNames],
|
|
34
|
+
gasPrice,
|
|
35
|
+
denomMap
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Translate fred's `credits` object + top-level `current_balance` /
|
|
40
|
+
* `hours_remaining` into the evaluator's nested `credits` input shape.
|
|
41
|
+
*
|
|
42
|
+
* Null preservation: when fred returns `credits: null`, the translator
|
|
43
|
+
* returns null even if `current_balance` / `hours_remaining` are
|
|
44
|
+
* present at the top level — synthesizing a credits object from the
|
|
45
|
+
* stray fields would suppress the "no credit account funded" warn
|
|
46
|
+
* rule the evaluator owns.
|
|
47
|
+
*/
|
|
48
|
+
function translateCredits(raw) {
|
|
49
|
+
if (raw.credits === null) return null;
|
|
50
|
+
const out = {};
|
|
51
|
+
if (Array.isArray(raw.credits.available_balances)) out.availableBalances = toCoinArray(raw.credits.available_balances);
|
|
52
|
+
if (Array.isArray(raw.credits.balances)) out.balances = toCoinArray(raw.credits.balances);
|
|
53
|
+
if (raw.current_balance !== void 0) out.currentBalance = toCoinArray(raw.current_balance);
|
|
54
|
+
if (raw.hours_remaining !== void 0) out.hoursRemaining = raw.hours_remaining;
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Translate fred's `SkuSummary | null` into the evaluator's
|
|
59
|
+
* `{ name: string; price: Coin } | null`.
|
|
60
|
+
*
|
|
61
|
+
* Drops fred-only fields (`uuid`, `provider_uuid`, `active`). Coerces
|
|
62
|
+
* a price-less SKU to `null` — the evaluator's `sku.price` is required
|
|
63
|
+
* (`Coin`), so an SKU without price is structurally invalid input.
|
|
64
|
+
* Without this coercion the evaluator would treat the SKU as truthy
|
|
65
|
+
* and crash accessing `price.amount`.
|
|
66
|
+
*/
|
|
67
|
+
function translateSku(sku) {
|
|
68
|
+
if (sku === null) return null;
|
|
69
|
+
if (sku.price === void 0) return null;
|
|
70
|
+
return {
|
|
71
|
+
name: sku.name,
|
|
72
|
+
price: {
|
|
73
|
+
denom: sku.price.denom,
|
|
74
|
+
amount: sku.price.amount
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Spread a readonly Coin-shaped array into a mutable Coin[]. The
|
|
80
|
+
* evaluator's `EvaluateReadinessInputs.walletBalances` / credit-balance
|
|
81
|
+
* arrays are mutable; fred's wire shapes are `ReadonlyArray<...>`. A
|
|
82
|
+
* shallow copy is sufficient — each element is a frozen-ish `{denom,
|
|
83
|
+
* amount}` value tuple, never mutated by the evaluator.
|
|
84
|
+
*/
|
|
85
|
+
function toCoinArray(arr) {
|
|
86
|
+
return arr.map((c) => ({
|
|
87
|
+
denom: c.denom,
|
|
88
|
+
amount: c.amount
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
export { evaluateReadinessFromFredResponse };
|
|
93
|
+
|
|
94
|
+
//# sourceMappingURL=evaluate-readiness-from-fred.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evaluate-readiness-from-fred.js","names":[],"sources":["../../src/internals/evaluate-readiness-from-fred.ts"],"sourcesContent":["/**\n * Translator: fred's `CheckDeploymentReadinessResult` (snake_case wire\n * shape) → canonical `evaluateReadiness`'s `EvaluateReadinessInputs`\n * (camelCase + deploy-app context). ENG-185 sub-PR B, item 1.\n *\n * Replaces the always-`'ok'` stub `evaluateReadinessFromRaw` previously\n * inlined in `deploy-app.ts`. With this translator wired in, the\n * `status === 'block'` short-circuit at BOTH call sites\n * (`deploy-app.ts` L207 initial-spec + L327 post-edit recall) fires\n * correctly, killing the silent \"always proceed\" path the stub kept open.\n *\n * Three concerns the translator owns:\n *\n * 1. **Field renames** — snake_case → camelCase across all 9 fred\n * top-level fields the evaluator consumes (`wallet_balances` →\n * `walletBalances`, `available_sku_names` → `availableSkuNames`,\n * `credits.available_balances` → `credits.availableBalances`, etc.).\n *\n * 2. **Folding top-level → nested** — fred's `getBalance` emits\n * `current_balance` and `hours_remaining` ALONGSIDE `credits`\n * (top-level on the response); the evaluator's input nests them\n * INSIDE `credits` as `currentBalance` / `hoursRemaining`. The\n * translator moves them across the boundary.\n *\n * Guard: when fred returns `credits: null`, the translator\n * preserves null — synthesizing a credits object from the stray\n * top-level fields would bypass the evaluator's\n * `credits === null` warn rule (\"No credit account funded for\n * compute leases\").\n *\n * 3. **Context injection** — `gasPrice`, `denomMap`, and `tenant`\n * come from the orchestrator's scope (not from fred). For\n * `tenant`, the translator deliberately IGNORES `raw.tenant` and\n * uses the `tenantAddress` arg: the orchestrator already resolved\n * and validated the canonical wallet/client address via the\n * address-source consistency guard (deploy-app.ts L154-161).\n *\n * Also: drops fred sku fields `uuid` / `provider_uuid` / `active` (the\n * evaluator only needs `name` + `price`), and coerces a price-less\n * SKU to `null` — `EvaluateReadinessInputs.sku` requires `price: Coin`,\n * so an SKU without price is structurally not a valid input.\n */\n\nimport type { CheckDeploymentReadinessResult } from '@manifest-network/manifest-mcp-fred';\nimport type { Coin, Readiness } from '../types.js';\nimport {\n type EvaluateReadinessInputs,\n evaluateReadiness,\n} from './evaluate-readiness.js';\nimport type { DenomMap } from './humanize-denom.js';\n\n/**\n * Translate fred's snake_case `CheckDeploymentReadinessResult` into\n * the canonical `EvaluateReadinessInputs` (camelCase + context) and\n * invoke `evaluateReadiness`. Returns the typed `Readiness` verdict\n * the orchestrator gates on (`status === 'block'` → INVALID_CONFIG).\n *\n * @param raw fred's wire response (snake_case, readonly).\n * @param gasPrice Gas-price string from `clientManager.getConfig().gasPrice`\n * (e.g. `'1umfx'`). Required by the evaluator's\n * wallet-gas check; defaulted upstream when absent.\n * @param denomMap Pre-loaded `DenomMap` for humanization. Pass\n * `EMPTY_DENOM_MAP` when no chain-data file is\n * configured.\n * @param tenantAddress Canonical tenant address from the orchestrator's\n * address-source consistency guard. PREFERRED over\n * `raw.tenant` so a fred response whose `tenant`\n * differs (configuration drift / replayed mock)\n * does NOT silently route the verdict against a\n * different wallet.\n */\nexport function evaluateReadinessFromFredResponse(\n raw: CheckDeploymentReadinessResult,\n gasPrice: string,\n denomMap: DenomMap,\n tenantAddress: string,\n): Readiness {\n // Union `raw.sku.name` into the names list (Copilot #3319670583).\n // Fred caps `available_sku_names` at `MAX_SKU_NAMES_RETURNED = 50`\n // (`packages/fred/src/tools/checkDeploymentReadiness.ts`); when the\n // chain has >50 SKUs and the user's requested size falls past the\n // slice, the evaluator's SKU-availability rule false-blocks even\n // though fred already resolved the SKU into `raw.sku`. Folding\n // `raw.sku.name` into the set closes the gap. Set handles dedupe\n // (`raw.sku.name` may already be in the first-50 list).\n const skuNames = new Set(raw.available_sku_names);\n if (raw.sku !== null) skuNames.add(raw.sku.name);\n\n return evaluateReadiness({\n tenant: tenantAddress,\n image: raw.image,\n size: raw.size,\n walletBalances: toCoinArray(raw.wallet_balances),\n credits: translateCredits(raw),\n sku: translateSku(raw.sku),\n availableSkuNames: [...skuNames],\n gasPrice,\n denomMap,\n });\n}\n\n/**\n * Translate fred's `credits` object + top-level `current_balance` /\n * `hours_remaining` into the evaluator's nested `credits` input shape.\n *\n * Null preservation: when fred returns `credits: null`, the translator\n * returns null even if `current_balance` / `hours_remaining` are\n * present at the top level — synthesizing a credits object from the\n * stray fields would suppress the \"no credit account funded\" warn\n * rule the evaluator owns.\n */\nfunction translateCredits(\n raw: CheckDeploymentReadinessResult,\n): EvaluateReadinessInputs['credits'] {\n if (raw.credits === null) return null;\n // Defensive emission: only write a field when fred actually supplied\n // it. Fred's `CheckDeploymentReadinessResult` declares `balances` /\n // `available_balances` as required, but the evaluator's input shape\n // accepts both as OPTIONAL — and skipping the field on absent input\n // is preferable to surfacing a `.map of undefined` crash if a mock or\n // upstream variant elides the field. The evaluator's source-of-truth\n // precedence (availableBalances → balances → currentBalance → []) is\n // already CJS-parity-correct for partial credits objects.\n const out: NonNullable<EvaluateReadinessInputs['credits']> = {};\n if (Array.isArray(raw.credits.available_balances)) {\n out.availableBalances = toCoinArray(raw.credits.available_balances);\n }\n if (Array.isArray(raw.credits.balances)) {\n out.balances = toCoinArray(raw.credits.balances);\n }\n if (raw.current_balance !== undefined) {\n out.currentBalance = toCoinArray(raw.current_balance);\n }\n if (raw.hours_remaining !== undefined) {\n out.hoursRemaining = raw.hours_remaining;\n }\n return out;\n}\n\n/**\n * Translate fred's `SkuSummary | null` into the evaluator's\n * `{ name: string; price: Coin } | null`.\n *\n * Drops fred-only fields (`uuid`, `provider_uuid`, `active`). Coerces\n * a price-less SKU to `null` — the evaluator's `sku.price` is required\n * (`Coin`), so an SKU without price is structurally invalid input.\n * Without this coercion the evaluator would treat the SKU as truthy\n * and crash accessing `price.amount`.\n */\nfunction translateSku(\n sku: CheckDeploymentReadinessResult['sku'],\n): EvaluateReadinessInputs['sku'] {\n if (sku === null) return null;\n if (sku.price === undefined) return null;\n return {\n name: sku.name,\n price: { denom: sku.price.denom, amount: sku.price.amount },\n };\n}\n\n/**\n * Spread a readonly Coin-shaped array into a mutable Coin[]. The\n * evaluator's `EvaluateReadinessInputs.walletBalances` / credit-balance\n * arrays are mutable; fred's wire shapes are `ReadonlyArray<...>`. A\n * shallow copy is sufficient — each element is a frozen-ish `{denom,\n * amount}` value tuple, never mutated by the evaluator.\n */\nfunction toCoinArray(\n arr: ReadonlyArray<{ readonly denom: string; readonly amount: string }>,\n): Coin[] {\n return arr.map((c) => ({ denom: c.denom, amount: c.amount }));\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAuEA,SAAgB,kCACd,KACA,UACA,UACA,eACW;CASX,MAAM,WAAW,IAAI,IAAI,IAAI,mBAAmB;CAChD,IAAI,IAAI,QAAQ,MAAM,SAAS,IAAI,IAAI,IAAI,IAAI;CAE/C,OAAO,kBAAkB;EACvB,QAAQ;EACR,OAAO,IAAI;EACX,MAAM,IAAI;EACV,gBAAgB,YAAY,IAAI,eAAe;EAC/C,SAAS,iBAAiB,GAAG;EAC7B,KAAK,aAAa,IAAI,GAAG;EACzB,mBAAmB,CAAC,GAAG,QAAQ;EAC/B;EACA;CACF,CAAC;AACH;;;;;;;;;;;AAYA,SAAS,iBACP,KACoC;CACpC,IAAI,IAAI,YAAY,MAAM,OAAO;CASjC,MAAM,MAAuD,CAAC;CAC9D,IAAI,MAAM,QAAQ,IAAI,QAAQ,kBAAkB,GAC9C,IAAI,oBAAoB,YAAY,IAAI,QAAQ,kBAAkB;CAEpE,IAAI,MAAM,QAAQ,IAAI,QAAQ,QAAQ,GACpC,IAAI,WAAW,YAAY,IAAI,QAAQ,QAAQ;CAEjD,IAAI,IAAI,oBAAoB,KAAA,GAC1B,IAAI,iBAAiB,YAAY,IAAI,eAAe;CAEtD,IAAI,IAAI,oBAAoB,KAAA,GAC1B,IAAI,iBAAiB,IAAI;CAE3B,OAAO;AACT;;;;;;;;;;;AAYA,SAAS,aACP,KACgC;CAChC,IAAI,QAAQ,MAAM,OAAO;CACzB,IAAI,IAAI,UAAU,KAAA,GAAW,OAAO;CACpC,OAAO;EACL,MAAM,IAAI;EACV,OAAO;GAAE,OAAO,IAAI,MAAM;GAAO,QAAQ,IAAI,MAAM;EAAO;CAC5D;AACF;;;;;;;;AASA,SAAS,YACP,KACQ;CACR,OAAO,IAAI,KAAK,OAAO;EAAE,OAAO,EAAE;EAAO,QAAQ,EAAE;CAAO,EAAE;AAC9D"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"evaluate-readiness.d.ts","names":[],"sources":["../../src/internals/evaluate-readiness.ts"],"mappings":";;;;AAwDA;;UAAiB,uBAAA;EAQC;EANhB,MAAA;EAWa;EATb,KAAA;EAgB4B;EAd5B,IAAA;EA+BmB;EA7BnB,cAAA,EAAgB,IAAA;EANhB;EAQA,OAAA;IACE,iBAAA,GAAoB,IAAA,IAHtB;IAKE,QAAA,GAAW,IAAA,IAHb;IAKE,cAAA,GAAiB,IAAA,IAJG;IAMpB,cAAA;EAAA;EAFA;EAKF,GAAA;IAAO,IAAA;IAAc,KAAA,EAAO,IAAA;EAAA;EAAP;EAErB,iBAAA;EAAA;EAEA,QAAA;EAEA;EAAA,YAAA;EAWW
|
|
1
|
+
{"version":3,"file":"evaluate-readiness.d.ts","names":[],"sources":["../../src/internals/evaluate-readiness.ts"],"mappings":";;;;AAwDA;;UAAiB,uBAAA;EAQC;EANhB,MAAA;EAWa;EATb,KAAA;EAgB4B;EAd5B,IAAA;EA+BmB;EA7BnB,cAAA,EAAgB,IAAA;EANhB;EAQA,OAAA;IACE,iBAAA,GAAoB,IAAA,IAHtB;IAKE,QAAA,GAAW,IAAA,IAHb;IAKE,cAAA,GAAiB,IAAA,IAJG;IAMpB,cAAA;EAAA;EAFA;EAKF,GAAA;IAAO,IAAA;IAAc,KAAA,EAAO,IAAA;EAAA;EAAP;EAErB,iBAAA;EAAA;EAEA,QAAA;EAEA;EAAA,YAAA;EAWW;;AAAQ;AASrB;;;;;;;EATE,QAAA,GAAW,QAAA;AAAA;;;;;;;iBASG,iBAAA,CAAkB,MAAA,EAAQ,uBAAA,GAA0B,SAAS"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"evaluate-readiness.js","names":[],"sources":["../../src/internals/evaluate-readiness.ts"],"sourcesContent":["import type { Coin, Readiness, ReadinessAction } from '../types.js';\nimport {\n type DenomMap,\n denomToSymbol,\n EMPTY_DENOM_MAP,\n humanizeCoin,\n} from './humanize-denom.js';\n\n/**\n * Evaluate `check_deployment_readiness` MCP response data into the frozen\n * `Readiness` shape (camelCase typed input + the `Readiness` contract from\n * ENG-128).\n *\n * Thresholds are encoded here (not in skill prose or caller config) so the\n * rules stay consistent across runs:\n * - HOURS_REMAINING_WARN_FLOOR = 24\n * - GAS_BALANCE_WARN_FLOOR (per-denom) = 50_000n umfx | upwr\n *\n * Status semantics (CJS-parity):\n * - `'block'` — cannot proceed (SKU unavailable, wallet empty)\n * - `'warn'` — proceedable but risky (low credits, low gas balance, no credit account)\n * - `'ok'` — silent pass\n *\n * `suggestedActions` are semantic tokens from the frozen `ReadinessAction`\n * union — not prose for the user. Surfaces map these to UI affordances.\n *\n * Walked-from-CJS field-rename: the MCP response uses snake_case\n * (`wallet_balances`, `available_balances`, `hours_remaining`,\n * `available_sku_names`, `current_balance`); the TS-port input is\n * camelCase, and high-level callers (PR 3's `deployApp`) translate the\n * snake_case wire shape into camelCase before passing in.\n */\n\nconst HOURS_REMAINING_WARN_FLOOR = 24;\n\n// Per-denom warn floors for low gas balance (in smallest unit). Mirrors the\n// CJS values: 50_000 umfx = 0.05 MFX (1 MFX = 1,000,000 umfx); comparable\n// headroom for upwr.\nconst GAS_BALANCE_WARN_FLOOR_DEFAULTS: Readonly<Record<string, bigint>> = {\n umfx: 50_000n,\n upwr: 50_000n,\n};\nconst GAS_BALANCE_WARN_FLOOR_FALLBACK = 50_000n;\n\n/**\n * Cosmos convention for gas-price strings: leading numeric (digits +\n * optional decimal point), then the denom. Denom grammar mirrors\n * `sdk.ValidateDenom`: `[a-zA-Z][a-zA-Z0-9/:._-]{2,127}`.\n * Anchored both ends so trailing whitespace fails fast.\n */\nconst GAS_PRICE_RE = /^[0-9]+(?:\\.[0-9]+)?([a-zA-Z][a-zA-Z0-9/:._-]{2,127})$/;\n\n/**\n * Inputs passed to `evaluateReadiness`. camelCase throughout — high-level\n * callers translate the snake_case MCP response shape before invocation.\n */\nexport interface EvaluateReadinessInputs {\n /** Tenant address (bech32). Not consumed by the algorithm; included for journal/log context. */\n tenant: string;\n /** Image ref being considered (may be `null` when only the size is selected). */\n image: string | null;\n /** SKU size string the caller wants (`'docker-micro'`, etc.). `null` when not yet chosen. */\n size: string | null;\n /** Wallet bank balances. */\n walletBalances: Coin[];\n /** Credit account data, or `null` when no credit account is funded. */\n credits: {\n availableBalances?: Coin[];\n /** Older response variant — fallback when `availableBalances` is absent. */\n balances?: Coin[];\n /** Live current credit balance(s) when the tenant has at least one active lease. */\n currentBalance?: Coin[];\n /** Hours of runtime at the user's current overall burn rate (string-encoded number). */\n hoursRemaining?: string;\n } | null;\n /** Chosen SKU + price, or `null` when no size selected. */\n sku: { name: string; price: Coin } | null;\n /** All active SKU names the chain currently advertises. */\n availableSkuNames: string[];\n /** Gas-price string (e.g. `'1umfx'`, `'0.37upwr'`). Required — drives the wallet-gas check denom. */\n gasPrice: string;\n /** Override the per-denom warn floor (smallest unit). When omitted, uses the per-denom default or 50_000n fallback. */\n gasWarnFloor?: bigint;\n /**\n * Pre-loaded `DenomMap` for symbol humanization. The orchestrator\n * (`deploy-app.ts` and PR-4 callers) is responsible for composing the\n * map via `loadChainDenomMap(chainDataFile)` and passing it in. This\n * keeps `evaluateReadiness` pure-sync — I/O lives at the orchestrator\n * boundary, not inside the decision function (post-Q4 Bii verdict).\n *\n * When omitted, the no-op map is used; balances + SKU prices render\n * with raw on-chain denoms.\n */\n denomMap?: DenomMap;\n}\n\n/**\n * Compute the `Readiness` verdict for a prospective deployment.\n *\n * Throws `TypeError` on malformed `gasPrice` (the only input field whose\n * runtime shape isn't enforced by the typed signature).\n */\nexport function evaluateReadiness(inputs: EvaluateReadinessInputs): Readiness {\n // --- Parse + validate gasPrice via String.match (avoids RegExp.exec) ---\n const gasDenomMatch = inputs.gasPrice.match(GAS_PRICE_RE);\n if (!gasDenomMatch || gasDenomMatch[1] === undefined) {\n throw new TypeError(\n `evaluateReadiness: gasPrice must match <numeric><denom> (e.g. \"1umfx\" or \"0.37upwr\"); got \"${inputs.gasPrice}\"`,\n );\n }\n const gasDenom = gasDenomMatch[1];\n\n // --- Resolve gas warn floor ---\n const gasWarnFloor =\n inputs.gasWarnFloor !== undefined\n ? validateGasWarnFloor(inputs.gasWarnFloor)\n : (GAS_BALANCE_WARN_FLOOR_DEFAULTS[gasDenom] ??\n GAS_BALANCE_WARN_FLOOR_FALLBACK);\n\n // --- Resolve denom map ---\n // Pure-sync decision function: callers pre-load the map via\n // `loadChainDenomMap(chainDataFile)` and pass it in. When absent, the\n // empty no-op map is used (raw on-chain denom rendering downstream).\n // See Q4 Bii rationale in EvaluateReadinessInputs.denomMap docstring.\n const denomMap = inputs.denomMap ?? EMPTY_DENOM_MAP;\n\n // --- Walk readiness rules ---\n const reasons: string[] = [];\n const actions = new Set<ReadinessAction>();\n let status: Readiness['status'] = 'ok';\n\n // 1. SKU availability — hard block when the user's chosen size isn't offered.\n if (inputs.size !== null && !inputs.availableSkuNames.includes(inputs.size)) {\n status = 'block';\n const available =\n inputs.availableSkuNames.length > 0\n ? inputs.availableSkuNames.join(', ')\n : '(none)';\n reasons.push(\n `Requested SKU \"${inputs.size}\" is not currently offered. Available: ${available}.`,\n );\n actions.add('pick_different_sku');\n }\n\n // 2. Wallet gas balance — hard block on absent/zero, warn on below-floor.\n const gasEntry = inputs.walletBalances.find((b) => b.denom === gasDenom);\n const gasAmount = gasEntry ? asBigInt(gasEntry.amount) : 0n;\n if (inputs.walletBalances.length === 0 || gasAmount === 0n) {\n status = 'block';\n reasons.push(\n `Wallet has no ${denomToSymbol(gasDenom, denomMap)} balance for gas.`,\n );\n actions.add('request_faucet');\n actions.add('topup_wallet');\n } else if (gasAmount < gasWarnFloor) {\n if (status === 'ok') status = 'warn';\n reasons.push(\n `Wallet balance (${humanizeCoin(\n gasAmount.toString(),\n gasDenom,\n denomMap,\n )}) is below ${humanizeCoin(\n gasWarnFloor.toString(),\n gasDenom,\n denomMap,\n )}; broadcast may run out of gas.`,\n );\n actions.add('topup_wallet');\n }\n\n // 3. Credits.\n //\n // CJS preserves a subtle source-of-truth selection: `credits.availableBalances`\n // is the \"right now\" balance net of pending reservations; `credits.balances`\n // (older variant) is the gross-funded fallback; `currentBalance` is from\n // the chain's credit estimator and is only present when the tenant has at\n // least one ACTIVE lease. A fresh deployer with credits but no active\n // leases would have `currentBalance` ABSENT — reading that field FIRST as\n // the credit source produces a false \"Credit account is empty\" warning.\n // Mirror the CJS precedence: availableBalances → balances → currentBalance.\n const credits = inputs.credits;\n if (credits === null) {\n if (status === 'ok') status = 'warn';\n reasons.push('No credit account funded for compute leases.');\n actions.add('fund_credit');\n } else if (\n inputs.sku !== null &&\n inputs.sku.price.amount.length > 0 &&\n inputs.sku.price.denom.length > 0\n ) {\n const skuPrice = inputs.sku.price;\n const creditBalances: Coin[] = Array.isArray(credits.availableBalances)\n ? credits.availableBalances\n : Array.isArray(credits.balances)\n ? credits.balances\n : Array.isArray(credits.currentBalance)\n ? credits.currentBalance\n : [];\n const creditEntry = creditBalances.find((b) => b.denom === skuPrice.denom);\n const pricePerHour = asBigInt(skuPrice.amount);\n if (creditEntry === undefined) {\n // The credit account has NO entry in the SKU's price denom. Distinct\n // from \"credits ran out\" — usually means credits are funded in a\n // different denom than the SKU charges in. Emit a specific\n // diagnostic so the user knows to fund_credit in the right denom\n // rather than seeing a false \"0 hours of runtime\" warning.\n const fundedDenoms = creditBalances\n .map((b) => b.denom)\n .filter((d): d is string => typeof d === 'string' && d.length > 0);\n const skuSymbol = denomToSymbol(skuPrice.denom, denomMap);\n const fundedSymbols = fundedDenoms.map((d) => denomToSymbol(d, denomMap));\n if (status === 'ok') status = 'warn';\n reasons.push(\n fundedDenoms.length > 0\n ? `Credit account has no ${skuSymbol} balance (the ${inputs.sku.name} SKU charges in ${skuSymbol}; account holds ${fundedSymbols.join(\n ', ',\n )}). Fund ${skuSymbol} credits before deploying.`\n : `Credit account is empty for the ${inputs.sku.name} SKU's ${skuSymbol} denom. Fund ${skuSymbol} credits before deploying.`,\n );\n actions.add('fund_credit');\n } else if (pricePerHour > 0n) {\n const creditAmount = asBigInt(creditEntry.amount);\n // Convert via Number for the human-readable hours figure. Credit\n // amounts are in the chain's smallest unit and bounded well below\n // Number.MAX_SAFE_INTEGER for any realistic balance.\n const hrsForThisSku = Number(creditAmount) / Number(pricePerHour);\n if (hrsForThisSku < HOURS_REMAINING_WARN_FLOOR) {\n if (status === 'ok') status = 'warn';\n reasons.push(\n `Credits cover ~${hrsForThisSku.toFixed(1)}h of runtime at the ${inputs.sku.name} SKU (${humanizeCoin(\n creditAmount.toString(),\n skuPrice.denom,\n denomMap,\n )} / ${humanizeCoin(\n pricePerHour.toString(),\n skuPrice.denom,\n denomMap,\n )} per hour); below the ${HOURS_REMAINING_WARN_FLOOR}h floor.`,\n );\n actions.add('fund_credit');\n }\n }\n } else if (credits.hoursRemaining !== undefined) {\n // Fallback for cases where SKU pricing is not available (e.g. caller\n // didn't pass --size). Use the chain's hoursRemaining but ONLY warn\n // when it's a meaningful positive number below the floor — `0` here\n // means \"no current burn\", not \"low credits\".\n const hrs = Number(credits.hoursRemaining);\n if (Number.isFinite(hrs) && hrs > 0 && hrs < HOURS_REMAINING_WARN_FLOOR) {\n if (status === 'ok') status = 'warn';\n reasons.push(\n `Credits cover ~${hrs.toFixed(1)}h of runtime at the current burn rate; below the ${HOURS_REMAINING_WARN_FLOOR}h floor.`,\n );\n actions.add('fund_credit');\n }\n }\n\n // --- Map input shape into the frozen `Readiness` carrier fields ---\n const creditsOut: Readiness['credits'] =\n credits === null\n ? null\n : {\n availableBalances: Array.isArray(credits.availableBalances)\n ? credits.availableBalances\n : Array.isArray(credits.balances)\n ? credits.balances\n : Array.isArray(credits.currentBalance)\n ? credits.currentBalance\n : [],\n };\n\n return {\n status,\n reasons,\n suggestedActions: Array.from(actions),\n walletBalances: inputs.walletBalances,\n credits: creditsOut,\n sku: inputs.sku,\n };\n}\n\nfunction asBigInt(s: string): bigint {\n try {\n return BigInt(s);\n } catch {\n return 0n;\n }\n}\n\nfunction validateGasWarnFloor(value: bigint): bigint {\n if (value < 0n) {\n throw new TypeError(\n `evaluateReadiness: gasWarnFloor must be a non-negative integer, got ${value}`,\n );\n }\n return value;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"evaluate-readiness.js","names":[],"sources":["../../src/internals/evaluate-readiness.ts"],"sourcesContent":["import type { Coin, Readiness, ReadinessAction } from '../types.js';\nimport {\n type DenomMap,\n denomToSymbol,\n EMPTY_DENOM_MAP,\n humanizeCoin,\n} from './humanize-denom.js';\n\n/**\n * Evaluate `check_deployment_readiness` MCP response data into the frozen\n * `Readiness` shape (camelCase typed input + the `Readiness` contract from\n * ENG-128).\n *\n * Thresholds are encoded here (not in skill prose or caller config) so the\n * rules stay consistent across runs:\n * - HOURS_REMAINING_WARN_FLOOR = 24\n * - GAS_BALANCE_WARN_FLOOR (per-denom) = 50_000n umfx | upwr\n *\n * Status semantics (CJS-parity):\n * - `'block'` — cannot proceed (SKU unavailable, wallet empty)\n * - `'warn'` — proceedable but risky (low credits, low gas balance, no credit account)\n * - `'ok'` — silent pass\n *\n * `suggestedActions` are semantic tokens from the frozen `ReadinessAction`\n * union — not prose for the user. Surfaces map these to UI affordances.\n *\n * Walked-from-CJS field-rename: the MCP response uses snake_case\n * (`wallet_balances`, `available_balances`, `hours_remaining`,\n * `available_sku_names`, `current_balance`); the TS-port input is\n * camelCase, and high-level callers (PR 3's `deployApp`) translate the\n * snake_case wire shape into camelCase before passing in.\n */\n\nconst HOURS_REMAINING_WARN_FLOOR = 24;\n\n// Per-denom warn floors for low gas balance (in smallest unit). Mirrors the\n// CJS values: 50_000 umfx = 0.05 MFX (1 MFX = 1,000,000 umfx); comparable\n// headroom for upwr.\nconst GAS_BALANCE_WARN_FLOOR_DEFAULTS: Readonly<Record<string, bigint>> = {\n umfx: 50_000n,\n upwr: 50_000n,\n};\nconst GAS_BALANCE_WARN_FLOOR_FALLBACK = 50_000n;\n\n/**\n * Cosmos convention for gas-price strings: leading numeric (digits +\n * optional decimal point), then the denom. Denom grammar mirrors\n * `sdk.ValidateDenom`: `[a-zA-Z][a-zA-Z0-9/:._-]{2,127}`.\n * Anchored both ends so trailing whitespace fails fast.\n */\nconst GAS_PRICE_RE = /^[0-9]+(?:\\.[0-9]+)?([a-zA-Z][a-zA-Z0-9/:._-]{2,127})$/;\n\n/**\n * Inputs passed to `evaluateReadiness`. camelCase throughout — high-level\n * callers translate the snake_case MCP response shape before invocation.\n */\nexport interface EvaluateReadinessInputs {\n /** Tenant address (bech32). Not consumed by the algorithm; included for journal/log context. */\n tenant: string;\n /** Image ref being considered (may be `null` when only the size is selected). */\n image: string | null;\n /** SKU size string the caller wants (`'docker-micro'`, etc.). `null` when not yet chosen. */\n size: string | null;\n /** Wallet bank balances. */\n walletBalances: Coin[];\n /** Credit account data, or `null` when no credit account is funded. */\n credits: {\n availableBalances?: Coin[];\n /** Older response variant — fallback when `availableBalances` is absent. */\n balances?: Coin[];\n /** Live current credit balance(s) when the tenant has at least one active lease. */\n currentBalance?: Coin[];\n /** Hours of runtime at the user's current overall burn rate (string-encoded number). */\n hoursRemaining?: string;\n } | null;\n /** Chosen SKU + price, or `null` when no size selected. */\n sku: { name: string; price: Coin } | null;\n /** All active SKU names the chain currently advertises. */\n availableSkuNames: string[];\n /** Gas-price string (e.g. `'1umfx'`, `'0.37upwr'`). Required — drives the wallet-gas check denom. */\n gasPrice: string;\n /** Override the per-denom warn floor (smallest unit). When omitted, uses the per-denom default or 50_000n fallback. */\n gasWarnFloor?: bigint;\n /**\n * Pre-loaded `DenomMap` for symbol humanization. The orchestrator\n * (`deploy-app.ts` and PR-4 callers) is responsible for composing the\n * map via `loadChainDenomMap(chainDataFile)` and passing it in. This\n * keeps `evaluateReadiness` pure-sync — I/O lives at the orchestrator\n * boundary, not inside the decision function (post-Q4 Bii verdict).\n *\n * When omitted, the no-op map is used; balances + SKU prices render\n * with raw on-chain denoms.\n */\n denomMap?: DenomMap;\n}\n\n/**\n * Compute the `Readiness` verdict for a prospective deployment.\n *\n * Throws `TypeError` on malformed `gasPrice` (the only input field whose\n * runtime shape isn't enforced by the typed signature).\n */\nexport function evaluateReadiness(inputs: EvaluateReadinessInputs): Readiness {\n // --- Parse + validate gasPrice via String.match (avoids RegExp.exec) ---\n const gasDenomMatch = inputs.gasPrice.match(GAS_PRICE_RE);\n if (!gasDenomMatch || gasDenomMatch[1] === undefined) {\n throw new TypeError(\n `evaluateReadiness: gasPrice must match <numeric><denom> (e.g. \"1umfx\" or \"0.37upwr\"); got \"${inputs.gasPrice}\"`,\n );\n }\n const gasDenom = gasDenomMatch[1];\n\n // --- Resolve gas warn floor ---\n const gasWarnFloor =\n inputs.gasWarnFloor !== undefined\n ? validateGasWarnFloor(inputs.gasWarnFloor)\n : (GAS_BALANCE_WARN_FLOOR_DEFAULTS[gasDenom] ??\n GAS_BALANCE_WARN_FLOOR_FALLBACK);\n\n // --- Resolve denom map ---\n // Pure-sync decision function: callers pre-load the map via\n // `loadChainDenomMap(chainDataFile)` and pass it in. When absent, the\n // empty no-op map is used (raw on-chain denom rendering downstream).\n // See Q4 Bii rationale in EvaluateReadinessInputs.denomMap docstring.\n const denomMap = inputs.denomMap ?? EMPTY_DENOM_MAP;\n\n // --- Walk readiness rules ---\n const reasons: string[] = [];\n const actions = new Set<ReadinessAction>();\n let status: Readiness['status'] = 'ok';\n\n // 1. SKU availability — hard block when the user's chosen size isn't offered.\n if (inputs.size !== null && !inputs.availableSkuNames.includes(inputs.size)) {\n status = 'block';\n const available =\n inputs.availableSkuNames.length > 0\n ? inputs.availableSkuNames.join(', ')\n : '(none)';\n reasons.push(\n `Requested SKU \"${inputs.size}\" is not currently offered. Available: ${available}.`,\n );\n actions.add('pick_different_sku');\n }\n\n // 2. Wallet gas balance — hard block on absent/zero, warn on below-floor.\n const gasEntry = inputs.walletBalances.find((b) => b.denom === gasDenom);\n const gasAmount = gasEntry ? asBigInt(gasEntry.amount) : 0n;\n if (inputs.walletBalances.length === 0 || gasAmount === 0n) {\n status = 'block';\n reasons.push(\n `Wallet has no ${denomToSymbol(gasDenom, denomMap)} balance for gas.`,\n );\n actions.add('request_faucet');\n actions.add('topup_wallet');\n } else if (gasAmount < gasWarnFloor) {\n if (status === 'ok') status = 'warn';\n reasons.push(\n `Wallet balance (${humanizeCoin(\n gasAmount.toString(),\n gasDenom,\n denomMap,\n )}) is below ${humanizeCoin(\n gasWarnFloor.toString(),\n gasDenom,\n denomMap,\n )}; broadcast may run out of gas.`,\n );\n actions.add('topup_wallet');\n }\n\n // 3. Credits.\n //\n // CJS preserves a subtle source-of-truth selection: `credits.availableBalances`\n // is the \"right now\" balance net of pending reservations; `credits.balances`\n // (older variant) is the gross-funded fallback; `currentBalance` is from\n // the chain's credit estimator and is only present when the tenant has at\n // least one ACTIVE lease. A fresh deployer with credits but no active\n // leases would have `currentBalance` ABSENT — reading that field FIRST as\n // the credit source produces a false \"Credit account is empty\" warning.\n // Mirror the CJS precedence: availableBalances → balances → currentBalance.\n const credits = inputs.credits;\n if (credits === null) {\n if (status === 'ok') status = 'warn';\n reasons.push('No credit account funded for compute leases.');\n actions.add('fund_credit');\n } else if (\n inputs.sku !== null &&\n inputs.sku.price.amount.length > 0 &&\n inputs.sku.price.denom.length > 0\n ) {\n const skuPrice = inputs.sku.price;\n const creditBalances: Coin[] = Array.isArray(credits.availableBalances)\n ? credits.availableBalances\n : Array.isArray(credits.balances)\n ? credits.balances\n : Array.isArray(credits.currentBalance)\n ? credits.currentBalance\n : [];\n const creditEntry = creditBalances.find((b) => b.denom === skuPrice.denom);\n const pricePerHour = asBigInt(skuPrice.amount);\n if (creditEntry === undefined) {\n // The credit account has NO entry in the SKU's price denom. Distinct\n // from \"credits ran out\" — usually means credits are funded in a\n // different denom than the SKU charges in. Emit a specific\n // diagnostic so the user knows to fund_credit in the right denom\n // rather than seeing a false \"0 hours of runtime\" warning.\n const fundedDenoms = creditBalances\n .map((b) => b.denom)\n .filter((d): d is string => typeof d === 'string' && d.length > 0);\n const skuSymbol = denomToSymbol(skuPrice.denom, denomMap);\n const fundedSymbols = fundedDenoms.map((d) => denomToSymbol(d, denomMap));\n if (status === 'ok') status = 'warn';\n reasons.push(\n fundedDenoms.length > 0\n ? `Credit account has no ${skuSymbol} balance (the ${inputs.sku.name} SKU charges in ${skuSymbol}; account holds ${fundedSymbols.join(\n ', ',\n )}). Fund ${skuSymbol} credits before deploying.`\n : `Credit account is empty for the ${inputs.sku.name} SKU's ${skuSymbol} denom. Fund ${skuSymbol} credits before deploying.`,\n );\n actions.add('fund_credit');\n } else if (pricePerHour > 0n) {\n const creditAmount = asBigInt(creditEntry.amount);\n // Convert via Number for the human-readable hours figure. Credit\n // amounts are in the chain's smallest unit and bounded well below\n // Number.MAX_SAFE_INTEGER for any realistic balance.\n const hrsForThisSku = Number(creditAmount) / Number(pricePerHour);\n if (hrsForThisSku < HOURS_REMAINING_WARN_FLOOR) {\n if (status === 'ok') status = 'warn';\n reasons.push(\n `Credits cover ~${hrsForThisSku.toFixed(1)}h of runtime at the ${inputs.sku.name} SKU (${humanizeCoin(\n creditAmount.toString(),\n skuPrice.denom,\n denomMap,\n )} / ${humanizeCoin(\n pricePerHour.toString(),\n skuPrice.denom,\n denomMap,\n )} per hour); below the ${HOURS_REMAINING_WARN_FLOOR}h floor.`,\n );\n actions.add('fund_credit');\n }\n }\n } else if (credits.hoursRemaining !== undefined) {\n // Fallback for cases where SKU pricing is not available (e.g. caller\n // didn't pass --size). Use the chain's hoursRemaining but ONLY warn\n // when it's a meaningful positive number below the floor — `0` here\n // means \"no current burn\", not \"low credits\".\n const hrs = Number(credits.hoursRemaining);\n if (Number.isFinite(hrs) && hrs > 0 && hrs < HOURS_REMAINING_WARN_FLOOR) {\n if (status === 'ok') status = 'warn';\n reasons.push(\n `Credits cover ~${hrs.toFixed(1)}h of runtime at the current burn rate; below the ${HOURS_REMAINING_WARN_FLOOR}h floor.`,\n );\n actions.add('fund_credit');\n }\n }\n\n // --- Map input shape into the frozen `Readiness` carrier fields ---\n const creditsOut: Readiness['credits'] =\n credits === null\n ? null\n : {\n availableBalances: Array.isArray(credits.availableBalances)\n ? credits.availableBalances\n : Array.isArray(credits.balances)\n ? credits.balances\n : Array.isArray(credits.currentBalance)\n ? credits.currentBalance\n : [],\n };\n\n return {\n status,\n reasons,\n suggestedActions: Array.from(actions),\n walletBalances: inputs.walletBalances,\n credits: creditsOut,\n sku: inputs.sku,\n };\n}\n\nfunction asBigInt(s: string): bigint {\n try {\n return BigInt(s);\n } catch {\n return 0n;\n }\n}\n\nfunction validateGasWarnFloor(value: bigint): bigint {\n if (value < 0n) {\n throw new TypeError(\n `evaluateReadiness: gasWarnFloor must be a non-negative integer, got ${value}`,\n );\n }\n return value;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"find-sku-uuid.d.ts","names":[],"sources":["../../src/internals/find-sku-uuid.ts"],"mappings":";;;;;AAmCA
|
|
1
|
+
{"version":3,"file":"find-sku-uuid.d.ts","names":[],"sources":["../../src/internals/find-sku-uuid.ts"],"mappings":";;;;;AAmCA;;;;AAIuB;AAGvB;;;;;;;;;;;;;;AAGwB;;;;;;UAVP,aAAA;;WAEN,OAAA;;WAEA,YAAY;AAAA;AAAA,iBAGD,WAAA,CACpB,aAAA,EAAe,mBAAA,EACf,IAAA,WACC,OAAA,CAAQ,aAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"find-sku-uuid.js","names":[],"sources":["../../src/internals/find-sku-uuid.ts"],"sourcesContent":["import {\n type CosmosClientManager,\n createPagination,\n MAX_PAGE_LIMIT,\n ManifestMCPError,\n ManifestMCPErrorCode,\n} from '@manifest-network/manifest-mcp-core';\n\n/**\n * Resolve a SKU-tier name (e.g. `'docker-micro'`, `'small'`) to its on-chain\n * UUID + the UUID of its publishing provider. Mirrors fred's internal\n * `findSkuUuid` helper at `packages/fred/src/tools/deployApp.ts:L62`.\n *\n * Used by `deploy-app.ts`'s pre-broadcast fee-estimation step (the\n * `cosmosEstimateFee('billing', 'create-lease', ...)` call needs the SKU\n * UUID as part of the item-arg string format `sku-uuid:quantity[:service-name]`).\n *\n * **Why duplicated (not imported from fred):** fred's `findSkuUuid` is an\n * internal helper, not re-exported from `packages/fred/src/index.ts`.\n * Per team-lead-2's Path 2 verdict (vs Path 1 export-from-fred), the\n * helper is duplicated here because:\n *\n * 1. Stateless query helper — zero cross-instance drift risk\n * (unlike `AuthTimestampTracker` which has cross-call state).\n * 2. fred's barrel deliberately keeps this helper internal; making it\n * public would be an architectural decision, not an oversight fix.\n * 3. The duplicated logic uses only already-exported core symbols\n * (`createPagination`, `MAX_PAGE_LIMIT`, `ManifestMCPError`).\n * 4. Drift bounded by the shared `@manifest-network/manifestjs@2.4.1`\n * proto pin — both fred and agent-core import the same SKU types.\n *\n * Throws `ManifestMCPError(QUERY_FAILED)` when no active SKU matches\n * the requested `size`. Error message includes the available SKU names\n * for caller-side debugging.\n */\nexport interface SkuResolution {\n /** On-chain SKU UUID. Used in `create-lease` item-arg construction. */\n readonly skuUuid: string;\n /** Publishing provider's UUID. Required by some downstream chain queries. */\n readonly providerUuid: string;\n}\n\nexport async function findSkuUuid(\n clientManager: CosmosClientManager,\n size: string,\n): Promise<SkuResolution> {\n const queryClient = await clientManager.getQueryClient();\n const pagination = createPagination(MAX_PAGE_LIMIT);\n const result = await queryClient.liftedinit.sku.v1.sKUs({\n activeOnly: true,\n pagination,\n });\n\n for (const sku of result.skus) {\n if (sku.name === size) {\n return { skuUuid: sku.uuid, providerUuid: sku.providerUuid };\n }\n }\n\n const available = result.skus.map((s) => s.name);\n throw new ManifestMCPError(\n ManifestMCPErrorCode.QUERY_FAILED,\n `SKU tier \"${size}\" not found. Available: ${available.join(', ')}`,\n );\n}\n"],"mappings":";;AA0CA,eAAsB,YACpB,eACA,MACwB;CACxB,MAAM,cAAc,MAAM,cAAc,
|
|
1
|
+
{"version":3,"file":"find-sku-uuid.js","names":[],"sources":["../../src/internals/find-sku-uuid.ts"],"sourcesContent":["import {\n type CosmosClientManager,\n createPagination,\n MAX_PAGE_LIMIT,\n ManifestMCPError,\n ManifestMCPErrorCode,\n} from '@manifest-network/manifest-mcp-core';\n\n/**\n * Resolve a SKU-tier name (e.g. `'docker-micro'`, `'small'`) to its on-chain\n * UUID + the UUID of its publishing provider. Mirrors fred's internal\n * `findSkuUuid` helper at `packages/fred/src/tools/deployApp.ts:L62`.\n *\n * Used by `deploy-app.ts`'s pre-broadcast fee-estimation step (the\n * `cosmosEstimateFee('billing', 'create-lease', ...)` call needs the SKU\n * UUID as part of the item-arg string format `sku-uuid:quantity[:service-name]`).\n *\n * **Why duplicated (not imported from fred):** fred's `findSkuUuid` is an\n * internal helper, not re-exported from `packages/fred/src/index.ts`.\n * Per team-lead-2's Path 2 verdict (vs Path 1 export-from-fred), the\n * helper is duplicated here because:\n *\n * 1. Stateless query helper — zero cross-instance drift risk\n * (unlike `AuthTimestampTracker` which has cross-call state).\n * 2. fred's barrel deliberately keeps this helper internal; making it\n * public would be an architectural decision, not an oversight fix.\n * 3. The duplicated logic uses only already-exported core symbols\n * (`createPagination`, `MAX_PAGE_LIMIT`, `ManifestMCPError`).\n * 4. Drift bounded by the shared `@manifest-network/manifestjs@2.4.1`\n * proto pin — both fred and agent-core import the same SKU types.\n *\n * Throws `ManifestMCPError(QUERY_FAILED)` when no active SKU matches\n * the requested `size`. Error message includes the available SKU names\n * for caller-side debugging.\n */\nexport interface SkuResolution {\n /** On-chain SKU UUID. Used in `create-lease` item-arg construction. */\n readonly skuUuid: string;\n /** Publishing provider's UUID. Required by some downstream chain queries. */\n readonly providerUuid: string;\n}\n\nexport async function findSkuUuid(\n clientManager: CosmosClientManager,\n size: string,\n): Promise<SkuResolution> {\n const queryClient = await clientManager.getQueryClient();\n const pagination = createPagination(MAX_PAGE_LIMIT);\n const result = await queryClient.liftedinit.sku.v1.sKUs({\n activeOnly: true,\n pagination,\n });\n\n for (const sku of result.skus) {\n if (sku.name === size) {\n return { skuUuid: sku.uuid, providerUuid: sku.providerUuid };\n }\n }\n\n const available = result.skus.map((s) => s.name);\n throw new ManifestMCPError(\n ManifestMCPErrorCode.QUERY_FAILED,\n `SKU tier \"${size}\" not found. Available: ${available.join(', ')}`,\n );\n}\n"],"mappings":";;AA0CA,eAAsB,YACpB,eACA,MACwB;CACxB,MAAM,cAAc,MAAM,cAAc,eAAe;CACvD,MAAM,aAAa,iBAAiB,cAAc;CAClD,MAAM,SAAS,MAAM,YAAY,WAAW,IAAI,GAAG,KAAK;EACtD,YAAY;EACZ;CACF,CAAC;CAED,KAAK,MAAM,OAAO,OAAO,MACvB,IAAI,IAAI,SAAS,MACf,OAAO;EAAE,SAAS,IAAI;EAAM,cAAc,IAAI;CAAa;CAI/D,MAAM,YAAY,OAAO,KAAK,KAAK,MAAM,EAAE,IAAI;CAC/C,MAAM,IAAI,iBACR,qBAAqB,cACrB,aAAa,KAAK,0BAA0B,UAAU,KAAK,IAAI,GACjE;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"format-success.d.ts","names":[],"sources":["../../src/internals/format-success.ts"],"mappings":";;;;UA4CiB,cAAA;EAAA;EAEf,KAAA;;EAEA,aAAA;EAFA;EAIA,aAAA;EAAA;EAEA,UAAA;EAWA
|
|
1
|
+
{"version":3,"file":"format-success.d.ts","names":[],"sources":["../../src/internals/format-success.ts"],"mappings":";;;;UA4CiB,cAAA;EAAA;EAEf,KAAA;;EAEA,aAAA;EAFA;EAIA,aAAA;EAAA;EAEA,UAAA;EAWA;;AAAG;AAGL;;;;;;;EAHE,GAAA;AAAA;AAAA,UAGe,kBAAA;EAOY;EAL3B,SAAA;EAKmC;EAHnC,cAAA,EAAgB,cAAc;AAAA;AAAA,iBAGhB,aAAA,CAAc,KAAyB,EAAlB,kBAAkB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"format-success.js","names":["decodeLeaseState"],"sources":["../../src/internals/format-success.ts"],"sourcesContent":["import {\n extractRunningEndpoints,\n formatEndpointAsIngress,\n normalizeFredUrl,\n type RunningEndpoint,\n} from './connection.js';\nimport { decode as decodeLeaseState } from './lease-state.js';\n\n/**\n * Render the user-facing \"Deployed.\" block for a successful `deployApp` run.\n *\n * Takes `lease-uuid` and `deploy_response` via the typed `FormatSuccessInput`.\n *\n * Output is plain text suitable for direct chat display. Designed to be\n * printed verbatim by `deployApp` (and downstream renderers) — no\n * paraphrasing or surrounding prose.\n *\n * **Lease-state decoding:** `deploy_response.state` may be an integer (raw\n * chain emit) or a `LEASE_STATE_*` string (codec.toJSON form). Both flow\n * through `lease-state.ts:decode`. Unknown values render as\n * `UNKNOWN(<raw>)` so the raw remains visible.\n *\n * **Multi-instance / multi-service stacks** emit `Ingresses:` followed by\n * one bare FQDN per UNIQUE FQDN across running instances. Instances\n * sharing an FQDN (e.g. replicas behind one subdomain) are deduped by\n * `extractRunningEndpoints`.\n *\n * **Custom-domain line** is emitted BEFORE the Ingress block when the\n * deploy response carries a `custom_domain` (the set-domain tx confirmed\n * alongside create-lease). The \"(provisioning)\" qualifier reflects that\n * the chain tx confirmed but the provider may still be issuing the cert.\n *\n * **Provider rendering**: the CJS once attempted to resolve a friendly\n * provider name via `browse_catalog` and dropped it when upstream's\n * catalog shape carried no `name` field. The TS port keeps the\n * `provider_uuid` rendering for the same reason — if upstream later adds\n * `name`, restore via a thin helper.\n */\n\n/** RFC 4122 UUID — 36 chars, hex + 4 hyphens, lowercase or upper. */\nconst UUID_RE =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\n\n/** Deploy response shape consumed by `formatSuccess`. Subset of fred's `mcp__manifest-fred__deploy_app` response. */\nexport interface DeployResponse {\n /** Chain lease state — int (raw) or `LEASE_STATE_*` string (codec). */\n state?: number | string;\n /** Provider UUID — rendered as-is (catalog has no friendly-name field). */\n provider_uuid?: string;\n /** Custom domain attached to the lease item; populated when set-domain confirmed. */\n custom_domain?: string;\n /** Provider connection payload — walked by `connection.ts`. */\n connection?: unknown;\n /**\n * Top-level URL — legacy fallback when provider reports\n * `connection.host` / `connection.ports` shape rather than\n * `connection.instances`. Used when no FQDN can be extracted from\n * `connection`. Defensive: `classify-deploy-response.ts:43-51,76-80`\n * already defends against the same legacy shape; mirroring here keeps\n * the renderer consistent with the classifier so a fred response of\n * `{ url: 'https://app.example.com/' }` renders an Ingress line\n * rather than `(none …)`.\n */\n url?: string;\n}\n\nexport interface FormatSuccessInput {\n /** Validated lease UUID (RFC 4122 v1-v5). */\n leaseUuid: string;\n /** Deploy response from `deploy_app` (or equivalent atomic broadcast). */\n deployResponse: DeployResponse;\n}\n\nexport function formatSuccess(input: FormatSuccessInput): string {\n if (!UUID_RE.test(input.leaseUuid)) {\n throw new TypeError(\n `formatSuccess: leaseUuid must be a UUID; got \"${input.leaseUuid}\"`,\n );\n }\n if (\n input.deployResponse === null ||\n typeof input.deployResponse !== 'object'\n ) {\n throw new TypeError(\n 'formatSuccess: deployResponse must be a non-null object',\n );\n }\n\n const dr = input.deployResponse;\n const providerName =\n typeof dr.provider_uuid === 'string' && dr.provider_uuid.length > 0\n ? dr.provider_uuid\n : '(unknown)';\n const stateName = decodeStateName(dr.state);\n const endpoints = extractRunningEndpoints(dr.connection);\n const ingresses = endpoints\n .map(formatEndpointAsIngress)\n .filter((s): s is string => typeof s === 'string' && s.length > 0);\n // Copilot review fix (PR #58 r3250192778): the custom-domain block's\n // TLS note (\\\"the Ingress URL below works immediately\\\") promises a\n // URL that may not exist when `connection.instances` is empty AND\n // there's no top-level `url`. Compute ingress availability up-front\n // so the custom-domain block can branch its second line accordingly.\n const hasIngress =\n ingresses.length > 0 || (typeof dr.url === 'string' && dr.url.length > 0);\n\n const lines: string[] = [\n 'Deployed.',\n ` Provider: ${providerName}`,\n ` Lease UUID: ${input.leaseUuid}`,\n ` Lease Status: ${stateName}`,\n ];\n\n // Custom-domain block — chain tx confirmed, provider may still be\n // provisioning. Present BEFORE Ingress so the user sees the requested\n // endpoint first, alongside the immediately-working provider FQDN (if\n // any). The TLS note's \"Ingress URL below works immediately\" promise\n // only fires when an Ingress is actually present (r3250192778).\n if (typeof dr.custom_domain === 'string' && dr.custom_domain.length > 0) {\n lines.push(` Custom domain (provisioning): https://${dr.custom_domain}/`);\n lines.push(\n hasIngress\n ? ' — TLS may take a few minutes; the Ingress URL below works immediately.'\n : ' — TLS may take a few minutes.',\n );\n }\n\n if (ingresses.length === 0) {\n // Legacy fallback: when no FQDN can be extracted from `connection`\n // (e.g. providers reporting the older `connection.host` / `ports`\n // shape rather than `connection.instances`), fred may still surface\n // the URL at the top level. `normalizeFredUrl` is the shared helper\n // (mirrored across `classify-deploy-response.ts`, this renderer,\n // and `deploy-app.ts`'s `DeployResult.urls` fallback). The\n // `(none …)` fallback stays for the truly-empty case.\n if (typeof dr.url === 'string' && dr.url.length > 0) {\n lines.push(` Ingress: ${normalizeFredUrl(dr.url)}`);\n } else {\n lines.push(\n ' Ingress: (none — service is internal or no FQDN reported)',\n );\n }\n } else if (ingresses.length === 1) {\n lines.push(` Ingress: ${ingresses[0]}`);\n } else {\n lines.push(' Ingresses:');\n for (const fqdn of ingresses) {\n lines.push(` - ${fqdn}`);\n }\n }\n lines.push('');\n lines.push(\n `For logs / status: /manifest-agent:troubleshoot-deployment ${input.leaseUuid}`,\n );\n\n return lines.join('\\n');\n}\n\n/**\n * Return the user-facing form of a lease state. The `LEASE_STATE_` prefix\n * is stripped for display (e.g. `LEASE_STATE_ACTIVE` → `ACTIVE`). Unknown\n * decodes render as `UNKNOWN(<raw>)` so the raw remains visible; absent\n * state renders as `(unknown)`.\n */\nfunction decodeStateName(state: number | string | undefined): string {\n if (state === undefined) return '(unknown)';\n const canonical = decodeLeaseState(state);\n if (canonical !== undefined) {\n return canonical.slice('LEASE_STATE_'.length);\n }\n return `UNKNOWN(${String(state)})`;\n}\n\n// Re-export for callers that want to walk endpoints themselves without\n// re-importing from `./connection.js`. Keeps `format-success.ts` the\n// single consumer-facing entry for success-rendering plumbing.\nexport type { RunningEndpoint };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,MAAM,UACJ;AAgCF,SAAgB,cAAc,OAAmC;
|
|
1
|
+
{"version":3,"file":"format-success.js","names":["decodeLeaseState"],"sources":["../../src/internals/format-success.ts"],"sourcesContent":["import {\n extractRunningEndpoints,\n formatEndpointAsIngress,\n normalizeFredUrl,\n type RunningEndpoint,\n} from './connection.js';\nimport { decode as decodeLeaseState } from './lease-state.js';\n\n/**\n * Render the user-facing \"Deployed.\" block for a successful `deployApp` run.\n *\n * Takes `lease-uuid` and `deploy_response` via the typed `FormatSuccessInput`.\n *\n * Output is plain text suitable for direct chat display. Designed to be\n * printed verbatim by `deployApp` (and downstream renderers) — no\n * paraphrasing or surrounding prose.\n *\n * **Lease-state decoding:** `deploy_response.state` may be an integer (raw\n * chain emit) or a `LEASE_STATE_*` string (codec.toJSON form). Both flow\n * through `lease-state.ts:decode`. Unknown values render as\n * `UNKNOWN(<raw>)` so the raw remains visible.\n *\n * **Multi-instance / multi-service stacks** emit `Ingresses:` followed by\n * one bare FQDN per UNIQUE FQDN across running instances. Instances\n * sharing an FQDN (e.g. replicas behind one subdomain) are deduped by\n * `extractRunningEndpoints`.\n *\n * **Custom-domain line** is emitted BEFORE the Ingress block when the\n * deploy response carries a `custom_domain` (the set-domain tx confirmed\n * alongside create-lease). The \"(provisioning)\" qualifier reflects that\n * the chain tx confirmed but the provider may still be issuing the cert.\n *\n * **Provider rendering**: the CJS once attempted to resolve a friendly\n * provider name via `browse_catalog` and dropped it when upstream's\n * catalog shape carried no `name` field. The TS port keeps the\n * `provider_uuid` rendering for the same reason — if upstream later adds\n * `name`, restore via a thin helper.\n */\n\n/** RFC 4122 UUID — 36 chars, hex + 4 hyphens, lowercase or upper. */\nconst UUID_RE =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\n\n/** Deploy response shape consumed by `formatSuccess`. Subset of fred's `mcp__manifest-fred__deploy_app` response. */\nexport interface DeployResponse {\n /** Chain lease state — int (raw) or `LEASE_STATE_*` string (codec). */\n state?: number | string;\n /** Provider UUID — rendered as-is (catalog has no friendly-name field). */\n provider_uuid?: string;\n /** Custom domain attached to the lease item; populated when set-domain confirmed. */\n custom_domain?: string;\n /** Provider connection payload — walked by `connection.ts`. */\n connection?: unknown;\n /**\n * Top-level URL — legacy fallback when provider reports\n * `connection.host` / `connection.ports` shape rather than\n * `connection.instances`. Used when no FQDN can be extracted from\n * `connection`. Defensive: `classify-deploy-response.ts:43-51,76-80`\n * already defends against the same legacy shape; mirroring here keeps\n * the renderer consistent with the classifier so a fred response of\n * `{ url: 'https://app.example.com/' }` renders an Ingress line\n * rather than `(none …)`.\n */\n url?: string;\n}\n\nexport interface FormatSuccessInput {\n /** Validated lease UUID (RFC 4122 v1-v5). */\n leaseUuid: string;\n /** Deploy response from `deploy_app` (or equivalent atomic broadcast). */\n deployResponse: DeployResponse;\n}\n\nexport function formatSuccess(input: FormatSuccessInput): string {\n if (!UUID_RE.test(input.leaseUuid)) {\n throw new TypeError(\n `formatSuccess: leaseUuid must be a UUID; got \"${input.leaseUuid}\"`,\n );\n }\n if (\n input.deployResponse === null ||\n typeof input.deployResponse !== 'object'\n ) {\n throw new TypeError(\n 'formatSuccess: deployResponse must be a non-null object',\n );\n }\n\n const dr = input.deployResponse;\n const providerName =\n typeof dr.provider_uuid === 'string' && dr.provider_uuid.length > 0\n ? dr.provider_uuid\n : '(unknown)';\n const stateName = decodeStateName(dr.state);\n const endpoints = extractRunningEndpoints(dr.connection);\n const ingresses = endpoints\n .map(formatEndpointAsIngress)\n .filter((s): s is string => typeof s === 'string' && s.length > 0);\n // Copilot review fix (PR #58 r3250192778): the custom-domain block's\n // TLS note (\\\"the Ingress URL below works immediately\\\") promises a\n // URL that may not exist when `connection.instances` is empty AND\n // there's no top-level `url`. Compute ingress availability up-front\n // so the custom-domain block can branch its second line accordingly.\n const hasIngress =\n ingresses.length > 0 || (typeof dr.url === 'string' && dr.url.length > 0);\n\n const lines: string[] = [\n 'Deployed.',\n ` Provider: ${providerName}`,\n ` Lease UUID: ${input.leaseUuid}`,\n ` Lease Status: ${stateName}`,\n ];\n\n // Custom-domain block — chain tx confirmed, provider may still be\n // provisioning. Present BEFORE Ingress so the user sees the requested\n // endpoint first, alongside the immediately-working provider FQDN (if\n // any). The TLS note's \"Ingress URL below works immediately\" promise\n // only fires when an Ingress is actually present (r3250192778).\n if (typeof dr.custom_domain === 'string' && dr.custom_domain.length > 0) {\n lines.push(` Custom domain (provisioning): https://${dr.custom_domain}/`);\n lines.push(\n hasIngress\n ? ' — TLS may take a few minutes; the Ingress URL below works immediately.'\n : ' — TLS may take a few minutes.',\n );\n }\n\n if (ingresses.length === 0) {\n // Legacy fallback: when no FQDN can be extracted from `connection`\n // (e.g. providers reporting the older `connection.host` / `ports`\n // shape rather than `connection.instances`), fred may still surface\n // the URL at the top level. `normalizeFredUrl` is the shared helper\n // (mirrored across `classify-deploy-response.ts`, this renderer,\n // and `deploy-app.ts`'s `DeployResult.urls` fallback). The\n // `(none …)` fallback stays for the truly-empty case.\n if (typeof dr.url === 'string' && dr.url.length > 0) {\n lines.push(` Ingress: ${normalizeFredUrl(dr.url)}`);\n } else {\n lines.push(\n ' Ingress: (none — service is internal or no FQDN reported)',\n );\n }\n } else if (ingresses.length === 1) {\n lines.push(` Ingress: ${ingresses[0]}`);\n } else {\n lines.push(' Ingresses:');\n for (const fqdn of ingresses) {\n lines.push(` - ${fqdn}`);\n }\n }\n lines.push('');\n lines.push(\n `For logs / status: /manifest-agent:troubleshoot-deployment ${input.leaseUuid}`,\n );\n\n return lines.join('\\n');\n}\n\n/**\n * Return the user-facing form of a lease state. The `LEASE_STATE_` prefix\n * is stripped for display (e.g. `LEASE_STATE_ACTIVE` → `ACTIVE`). Unknown\n * decodes render as `UNKNOWN(<raw>)` so the raw remains visible; absent\n * state renders as `(unknown)`.\n */\nfunction decodeStateName(state: number | string | undefined): string {\n if (state === undefined) return '(unknown)';\n const canonical = decodeLeaseState(state);\n if (canonical !== undefined) {\n return canonical.slice('LEASE_STATE_'.length);\n }\n return `UNKNOWN(${String(state)})`;\n}\n\n// Re-export for callers that want to walk endpoints themselves without\n// re-importing from `./connection.js`. Keeps `format-success.ts` the\n// single consumer-facing entry for success-rendering plumbing.\nexport type { RunningEndpoint };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,MAAM,UACJ;AAgCF,SAAgB,cAAc,OAAmC;CAC/D,IAAI,CAAC,QAAQ,KAAK,MAAM,SAAS,GAC/B,MAAM,IAAI,UACR,iDAAiD,MAAM,UAAU,EACnE;CAEF,IACE,MAAM,mBAAmB,QACzB,OAAO,MAAM,mBAAmB,UAEhC,MAAM,IAAI,UACR,yDACF;CAGF,MAAM,KAAK,MAAM;CACjB,MAAM,eACJ,OAAO,GAAG,kBAAkB,YAAY,GAAG,cAAc,SAAS,IAC9D,GAAG,gBACH;CACN,MAAM,YAAY,gBAAgB,GAAG,KAAK;CAE1C,MAAM,YADY,wBAAwB,GAAG,UACnB,EACvB,IAAI,uBAAuB,EAC3B,QAAQ,MAAmB,OAAO,MAAM,YAAY,EAAE,SAAS,CAAC;CAMnE,MAAM,aACJ,UAAU,SAAS,KAAM,OAAO,GAAG,QAAQ,YAAY,GAAG,IAAI,SAAS;CAEzE,MAAM,QAAkB;EACtB;EACA,oBAAoB;EACpB,oBAAoB,MAAM;EAC1B,oBAAoB;CACtB;CAOA,IAAI,OAAO,GAAG,kBAAkB,YAAY,GAAG,cAAc,SAAS,GAAG;EACvE,MAAM,KAAK,4CAA4C,GAAG,cAAc,EAAE;EAC1E,MAAM,KACJ,aACI,+EACA,mCACN;CACF;CAEA,IAAI,UAAU,WAAW,GAQvB,IAAI,OAAO,GAAG,QAAQ,YAAY,GAAG,IAAI,SAAS,GAChD,MAAM,KAAK,oBAAoB,iBAAiB,GAAG,GAAG,GAAG;MAEzD,MAAM,KACJ,mEACF;MAEG,IAAI,UAAU,WAAW,GAC9B,MAAM,KAAK,oBAAoB,UAAU,IAAI;MACxC;EACL,MAAM,KAAK,cAAc;EACzB,KAAK,MAAM,QAAQ,WACjB,MAAM,KAAK,SAAS,MAAM;CAE9B;CACA,MAAM,KAAK,EAAE;CACb,MAAM,KACJ,+DAA+D,MAAM,WACvE;CAEA,OAAO,MAAM,KAAK,IAAI;AACxB;;;;;;;AAQA,SAAS,gBAAgB,OAA4C;CACnE,IAAI,UAAU,KAAA,GAAW,OAAO;CAChC,MAAM,YAAYA,OAAiB,KAAK;CACxC,IAAI,cAAc,KAAA,GAChB,OAAO,UAAU,MAAM,EAAqB;CAE9C,OAAO,WAAW,OAAO,KAAK,EAAE;AAClC"}
|