@manifest-network/manifest-agent-core 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +39 -0
  2. package/dist/close-lease.d.ts +33 -0
  3. package/dist/close-lease.d.ts.map +1 -0
  4. package/dist/close-lease.js +138 -0
  5. package/dist/close-lease.js.map +1 -0
  6. package/dist/deploy-app.d.ts +24 -0
  7. package/dist/deploy-app.d.ts.map +1 -0
  8. package/dist/deploy-app.js +446 -0
  9. package/dist/deploy-app.js.map +1 -0
  10. package/dist/index.d.ts +8 -0
  11. package/dist/index.js +7 -0
  12. package/dist/internals/classify-deploy-error.d.ts +41 -0
  13. package/dist/internals/classify-deploy-error.d.ts.map +1 -0
  14. package/dist/internals/classify-deploy-error.js +79 -0
  15. package/dist/internals/classify-deploy-error.js.map +1 -0
  16. package/dist/internals/classify-deploy-response.d.ts +56 -0
  17. package/dist/internals/classify-deploy-response.d.ts.map +1 -0
  18. package/dist/internals/classify-deploy-response.js +33 -0
  19. package/dist/internals/classify-deploy-response.js.map +1 -0
  20. package/dist/internals/connection.d.ts +76 -0
  21. package/dist/internals/connection.d.ts.map +1 -0
  22. package/dist/internals/connection.js +94 -0
  23. package/dist/internals/connection.js.map +1 -0
  24. package/dist/internals/evaluate-readiness.d.ts +55 -0
  25. package/dist/internals/evaluate-readiness.d.ts.map +1 -0
  26. package/dist/internals/evaluate-readiness.js +131 -0
  27. package/dist/internals/evaluate-readiness.js.map +1 -0
  28. package/dist/internals/find-sku-uuid.d.ts +40 -0
  29. package/dist/internals/find-sku-uuid.d.ts.map +1 -0
  30. package/dist/internals/find-sku-uuid.js +20 -0
  31. package/dist/internals/find-sku-uuid.js.map +1 -0
  32. package/dist/internals/format-success.d.ts +35 -0
  33. package/dist/internals/format-success.d.ts.map +1 -0
  34. package/dist/internals/format-success.js +80 -0
  35. package/dist/internals/format-success.js.map +1 -0
  36. package/dist/internals/guarded-fetch.d.ts +138 -0
  37. package/dist/internals/guarded-fetch.d.ts.map +1 -0
  38. package/dist/internals/guarded-fetch.js +242 -0
  39. package/dist/internals/guarded-fetch.js.map +1 -0
  40. package/dist/internals/humanize-denom.d.ts +45 -0
  41. package/dist/internals/humanize-denom.d.ts.map +1 -0
  42. package/dist/internals/humanize-denom.js +105 -0
  43. package/dist/internals/humanize-denom.js.map +1 -0
  44. package/dist/internals/inspect-image.d.ts +31 -0
  45. package/dist/internals/inspect-image.d.ts.map +1 -0
  46. package/dist/internals/inspect-image.js +345 -0
  47. package/dist/internals/inspect-image.js.map +1 -0
  48. package/dist/internals/lease-items.d.ts +46 -0
  49. package/dist/internals/lease-items.d.ts.map +1 -0
  50. package/dist/internals/lease-items.js +58 -0
  51. package/dist/internals/lease-items.js.map +1 -0
  52. package/dist/internals/lease-state.d.ts +32 -0
  53. package/dist/internals/lease-state.d.ts.map +1 -0
  54. package/dist/internals/lease-state.js +80 -0
  55. package/dist/internals/lease-state.js.map +1 -0
  56. package/dist/internals/render-deployment-plan.d.ts +22 -0
  57. package/dist/internals/render-deployment-plan.d.ts.map +1 -0
  58. package/dist/internals/render-deployment-plan.js +135 -0
  59. package/dist/internals/render-deployment-plan.js.map +1 -0
  60. package/dist/internals/render-intent-recap.d.ts +43 -0
  61. package/dist/internals/render-intent-recap.d.ts.map +1 -0
  62. package/dist/internals/render-intent-recap.js +136 -0
  63. package/dist/internals/render-intent-recap.js.map +1 -0
  64. package/dist/internals/render-partial-success-prompt.d.ts +26 -0
  65. package/dist/internals/render-partial-success-prompt.d.ts.map +1 -0
  66. package/dist/internals/render-partial-success-prompt.js +53 -0
  67. package/dist/internals/render-partial-success-prompt.js.map +1 -0
  68. package/dist/internals/save-manifest.d.ts +105 -0
  69. package/dist/internals/save-manifest.d.ts.map +1 -0
  70. package/dist/internals/save-manifest.js +122 -0
  71. package/dist/internals/save-manifest.js.map +1 -0
  72. package/dist/internals/secret-denylist.d.ts +42 -0
  73. package/dist/internals/secret-denylist.d.ts.map +1 -0
  74. package/dist/internals/secret-denylist.js +59 -0
  75. package/dist/internals/secret-denylist.js.map +1 -0
  76. package/dist/internals/spec-normalize.d.ts +84 -0
  77. package/dist/internals/spec-normalize.d.ts.map +1 -0
  78. package/dist/internals/spec-normalize.js +169 -0
  79. package/dist/internals/spec-normalize.js.map +1 -0
  80. package/dist/internals/verify-domain-state.d.ts +20 -0
  81. package/dist/internals/verify-domain-state.d.ts.map +1 -0
  82. package/dist/internals/verify-domain-state.js +63 -0
  83. package/dist/internals/verify-domain-state.js.map +1 -0
  84. package/dist/internals/verify-recover.d.ts +120 -0
  85. package/dist/internals/verify-recover.d.ts.map +1 -0
  86. package/dist/internals/verify-recover.js +91 -0
  87. package/dist/internals/verify-recover.js.map +1 -0
  88. package/dist/manage-domain.d.ts +36 -0
  89. package/dist/manage-domain.d.ts.map +1 -0
  90. package/dist/manage-domain.js +230 -0
  91. package/dist/manage-domain.js.map +1 -0
  92. package/dist/troubleshoot.d.ts +23 -0
  93. package/dist/troubleshoot.d.ts.map +1 -0
  94. package/dist/troubleshoot.js +124 -0
  95. package/dist/troubleshoot.js.map +1 -0
  96. package/dist/types.d.ts +294 -0
  97. package/dist/types.d.ts.map +1 -0
  98. package/dist/types.js +0 -0
  99. package/package.json +56 -0
@@ -0,0 +1,79 @@
1
+ //#region src/internals/classify-deploy-error.ts
2
+ /**
3
+ * Classify the MCP error envelope thrown by `mcp__manifest-fred__deploy_app`
4
+ * when the call fails AFTER the create-lease tx already confirmed.
5
+ *
6
+ * Companion to `classify-deploy-response.ts`: that file handles the RETURN
7
+ * path; this file handles the THROW path. The split exists because
8
+ * `manifest-mcp-fred` 0.8.0 `deployApp` throws `ManifestMCPError` with the
9
+ * message prefix `Deploy partially succeeded: lease ${uuid} was created
10
+ * but subsequent steps failed.` and `details.lease_uuid` populated when
11
+ * create-lease succeeded but something downstream (set-domain, manifest
12
+ * upload, readiness poll) fell over.
13
+ *
14
+ * Recognised input envelope shapes:
15
+ * - `{ message, details?, code? }`
16
+ * - `{ error: { message, details?, code? } }`
17
+ *
18
+ * Returns deterministically — never throws. A malformed envelope is
19
+ * classified as `outcome: 'failed'` with a stable `reason`, so the
20
+ * orchestrator can branch on the JSON without an outer try/catch.
21
+ *
22
+ * `outcome: 'partially_succeeded'` triggers ONLY when `err.message` starts
23
+ * with the exact prefix `Deploy partially succeeded:`. Looser matching
24
+ * would risk false positives on wrapper errors that happen to contain the
25
+ * phrase nested inside other text.
26
+ */
27
+ /** Permissive UUID pattern (RFC-4122 8-4-4-4-12, version byte lenient). */
28
+ const UUID_PATTERN = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
29
+ const PARTIAL_PREFIX = "Deploy partially succeeded:";
30
+ /**
31
+ * Pick the inner envelope when the error is wrapped as `{ error: {...} }`.
32
+ * `JSON.stringify(err)` produces this shape in some SDKs.
33
+ */
34
+ function pickEnvelope(raw) {
35
+ if (raw !== null && typeof raw === "object") {
36
+ const r = raw;
37
+ if (r.error !== null && typeof r.error === "object") return r.error;
38
+ }
39
+ return raw;
40
+ }
41
+ function classifyDeployError(err, opts = {}) {
42
+ const expectedCustomDomain = opts.expectedCustomDomain;
43
+ const e = pickEnvelope(err);
44
+ if (e === null || typeof e !== "object") return finalize({
45
+ outcome: "failed",
46
+ reason: "stdin envelope is not an object"
47
+ }, expectedCustomDomain);
48
+ const envelope = e;
49
+ const message = typeof envelope.message === "string" ? envelope.message : "";
50
+ const details = envelope.details !== null && typeof envelope.details === "object" && !Array.isArray(envelope.details) ? envelope.details : {};
51
+ if (message.startsWith(PARTIAL_PREFIX)) {
52
+ let leaseUuid;
53
+ if (typeof details.lease_uuid === "string") leaseUuid = details.lease_uuid;
54
+ else {
55
+ const m = message.match(UUID_PATTERN);
56
+ if (m) leaseUuid = m[0];
57
+ }
58
+ return finalize({
59
+ outcome: "partially_succeeded",
60
+ ...leaseUuid !== void 0 && { leaseUuid },
61
+ reason: message
62
+ }, expectedCustomDomain);
63
+ }
64
+ return finalize({
65
+ outcome: "failed",
66
+ reason: message || "deploy_app threw an empty error"
67
+ }, expectedCustomDomain);
68
+ }
69
+ function finalize(base, expectedCustomDomain) {
70
+ if (expectedCustomDomain !== void 0) return {
71
+ ...base,
72
+ requestedCustomDomain: expectedCustomDomain
73
+ };
74
+ return base;
75
+ }
76
+ //#endregion
77
+ export { classifyDeployError };
78
+
79
+ //# sourceMappingURL=classify-deploy-error.js.map
@@ -0,0 +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` 0.8.0 `deployApp` throws `ManifestMCPError` with the\n * message prefix `Deploy partially succeeded: lease ${uuid} was created\n * but subsequent steps failed.` and `details.lease_uuid` populated when\n * create-lease succeeded but something downstream (set-domain, manifest\n * upload, readiness poll) fell over.\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 ONLY when `err.message` starts\n * with the exact prefix `Deploy partially succeeded:`. Looser matching\n * would risk false positives on wrapper errors that happen to contain the\n * 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: EXACT upstream prefix. Anything looser risks\n // mis-classifying wrapper errors whose message merely contains the\n // phrase as a substring (defended by case #5 in the CJS test).\n if (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 reason: message,\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,MAAM,eACJ;AAEF,MAAM,iBAAiB;;;;;AAgBvB,SAAS,aAAa,KAAuB;AAC3C,KAAI,QAAQ,QAAQ,OAAO,QAAQ,UAAU;EAC3C,MAAM,IAAI;AACV,MAAI,EAAE,UAAU,QAAQ,OAAO,EAAE,UAAU,SAAU,QAAO,EAAE;;AAEhE,QAAO;;AAGT,SAAgB,oBACd,KACA,OAA0C,EAAE,EACjB;CAC3B,MAAM,uBAAuB,KAAK;CAClC,MAAM,IAAI,aAAa,IAAI;AAE3B,KAAI,MAAM,QAAQ,OAAO,MAAM,SAC7B,QAAO,SACL;EACE,SAAS;EACT,QAAQ;EACT,EACD,qBACD;CAGH,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,QAAQ,GAC3B,SAAS,UACV,EAAE;AAKR,KAAI,QAAQ,WAAW,eAAe,EAAE;EACtC,IAAI;AACJ,MAAI,OAAO,QAAQ,eAAe,SAChC,aAAY,QAAQ;OACf;GACL,MAAM,IAAI,QAAQ,MAAM,aAAa;AACrC,OAAI,EAAG,aAAY,EAAE;;AAEvB,SAAO,SACL;GACE,SAAS;GACT,GAAI,cAAc,KAAA,KAAa,EAAE,WAAW;GAC5C,QAAQ;GACT,EACD,qBACD;;AAKH,QAAO,SACL;EACE,SAAS;EACT,QAAQ,WAAW;EACpB,EACD,qBACD;;AAGH,SAAS,SACP,MACA,sBAC2B;AAC3B,KAAI,yBAAyB,KAAA,EAC3B,QAAO;EAAE,GAAG;EAAM,uBAAuB;EAAsB;AAEjE,QAAO"}
@@ -0,0 +1,56 @@
1
+ //#region src/internals/classify-deploy-response.d.ts
2
+ /**
3
+ * Classify the RETURN envelope of `mcp__manifest-fred__deploy_app` into one
4
+ * of three outcomes for the orchestrator to branch on.
5
+ *
6
+ * Companion to `classify-deploy-error.ts` (which handles the THROW path).
7
+ *
8
+ * Outcomes:
9
+ * - `'active'` — state is LEASE_STATE_ACTIVE AND at least one
10
+ * running instance exists. Internal-only deploys
11
+ * (every port `ingress: false`) have running
12
+ * instances but no FQDN, so the URL count alone
13
+ * can't gate this; `hasRunningInstances` covers it.
14
+ * Orchestrator can skip `wait_for_app_ready`.
15
+ * - `'needs_wait'` — lease created but not yet active, OR no running
16
+ * instances yet (provider hasn't started the
17
+ * container). Orchestrator polls `wait_for_app_ready`.
18
+ * - `'failed'` — no `lease_uuid` present, OR state is a terminal
19
+ * failure state (CLOSED / REJECTED / EXPIRED, plus
20
+ * the legacy INSUFFICIENT_FUNDS defense-in-depth).
21
+ * Orchestrator routes to troubleshoot/cleanup.
22
+ *
23
+ * Terminal-state set is the union of `TERMINAL_STATES` from `lease-state.ts`
24
+ * — extended from the CJS's `{CLOSED, INSUFFICIENT_FUNDS}` to also cover
25
+ * `REJECTED` and `EXPIRED` since those are emitted by manifestjs 2.4.1
26
+ * (chain v2.1.0) as terminal states. INSUFFICIENT_FUNDS is unreachable
27
+ * from `decode()` on the current chain but retained as defense-in-depth.
28
+ * See `lease-state.ts` for the divergence rationale.
29
+ *
30
+ * Error summary format (qa-engineer's parity pin):
31
+ * - Lease present + terminal: `Lease ${leaseUuid} reached terminal state ${stateName || 'UNKNOWN'}`
32
+ * - Lease missing: `deploy_app returned no lease_uuid`
33
+ * - `connectionError` string present: passed through verbatim
34
+ */
35
+ interface DeployResponseShape {
36
+ lease_uuid?: unknown;
37
+ provider_uuid?: unknown;
38
+ provider_url?: unknown;
39
+ state?: unknown;
40
+ url?: unknown;
41
+ connection?: unknown;
42
+ connectionError?: unknown;
43
+ }
44
+ interface DeployResponseClassification {
45
+ outcome: 'active' | 'needs_wait' | 'failed';
46
+ leaseUuid?: string;
47
+ providerUuid?: string;
48
+ providerUrl?: string;
49
+ urls: string[];
50
+ stateName?: string;
51
+ errorSummary?: string;
52
+ }
53
+ declare function classifyDeployResponse(response: DeployResponseShape): DeployResponseClassification;
54
+ //#endregion
55
+ export { DeployResponseClassification, DeployResponseShape, classifyDeployResponse };
56
+ //# sourceMappingURL=classify-deploy-response.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify-deploy-response.d.ts","names":[],"sources":["../../src/internals/classify-deploy-response.ts"],"mappings":";;AA0CA;;;;;;;;;;;;;AAUA;;;;;;;;;;;;;AAUA;;;;;;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,4BAAA"}
@@ -0,0 +1,33 @@
1
+ import { decode, isTerminal } from "./lease-state.js";
2
+ import { extractRunningEndpoints, formatEndpointAsUrl, hasRunningInstances, normalizeFredUrl } from "./connection.js";
3
+ //#region src/internals/classify-deploy-response.ts
4
+ function classifyDeployResponse(response) {
5
+ const stateName = decode(typeof response.state === "number" || typeof response.state === "string" ? response.state : void 0);
6
+ const urls = extractRunningEndpoints(response.connection).map(formatEndpointAsUrl);
7
+ if (typeof response.url === "string" && response.url.length > 0) {
8
+ const u = normalizeFredUrl(response.url);
9
+ if (u.length > 0 && !urls.includes(u)) urls.unshift(u);
10
+ }
11
+ const leaseUuid = typeof response.lease_uuid === "string" ? response.lease_uuid : void 0;
12
+ let outcome;
13
+ if (!leaseUuid) outcome = "failed";
14
+ else if (stateName === "LEASE_STATE_ACTIVE" && (urls.length > 0 || hasRunningInstances(response.connection))) outcome = "active";
15
+ else if (stateName !== void 0 && isTerminal(stateName)) outcome = "failed";
16
+ else outcome = "needs_wait";
17
+ const out = {
18
+ outcome,
19
+ ...leaseUuid !== void 0 && { leaseUuid },
20
+ ...typeof response.provider_uuid === "string" && { providerUuid: response.provider_uuid },
21
+ ...typeof response.provider_url === "string" && { providerUrl: response.provider_url },
22
+ urls,
23
+ ...stateName !== void 0 && { stateName }
24
+ };
25
+ if (outcome === "failed") if (typeof response.connectionError === "string") out.errorSummary = response.connectionError;
26
+ else if (!leaseUuid) out.errorSummary = "deploy_app returned no lease_uuid";
27
+ else out.errorSummary = `Lease ${leaseUuid} reached terminal state ${stateName || "UNKNOWN"}`;
28
+ return out;
29
+ }
30
+ //#endregion
31
+ export { classifyDeployResponse };
32
+
33
+ //# sourceMappingURL=classify-deploy-response.js.map
@@ -0,0 +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,EACL;CAGD,MAAM,OAAiB,wBAAwB,SAAS,WAAW,CAAC,IAClE,oBACD;AACD,KAAI,OAAO,SAAS,QAAQ,YAAY,SAAS,IAAI,SAAS,GAAG;EAC/D,MAAM,IAAI,iBAAiB,SAAS,IAAI;AACxC,MAAI,EAAE,SAAS,KAAK,CAAC,KAAK,SAAS,EAAE,CAAE,MAAK,QAAQ,EAAE;;CAGxD,MAAM,YACJ,OAAO,SAAS,eAAe,WAAW,SAAS,aAAa,KAAA;CAElE,IAAI;AACJ,KAAI,CAAC,UACH,WAAU;UAEV,cAAc,yBACb,KAAK,SAAS,KAAK,oBAAoB,SAAS,WAAW,EAE5D,WAAU;UACD,cAAc,KAAA,KAAa,WAAW,UAAU,CACzD,WAAU;KAGV,WAAU;CAGZ,MAAM,MAAoC;EACxC;EACA,GAAI,cAAc,KAAA,KAAa,EAAE,WAAW;EAC5C,GAAI,OAAO,SAAS,kBAAkB,YAAY,EAChD,cAAc,SAAS,eACxB;EACD,GAAI,OAAO,SAAS,iBAAiB,YAAY,EAC/C,aAAa,SAAS,cACvB;EACD;EACA,GAAI,cAAc,KAAA,KAAa,EAAE,WAAW;EAC7C;AAED,KAAI,YAAY,SACd,KAAI,OAAO,SAAS,oBAAoB,SACtC,KAAI,eAAe,SAAS;UACnB,CAAC,UACV,KAAI,eAAe;KAInB,KAAI,eAAe,SAAS,UAAU,0BACpC,aAAa;AAKnB,QAAO"}
@@ -0,0 +1,76 @@
1
+ //#region src/internals/connection.d.ts
2
+ /**
3
+ * Helpers for walking the provider's `connection` payload returned by
4
+ * `mcp__manifest-fred__deploy_app` / `app_status` / `wait_for_app_ready`.
5
+ *
6
+ * The provider emits instance lists in one or both of:
7
+ * - top-level `connection.instances[]` (single-service / non-services-map shape)
8
+ * - per-service `connection.services.<name>.instances[]` (stack /
9
+ * services-map shape — emitted whenever the spec uses services-map form,
10
+ * which `build_manifest_preview` does even for single-service deploys to
11
+ * enable per-port `ingress: bool`)
12
+ *
13
+ * Subdomain-based routing on the provider: port is NOT part of the URL.
14
+ * One user-facing URL per FQDN regardless of container port count.
15
+ *
16
+ * Unrecognized payload shape: returns `[]` and invokes the logger
17
+ * (defaults to `console.warn`; override via `opts.logger`) so a future
18
+ * provider-shape divergence is loud rather than silent. The CJS uses
19
+ * `process.stderr.write`; the TS port surfaces via an injectable logger
20
+ * to keep agent-core platform-neutral while preserving the always-loud
21
+ * CJS posture by default.
22
+ */
23
+ interface RunningEndpoint {
24
+ readonly fqdn: string;
25
+ }
26
+ interface ConnectionWalkOptions {
27
+ /**
28
+ * Sink for warnings about unrecognized connection shapes. Defaults to
29
+ * `console.warn` (Web Standard; platform-neutral across Node, browsers,
30
+ * Deno, Bun) so a future provider-shape divergence is loud rather than
31
+ * silent. Host surfaces that want to route elsewhere (structured
32
+ * stderr, UI toast, log file) can override; surfaces that want to
33
+ * suppress entirely can pass `() => {}` explicitly — silence becomes a
34
+ * consumer-controlled opt-out instead of the easy-to-forget default.
35
+ */
36
+ logger?: (reason: string) => void;
37
+ }
38
+ /**
39
+ * Returns a deduped list of running instances (status === 'running' AND
40
+ * `fqdn` populated) found anywhere under `connection.instances` or
41
+ * `connection.services.<name>.instances`.
42
+ */
43
+ declare function extractRunningEndpoints(connection: unknown, opts?: ConnectionWalkOptions): RunningEndpoint[];
44
+ /** Render an endpoint as a bare FQDN string (for ingress lists). */
45
+ declare function formatEndpointAsIngress(ep: RunningEndpoint): string;
46
+ /** Render an endpoint as a full `https://<fqdn>/` URL. */
47
+ declare function formatEndpointAsUrl(ep: RunningEndpoint): string;
48
+ /**
49
+ * Normalize fred's top-level `url` field to a full `http(s)://...`
50
+ * string. Defensive fallback for the legacy `connection.host` / `ports`
51
+ * shape: fred surfaces a top-level `url` when no `connection.instances`
52
+ * FQDN is available, and the value may or may not carry a scheme.
53
+ *
54
+ * Mirrors the inline logic that lived in three call sites
55
+ * (`classify-deploy-response.ts:76-80`, `format-success.ts` ingress
56
+ * fallback, `deploy-app.ts` `DeployResult.urls` fallback) — factored
57
+ * here so all three share one source of truth.
58
+ *
59
+ * - Returns `''` for empty input (caller branches into a different
60
+ * render path if needed).
61
+ * - Passes through unchanged if already prefixed `http://` or
62
+ * `https://` (case-insensitive).
63
+ * - Otherwise wraps as `https://${raw}/`.
64
+ */
65
+ declare function normalizeFredUrl(raw: string): string;
66
+ /**
67
+ * True iff any instance anywhere in the connection payload has
68
+ * `status === 'running'`, regardless of `fqdn`. Used by
69
+ * `classify-deploy-response.ts` to recognize internal-only deploys
70
+ * (every port `ingress: false`) as `active` rather than misclassifying
71
+ * them as `needs_wait` because they have no public URLs to surface.
72
+ */
73
+ declare function hasRunningInstances(connection: unknown): boolean;
74
+ //#endregion
75
+ export { ConnectionWalkOptions, RunningEndpoint, extractRunningEndpoints, formatEndpointAsIngress, formatEndpointAsUrl, hasRunningInstances, normalizeFredUrl };
76
+ //# sourceMappingURL=connection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection.d.ts","names":[],"sources":["../../src/internals/connection.ts"],"mappings":";;AAsBA;;;;;AAIA;;;;;AAkBA;;;;;;;;;;UAtBiB,eAAA;EAAA,SACN,IAAA;AAAA;AAAA,UAGM,qBAAA;EAuE0C;AAK3D;;;;;AAqBA;;;EAvFE,MAAA,IAAU,MAAA;AAAA;AAmGZ;;;;;AAAA,iBA3FgB,uBAAA,CACd,UAAA,WACA,IAAA,GAAM,qBAAA,GACL,eAAA;;iBAkDa,uBAAA,CAAwB,EAAA,EAAI,eAAA;;iBAK5B,mBAAA,CAAoB,EAAA,EAAI,eAAA;;;;;;;;;;;;;;;;;;iBAqBxB,gBAAA,CAAiB,GAAA;;;;;;;;iBAYjB,mBAAA,CAAoB,UAAA"}
@@ -0,0 +1,94 @@
1
+ //#region src/internals/connection.ts
2
+ /**
3
+ * Returns a deduped list of running instances (status === 'running' AND
4
+ * `fqdn` populated) found anywhere under `connection.instances` or
5
+ * `connection.services.<name>.instances`.
6
+ */
7
+ function extractRunningEndpoints(connection, opts = {}) {
8
+ if (!isPlainObject(connection)) return [];
9
+ const seen = /* @__PURE__ */ new Set();
10
+ const endpoints = [];
11
+ const pushFromInstances = (instances) => {
12
+ if (!Array.isArray(instances)) return;
13
+ for (const inst of instances) {
14
+ if (!isPlainObject(inst)) continue;
15
+ if (inst.status !== "running") continue;
16
+ if (typeof inst.fqdn !== "string" || inst.fqdn.length === 0) continue;
17
+ if (seen.has(inst.fqdn)) continue;
18
+ seen.add(inst.fqdn);
19
+ endpoints.push({ fqdn: inst.fqdn });
20
+ }
21
+ };
22
+ pushFromInstances(connection.instances);
23
+ const services = connection.services;
24
+ if (isPlainObject(services)) {
25
+ for (const svc of Object.values(services)) if (isPlainObject(svc)) pushFromInstances(svc.instances);
26
+ }
27
+ const ownKeys = Object.keys(connection);
28
+ if (!(ownKeys.includes("instances") || ownKeys.includes("services"))) {
29
+ const keys = Object.keys(connection).slice(0, 8).join(", ") || "(empty)";
30
+ (opts.logger ?? defaultLogger)(`connection: unrecognized shape (no 'instances' or 'services' key found; keys present: ${keys}). Returning empty endpoints — the orchestrator will report no ingresses for this lease. Provider may have shipped a new shape; check manifest-mcp-fred ConnectionDetails.`);
31
+ }
32
+ return endpoints;
33
+ }
34
+ /** Render an endpoint as a bare FQDN string (for ingress lists). */
35
+ function formatEndpointAsIngress(ep) {
36
+ return ep.fqdn;
37
+ }
38
+ /** Render an endpoint as a full `https://<fqdn>/` URL. */
39
+ function formatEndpointAsUrl(ep) {
40
+ return `https://${ep.fqdn}/`;
41
+ }
42
+ /**
43
+ * Normalize fred's top-level `url` field to a full `http(s)://...`
44
+ * string. Defensive fallback for the legacy `connection.host` / `ports`
45
+ * shape: fred surfaces a top-level `url` when no `connection.instances`
46
+ * FQDN is available, and the value may or may not carry a scheme.
47
+ *
48
+ * Mirrors the inline logic that lived in three call sites
49
+ * (`classify-deploy-response.ts:76-80`, `format-success.ts` ingress
50
+ * fallback, `deploy-app.ts` `DeployResult.urls` fallback) — factored
51
+ * here so all three share one source of truth.
52
+ *
53
+ * - Returns `''` for empty input (caller branches into a different
54
+ * render path if needed).
55
+ * - Passes through unchanged if already prefixed `http://` or
56
+ * `https://` (case-insensitive).
57
+ * - Otherwise wraps as `https://${raw}/`.
58
+ */
59
+ function normalizeFredUrl(raw) {
60
+ if (raw.length === 0) return "";
61
+ return /^https?:\/\//i.test(raw) ? raw : `https://${raw}/`;
62
+ }
63
+ /**
64
+ * True iff any instance anywhere in the connection payload has
65
+ * `status === 'running'`, regardless of `fqdn`. Used by
66
+ * `classify-deploy-response.ts` to recognize internal-only deploys
67
+ * (every port `ingress: false`) as `active` rather than misclassifying
68
+ * them as `needs_wait` because they have no public URLs to surface.
69
+ */
70
+ function hasRunningInstances(connection) {
71
+ if (!isPlainObject(connection)) return false;
72
+ const runs = (instances) => Array.isArray(instances) && instances.some((i) => isPlainObject(i) && i.status === "running");
73
+ if (runs(connection.instances)) return true;
74
+ const services = connection.services;
75
+ if (isPlainObject(services)) {
76
+ for (const svc of Object.values(services)) if (isPlainObject(svc) && runs(svc.instances)) return true;
77
+ }
78
+ return false;
79
+ }
80
+ function isPlainObject(value) {
81
+ return value !== null && typeof value === "object" && !Array.isArray(value);
82
+ }
83
+ /**
84
+ * Default logger — `console.warn`. Defined as a module-level constant so
85
+ * test code can spy on it (`vi.spyOn(console, 'warn')`) without races
86
+ * around the import order of the binding.
87
+ */
88
+ const defaultLogger = (reason) => {
89
+ console.warn(reason);
90
+ };
91
+ //#endregion
92
+ export { extractRunningEndpoints, formatEndpointAsIngress, formatEndpointAsUrl, hasRunningInstances, normalizeFredUrl };
93
+
94
+ //# sourceMappingURL=connection.js.map
@@ -0,0 +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,EAAE,EACb;AACnB,KAAI,CAAC,cAAc,WAAW,CAAE,QAAO,EAAE;CACzC,MAAM,uBAAO,IAAI,KAAa;CAC9B,MAAM,YAA+B,EAAE;CAEvC,MAAM,qBAAqB,cAA6B;AACtD,MAAI,CAAC,MAAM,QAAQ,UAAU,CAAE;AAC/B,OAAK,MAAM,QAAQ,WAAW;AAC5B,OAAI,CAAC,cAAc,KAAK,CAAE;AAC1B,OAAI,KAAK,WAAW,UAAW;AAC/B,OAAI,OAAO,KAAK,SAAS,YAAY,KAAK,KAAK,WAAW,EAAG;AAC7D,OAAI,KAAK,IAAI,KAAK,KAAK,CAAE;AACzB,QAAK,IAAI,KAAK,KAAK;AACnB,aAAU,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;;;AAIvC,mBAAkB,WAAW,UAAU;CAEvC,MAAM,WAAW,WAAW;AAC5B,KAAI,cAAc,SAAS;OACpB,MAAM,OAAO,OAAO,OAAO,SAAS,CACvC,KAAI,cAAc,IAAI,CAAE,mBAAkB,IAAI,UAAU;;CAW5D,MAAM,UAAU,OAAO,KAAK,WAAW;AAGvC,KAAI,EADF,QAAQ,SAAS,YAAY,IAAI,QAAQ,SAAS,WAAW,GAC1C;EACnB,MAAM,OAAO,OAAO,KAAK,WAAW,CAAC,MAAM,GAAG,EAAE,CAAC,KAAK,KAAK,IAAI;AAE/D,GADe,KAAK,UAAU,eAE5B,yFAAyF,KAAK,4KAG/F;;AAGH,QAAO;;;AAIT,SAAgB,wBAAwB,IAA6B;AACnE,QAAO,GAAG;;;AAIZ,SAAgB,oBAAoB,IAA6B;AAC/D,QAAO,WAAW,GAAG,KAAK;;;;;;;;;;;;;;;;;;;AAoB5B,SAAgB,iBAAiB,KAAqB;AACpD,KAAI,IAAI,WAAW,EAAG,QAAO;AAC7B,QAAO,gBAAgB,KAAK,IAAI,GAAG,MAAM,WAAW,IAAI;;;;;;;;;AAU1D,SAAgB,oBAAoB,YAA8B;AAChE,KAAI,CAAC,cAAc,WAAW,CAAE,QAAO;CACvC,MAAM,QAAQ,cACZ,MAAM,QAAQ,UAAU,IACxB,UAAU,MAAM,MAAM,cAAc,EAAE,IAAI,EAAE,WAAW,UAAU;AACnE,KAAI,KAAK,WAAW,UAAU,CAAE,QAAO;CACvC,MAAM,WAAW,WAAW;AAC5B,KAAI,cAAc,SAAS;OACpB,MAAM,OAAO,OAAO,OAAO,SAAS,CACvC,KAAI,cAAc,IAAI,IAAI,KAAK,IAAI,UAAU,CAAE,QAAO;;AAG1D,QAAO;;AAGT,SAAS,cAAc,OAAkD;AACvE,QAAO,UAAU,QAAQ,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,MAAM;;;;;;;AAQ7E,MAAM,iBAA2C,WAAW;AAC1D,SAAQ,KAAK,OAAO"}
@@ -0,0 +1,55 @@
1
+ import { Coin, DenomMap, Readiness } from "../types.js";
2
+ //#region src/internals/evaluate-readiness.d.ts
3
+ /**
4
+ * Inputs passed to `evaluateReadiness`. camelCase throughout — high-level
5
+ * callers translate the snake_case MCP response shape before invocation.
6
+ */
7
+ interface EvaluateReadinessInputs {
8
+ /** Tenant address (bech32). Not consumed by the algorithm; included for journal/log context. */
9
+ tenant: string;
10
+ /** Image ref being considered (may be `null` when only the size is selected). */
11
+ image: string | null;
12
+ /** SKU size string the caller wants (`'docker-micro'`, etc.). `null` when not yet chosen. */
13
+ size: string | null;
14
+ /** Wallet bank balances. */
15
+ walletBalances: Coin[];
16
+ /** Credit account data, or `null` when no credit account is funded. */
17
+ credits: {
18
+ availableBalances?: Coin[]; /** Older response variant — fallback when `availableBalances` is absent. */
19
+ balances?: Coin[]; /** Live current credit balance(s) when the tenant has at least one active lease. */
20
+ currentBalance?: Coin[]; /** Hours of runtime at the user's current overall burn rate (string-encoded number). */
21
+ hoursRemaining?: string;
22
+ } | null;
23
+ /** Chosen SKU + price, or `null` when no size selected. */
24
+ sku: {
25
+ name: string;
26
+ price: Coin;
27
+ } | null;
28
+ /** All active SKU names the chain currently advertises. */
29
+ availableSkuNames: string[];
30
+ /** Gas-price string (e.g. `'1umfx'`, `'0.37upwr'`). Required — drives the wallet-gas check denom. */
31
+ gasPrice: string;
32
+ /** Override the per-denom warn floor (smallest unit). When omitted, uses the per-denom default or 50_000n fallback. */
33
+ gasWarnFloor?: bigint;
34
+ /**
35
+ * Pre-loaded `DenomMap` for symbol humanization. The orchestrator
36
+ * (`deploy-app.ts` and PR-4 callers) is responsible for composing the
37
+ * map via `loadChainDenomMap(chainDataFile)` and passing it in. This
38
+ * keeps `evaluateReadiness` pure-sync — I/O lives at the orchestrator
39
+ * boundary, not inside the decision function (post-Q4 Bii verdict).
40
+ *
41
+ * When omitted, the no-op map is used; balances + SKU prices render
42
+ * with raw on-chain denoms.
43
+ */
44
+ denomMap?: DenomMap;
45
+ }
46
+ /**
47
+ * Compute the `Readiness` verdict for a prospective deployment.
48
+ *
49
+ * Throws `TypeError` on malformed `gasPrice` (the only input field whose
50
+ * runtime shape isn't enforced by the typed signature).
51
+ */
52
+ declare function evaluateReadiness(inputs: EvaluateReadinessInputs): Readiness;
53
+ //#endregion
54
+ export { EvaluateReadinessInputs, evaluateReadiness };
55
+ //# sourceMappingURL=evaluate-readiness.d.ts.map
@@ -0,0 +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;;;AASb;;;;;;;EATE,QAAA,GAAW,QAAA;AAAA;;;;;;;iBASG,iBAAA,CAAkB,MAAA,EAAQ,uBAAA,GAA0B,SAAA"}
@@ -0,0 +1,131 @@
1
+ import { EMPTY_DENOM_MAP, denomToSymbol, humanizeCoin } from "./humanize-denom.js";
2
+ //#region src/internals/evaluate-readiness.ts
3
+ /**
4
+ * Evaluate `check_deployment_readiness` MCP response data into the frozen
5
+ * `Readiness` shape (camelCase typed input + the `Readiness` contract from
6
+ * ENG-128).
7
+ *
8
+ * Thresholds are encoded here (not in skill prose or caller config) so the
9
+ * rules stay consistent across runs:
10
+ * - HOURS_REMAINING_WARN_FLOOR = 24
11
+ * - GAS_BALANCE_WARN_FLOOR (per-denom) = 50_000n umfx | upwr
12
+ *
13
+ * Status semantics (CJS-parity):
14
+ * - `'block'` — cannot proceed (SKU unavailable, wallet empty)
15
+ * - `'warn'` — proceedable but risky (low credits, low gas balance, no credit account)
16
+ * - `'ok'` — silent pass
17
+ *
18
+ * `suggestedActions` are semantic tokens from the frozen `ReadinessAction`
19
+ * union — not prose for the user. Surfaces map these to UI affordances.
20
+ *
21
+ * Walked-from-CJS field-rename: the MCP response uses snake_case
22
+ * (`wallet_balances`, `available_balances`, `hours_remaining`,
23
+ * `available_sku_names`, `current_balance`); the TS-port input is
24
+ * camelCase, and high-level callers (PR 3's `deployApp`) translate the
25
+ * snake_case wire shape into camelCase before passing in.
26
+ */
27
+ const HOURS_REMAINING_WARN_FLOOR = 24;
28
+ const GAS_BALANCE_WARN_FLOOR_DEFAULTS = {
29
+ umfx: 50000n,
30
+ upwr: 50000n
31
+ };
32
+ const GAS_BALANCE_WARN_FLOOR_FALLBACK = 50000n;
33
+ /**
34
+ * Cosmos convention for gas-price strings: leading numeric (digits +
35
+ * optional decimal point), then the denom. Denom grammar mirrors
36
+ * `sdk.ValidateDenom`: `[a-zA-Z][a-zA-Z0-9/:._-]{2,127}`.
37
+ * Anchored both ends so trailing whitespace fails fast.
38
+ */
39
+ const GAS_PRICE_RE = /^[0-9]+(?:\.[0-9]+)?([a-zA-Z][a-zA-Z0-9/:._-]{2,127})$/;
40
+ /**
41
+ * Compute the `Readiness` verdict for a prospective deployment.
42
+ *
43
+ * Throws `TypeError` on malformed `gasPrice` (the only input field whose
44
+ * runtime shape isn't enforced by the typed signature).
45
+ */
46
+ function evaluateReadiness(inputs) {
47
+ const gasDenomMatch = inputs.gasPrice.match(GAS_PRICE_RE);
48
+ if (!gasDenomMatch || gasDenomMatch[1] === void 0) throw new TypeError(`evaluateReadiness: gasPrice must match <numeric><denom> (e.g. "1umfx" or "0.37upwr"); got "${inputs.gasPrice}"`);
49
+ const gasDenom = gasDenomMatch[1];
50
+ const gasWarnFloor = inputs.gasWarnFloor !== void 0 ? validateGasWarnFloor(inputs.gasWarnFloor) : GAS_BALANCE_WARN_FLOOR_DEFAULTS[gasDenom] ?? GAS_BALANCE_WARN_FLOOR_FALLBACK;
51
+ const denomMap = inputs.denomMap ?? EMPTY_DENOM_MAP;
52
+ const reasons = [];
53
+ const actions = /* @__PURE__ */ new Set();
54
+ let status = "ok";
55
+ if (inputs.size !== null && !inputs.availableSkuNames.includes(inputs.size)) {
56
+ status = "block";
57
+ const available = inputs.availableSkuNames.length > 0 ? inputs.availableSkuNames.join(", ") : "(none)";
58
+ reasons.push(`Requested SKU "${inputs.size}" is not currently offered. Available: ${available}.`);
59
+ actions.add("pick_different_sku");
60
+ }
61
+ const gasEntry = inputs.walletBalances.find((b) => b.denom === gasDenom);
62
+ const gasAmount = gasEntry ? asBigInt(gasEntry.amount) : 0n;
63
+ if (inputs.walletBalances.length === 0 || gasAmount === 0n) {
64
+ status = "block";
65
+ reasons.push(`Wallet has no ${denomToSymbol(gasDenom, denomMap)} balance for gas.`);
66
+ actions.add("request_faucet");
67
+ actions.add("topup_wallet");
68
+ } else if (gasAmount < gasWarnFloor) {
69
+ if (status === "ok") status = "warn";
70
+ reasons.push(`Wallet balance (${humanizeCoin(gasAmount.toString(), gasDenom, denomMap)}) is below ${humanizeCoin(gasWarnFloor.toString(), gasDenom, denomMap)}; broadcast may run out of gas.`);
71
+ actions.add("topup_wallet");
72
+ }
73
+ const credits = inputs.credits;
74
+ if (credits === null) {
75
+ if (status === "ok") status = "warn";
76
+ reasons.push("No credit account funded for compute leases.");
77
+ actions.add("fund_credit");
78
+ } else if (inputs.sku !== null && inputs.sku.price.amount.length > 0 && inputs.sku.price.denom.length > 0) {
79
+ const skuPrice = inputs.sku.price;
80
+ const creditBalances = Array.isArray(credits.availableBalances) ? credits.availableBalances : Array.isArray(credits.balances) ? credits.balances : Array.isArray(credits.currentBalance) ? credits.currentBalance : [];
81
+ const creditEntry = creditBalances.find((b) => b.denom === skuPrice.denom);
82
+ const pricePerHour = asBigInt(skuPrice.amount);
83
+ if (creditEntry === void 0) {
84
+ const fundedDenoms = creditBalances.map((b) => b.denom).filter((d) => typeof d === "string" && d.length > 0);
85
+ const skuSymbol = denomToSymbol(skuPrice.denom, denomMap);
86
+ const fundedSymbols = fundedDenoms.map((d) => denomToSymbol(d, denomMap));
87
+ if (status === "ok") status = "warn";
88
+ reasons.push(fundedDenoms.length > 0 ? `Credit account has no ${skuSymbol} balance (the ${inputs.sku.name} SKU charges in ${skuSymbol}; account holds ${fundedSymbols.join(", ")}). Fund ${skuSymbol} credits before deploying.` : `Credit account is empty for the ${inputs.sku.name} SKU's ${skuSymbol} denom. Fund ${skuSymbol} credits before deploying.`);
89
+ actions.add("fund_credit");
90
+ } else if (pricePerHour > 0n) {
91
+ const creditAmount = asBigInt(creditEntry.amount);
92
+ const hrsForThisSku = Number(creditAmount) / Number(pricePerHour);
93
+ if (hrsForThisSku < HOURS_REMAINING_WARN_FLOOR) {
94
+ if (status === "ok") status = "warn";
95
+ reasons.push(`Credits cover ~${hrsForThisSku.toFixed(1)}h of runtime at the ${inputs.sku.name} SKU (${humanizeCoin(creditAmount.toString(), skuPrice.denom, denomMap)} / ${humanizeCoin(pricePerHour.toString(), skuPrice.denom, denomMap)} per hour); below the ${HOURS_REMAINING_WARN_FLOOR}h floor.`);
96
+ actions.add("fund_credit");
97
+ }
98
+ }
99
+ } else if (credits.hoursRemaining !== void 0) {
100
+ const hrs = Number(credits.hoursRemaining);
101
+ if (Number.isFinite(hrs) && hrs > 0 && hrs < HOURS_REMAINING_WARN_FLOOR) {
102
+ if (status === "ok") status = "warn";
103
+ reasons.push(`Credits cover ~${hrs.toFixed(1)}h of runtime at the current burn rate; below the ${HOURS_REMAINING_WARN_FLOOR}h floor.`);
104
+ actions.add("fund_credit");
105
+ }
106
+ }
107
+ const creditsOut = credits === null ? null : { availableBalances: Array.isArray(credits.availableBalances) ? credits.availableBalances : Array.isArray(credits.balances) ? credits.balances : Array.isArray(credits.currentBalance) ? credits.currentBalance : [] };
108
+ return {
109
+ status,
110
+ reasons,
111
+ suggestedActions: Array.from(actions),
112
+ walletBalances: inputs.walletBalances,
113
+ credits: creditsOut,
114
+ sku: inputs.sku
115
+ };
116
+ }
117
+ function asBigInt(s) {
118
+ try {
119
+ return BigInt(s);
120
+ } catch {
121
+ return 0n;
122
+ }
123
+ }
124
+ function validateGasWarnFloor(value) {
125
+ if (value < 0n) throw new TypeError(`evaluateReadiness: gasWarnFloor must be a non-negative integer, got ${value}`);
126
+ return value;
127
+ }
128
+ //#endregion
129
+ export { evaluateReadiness };
130
+
131
+ //# sourceMappingURL=evaluate-readiness.js.map
@@ -0,0 +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":";;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,MAAM,6BAA6B;AAKnC,MAAM,kCAAoE;CACxE,MAAM;CACN,MAAM;CACP;AACD,MAAM,kCAAkC;;;;;;;AAQxC,MAAM,eAAe;;;;;;;AAoDrB,SAAgB,kBAAkB,QAA4C;CAE5E,MAAM,gBAAgB,OAAO,SAAS,MAAM,aAAa;AACzD,KAAI,CAAC,iBAAiB,cAAc,OAAO,KAAA,EACzC,OAAM,IAAI,UACR,8FAA8F,OAAO,SAAS,GAC/G;CAEH,MAAM,WAAW,cAAc;CAG/B,MAAM,eACJ,OAAO,iBAAiB,KAAA,IACpB,qBAAqB,OAAO,aAAa,GACxC,gCAAgC,aACjC;CAON,MAAM,WAAW,OAAO,YAAY;CAGpC,MAAM,UAAoB,EAAE;CAC5B,MAAM,0BAAU,IAAI,KAAsB;CAC1C,IAAI,SAA8B;AAGlC,KAAI,OAAO,SAAS,QAAQ,CAAC,OAAO,kBAAkB,SAAS,OAAO,KAAK,EAAE;AAC3E,WAAS;EACT,MAAM,YACJ,OAAO,kBAAkB,SAAS,IAC9B,OAAO,kBAAkB,KAAK,KAAK,GACnC;AACN,UAAQ,KACN,kBAAkB,OAAO,KAAK,yCAAyC,UAAU,GAClF;AACD,UAAQ,IAAI,qBAAqB;;CAInC,MAAM,WAAW,OAAO,eAAe,MAAM,MAAM,EAAE,UAAU,SAAS;CACxE,MAAM,YAAY,WAAW,SAAS,SAAS,OAAO,GAAG;AACzD,KAAI,OAAO,eAAe,WAAW,KAAK,cAAc,IAAI;AAC1D,WAAS;AACT,UAAQ,KACN,iBAAiB,cAAc,UAAU,SAAS,CAAC,mBACpD;AACD,UAAQ,IAAI,iBAAiB;AAC7B,UAAQ,IAAI,eAAe;YAClB,YAAY,cAAc;AACnC,MAAI,WAAW,KAAM,UAAS;AAC9B,UAAQ,KACN,mBAAmB,aACjB,UAAU,UAAU,EACpB,UACA,SACD,CAAC,aAAa,aACb,aAAa,UAAU,EACvB,UACA,SACD,CAAC,iCACH;AACD,UAAQ,IAAI,eAAe;;CAa7B,MAAM,UAAU,OAAO;AACvB,KAAI,YAAY,MAAM;AACpB,MAAI,WAAW,KAAM,UAAS;AAC9B,UAAQ,KAAK,+CAA+C;AAC5D,UAAQ,IAAI,cAAc;YAE1B,OAAO,QAAQ,QACf,OAAO,IAAI,MAAM,OAAO,SAAS,KACjC,OAAO,IAAI,MAAM,MAAM,SAAS,GAChC;EACA,MAAM,WAAW,OAAO,IAAI;EAC5B,MAAM,iBAAyB,MAAM,QAAQ,QAAQ,kBAAkB,GACnE,QAAQ,oBACR,MAAM,QAAQ,QAAQ,SAAS,GAC7B,QAAQ,WACR,MAAM,QAAQ,QAAQ,eAAe,GACnC,QAAQ,iBACR,EAAE;EACV,MAAM,cAAc,eAAe,MAAM,MAAM,EAAE,UAAU,SAAS,MAAM;EAC1E,MAAM,eAAe,SAAS,SAAS,OAAO;AAC9C,MAAI,gBAAgB,KAAA,GAAW;GAM7B,MAAM,eAAe,eAClB,KAAK,MAAM,EAAE,MAAM,CACnB,QAAQ,MAAmB,OAAO,MAAM,YAAY,EAAE,SAAS,EAAE;GACpE,MAAM,YAAY,cAAc,SAAS,OAAO,SAAS;GACzD,MAAM,gBAAgB,aAAa,KAAK,MAAM,cAAc,GAAG,SAAS,CAAC;AACzE,OAAI,WAAW,KAAM,UAAS;AAC9B,WAAQ,KACN,aAAa,SAAS,IAClB,yBAAyB,UAAU,gBAAgB,OAAO,IAAI,KAAK,kBAAkB,UAAU,kBAAkB,cAAc,KAC7H,KACD,CAAC,UAAU,UAAU,8BACtB,mCAAmC,OAAO,IAAI,KAAK,SAAS,UAAU,eAAe,UAAU,4BACpG;AACD,WAAQ,IAAI,cAAc;aACjB,eAAe,IAAI;GAC5B,MAAM,eAAe,SAAS,YAAY,OAAO;GAIjD,MAAM,gBAAgB,OAAO,aAAa,GAAG,OAAO,aAAa;AACjE,OAAI,gBAAgB,4BAA4B;AAC9C,QAAI,WAAW,KAAM,UAAS;AAC9B,YAAQ,KACN,kBAAkB,cAAc,QAAQ,EAAE,CAAC,sBAAsB,OAAO,IAAI,KAAK,QAAQ,aACvF,aAAa,UAAU,EACvB,SAAS,OACT,SACD,CAAC,KAAK,aACL,aAAa,UAAU,EACvB,SAAS,OACT,SACD,CAAC,wBAAwB,2BAA2B,UACtD;AACD,YAAQ,IAAI,cAAc;;;YAGrB,QAAQ,mBAAmB,KAAA,GAAW;EAK/C,MAAM,MAAM,OAAO,QAAQ,eAAe;AAC1C,MAAI,OAAO,SAAS,IAAI,IAAI,MAAM,KAAK,MAAM,4BAA4B;AACvE,OAAI,WAAW,KAAM,UAAS;AAC9B,WAAQ,KACN,kBAAkB,IAAI,QAAQ,EAAE,CAAC,mDAAmD,2BAA2B,UAChH;AACD,WAAQ,IAAI,cAAc;;;CAK9B,MAAM,aACJ,YAAY,OACR,OACA,EACE,mBAAmB,MAAM,QAAQ,QAAQ,kBAAkB,GACvD,QAAQ,oBACR,MAAM,QAAQ,QAAQ,SAAS,GAC7B,QAAQ,WACR,MAAM,QAAQ,QAAQ,eAAe,GACnC,QAAQ,iBACR,EAAE,EACX;AAEP,QAAO;EACL;EACA;EACA,kBAAkB,MAAM,KAAK,QAAQ;EACrC,gBAAgB,OAAO;EACvB,SAAS;EACT,KAAK,OAAO;EACb;;AAGH,SAAS,SAAS,GAAmB;AACnC,KAAI;AACF,SAAO,OAAO,EAAE;SACV;AACN,SAAO;;;AAIX,SAAS,qBAAqB,OAAuB;AACnD,KAAI,QAAQ,GACV,OAAM,IAAI,UACR,uEAAuE,QACxE;AAEH,QAAO"}