@shipeasy/openapi 2.3.0 → 2.5.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 (46) hide show
  1. package/dist/{chunk-PRN4AEAM.js → chunk-DBTSD3W5.js} +1085 -261
  2. package/dist/chunk-DBTSD3W5.js.map +1 -0
  3. package/dist/{chunk-LYNWK55D.js → chunk-RDMD5XQT.js} +171 -1
  4. package/dist/chunk-RDMD5XQT.js.map +1 -0
  5. package/dist/{client-DO5HTb_L.d.ts → client-ObcuFHaV.d.ts} +5605 -2734
  6. package/dist/client.d.ts +1 -1
  7. package/dist/client.js +47 -1
  8. package/dist/custom.d.ts +9 -4
  9. package/dist/custom.js +57 -9
  10. package/dist/custom.js.map +1 -1
  11. package/dist/index.d.ts +2 -2
  12. package/dist/index.js +48 -2
  13. package/dist/index.js.map +1 -1
  14. package/dist/schemas.d.ts +3171 -485
  15. package/dist/schemas.js +229 -1
  16. package/openapi.json +13264 -8071
  17. package/openapi.yaml +5108 -1550
  18. package/package.json +1 -1
  19. package/spec/components/errors.yaml +3 -0
  20. package/spec/components/schemas/alerts.yaml +138 -0
  21. package/spec/components/schemas/attributes.yaml +53 -3
  22. package/spec/components/schemas/common.yaml +10 -0
  23. package/spec/components/schemas/configs.yaml +91 -3
  24. package/spec/components/schemas/connectors.yaml +5 -1
  25. package/spec/components/schemas/errors-api.yaml +51 -4
  26. package/spec/components/schemas/experiments.yaml +347 -87
  27. package/spec/components/schemas/gates.yaml +350 -277
  28. package/spec/components/schemas/i18n.yaml +169 -40
  29. package/spec/components/schemas/killswitches.yaml +36 -0
  30. package/spec/components/schemas/metrics.yaml +199 -7
  31. package/spec/components/schemas/ops.yaml +961 -29
  32. package/spec/components/schemas/projects.yaml +129 -3
  33. package/spec/openapi.yaml +58 -0
  34. package/spec/paths/alerts.yaml +184 -0
  35. package/spec/paths/attributes.yaml +23 -5
  36. package/spec/paths/configs.yaml +98 -21
  37. package/spec/paths/errors.yaml +16 -0
  38. package/spec/paths/experiments.yaml +236 -16
  39. package/spec/paths/gates.yaml +137 -0
  40. package/spec/paths/i18n.yaml +290 -6
  41. package/spec/paths/killswitches.yaml +6 -0
  42. package/spec/paths/metrics.yaml +186 -0
  43. package/spec/paths/ops.yaml +382 -0
  44. package/spec/paths/projects.yaml +60 -0
  45. package/dist/chunk-LYNWK55D.js.map +0 -1
  46. package/dist/chunk-PRN4AEAM.js.map +0 -1
package/dist/client.d.ts CHANGED
@@ -1 +1 @@
1
- export { i as Client, k as Config, m as ConfigureOptions, L as CreateClientConfig, h0 as Options, hx as RequestResult, kX as approveEvent, kY as client, kZ as configure, k_ as createAlertRule, k$ as createAttribute, l0 as createClient, l1 as createClientConfig, l2 as createConfig, l3 as createConnector, l4 as createEvent, l5 as createExperiment, l6 as createGate, l7 as createGateTemplate, l8 as createI18nDraft, l9 as createI18nProfile, la as createKey, lb as createKillswitch, lc as createMetric, ld as createOpsComment, le as createOpsItem, lf as createTriggerConnector, lg as createUniverse, lh as deleteAlertRule, li as deleteAttribute, lj as deleteConfig, lk as deleteConnector, ll as deleteEvent, lm as deleteExperiment, ln as deleteGate, lo as deleteGateTemplate, lp as deleteKillswitch, lq as deleteMetric, lr as deleteUniverse, ls as disableGate, lt as discardConfigDraft, lu as enableGate, lv as fileErrorTicket, lw as fireConnector, lx as getAttribute, ly as getConfig, lz as getConnector, lA as getCurrentProject, lB as getError, lC as getErrorSeries, lD as getEvent, lE as getExperiment, lF as getExperimentResults, lG as getExperimentTimeseries, lH as getGateTemplate, lI as getKillswitch, lJ as getMetric, lK as getOpsItem, lL as linkPrToOpsItem, lM as listAlertRules, lN as listAttributes, lO as listConfigActivity, lP as listConfigs, lQ as listConnectors, lR as listErrors, lS as listEvents, lT as listExperiments, lU as listGateTemplates, lV as listGates, lW as listI18nDrafts, lX as listI18nKeys, lY as listI18nProfiles, lZ as listKeys, l_ as listKillswitches, l$ as listMetrics, m0 as listOpsComments, m1 as listOpsItems, m2 as listSlackChannels, m3 as listUniverses, m4 as notifyOps, m5 as publishConfigDraft, m6 as publishI18nProfile, m7 as pushI18nKeys, m8 as reanalyzeExperiment, m9 as resolveError, ma as revokeKey, mb as saveConfigDraft, mc as searchResources, md as setExperimentMetrics, me as setExperimentStatus, mf as setI18nLabel, mg as setKillswitchSwitch, mh as setKillswitchValue, mi as testConnector, mj as unsetKillswitchSwitch, mk as updateAlertRule, ml as updateAttribute, mm as updateConfig, mn as updateConfigSchema, mo as updateConnector, mp as updateErrorStatus, mq as updateEvent, mr as updateExperiment, ms as updateGate, mt as updateGateTemplate, mu as updateI18nDraft, mv as updateI18nKey, mw as updateKillswitch, mx as updateMetric, my as updateOpsItem, mz as updateProject, mA as updateTriggerConnector, mB as updateUniverse, mC as upsertProject } from './client-DO5HTb_L.js';
1
+ export { q as Client, s as Config, u as ConfigureOptions, T as CreateClientConfig, iX as Options, js as RequestResult, nj as ackOpsItem, nk as approveEvent, nl as client, nm as configure, nn as createAlertRule, no as createAttribute, np as createClient, nq as createClientConfig, nr as createConfig, ns as createConnector, nt as createEvent, nu as createExperiment, nv as createExperimentReadout, nw as createGate, nx as createGateTemplate, ny as createI18nDraft, nz as createI18nProfile, nA as createKey, nB as createKillswitch, nC as createMetric, nD as createOpsComment, nE as createOpsInvestigation, nF as createOpsItem, nG as createTriggerConnector, nH as createUniverse, nI as deleteAlertRule, nJ as deleteAttribute, nK as deleteConfig, nL as deleteConnector, nM as deleteEvent, nN as deleteExperiment, nO as deleteGate, nP as deleteGateTemplate, nQ as deleteI18nDraft, nR as deleteI18nKey, nS as deleteI18nProfile, nT as deleteKillswitch, nU as deleteMetric, nV as deleteOpsItem, nW as deleteUniverse, nX as disableGate, nY as discardConfigDraft, nZ as enableGate, n_ as fileErrorTicket, n$ as fireConnector, o0 as getAttribute, o1 as getConfig, o2 as getConnector, o3 as getCurrentProject, o4 as getError, o5 as getErrorSeries, o6 as getEvent, o7 as getExperiment, o8 as getExperimentReadout, o9 as getExperimentResults, oa as getExperimentTimeseries, ob as getGate, oc as getGateTemplate, od as getKillswitch, oe as getMetric, of as getMetricSeries, og as getOpsItem, oh as getProject, oi as linkPrToOpsItem, oj as listAlertRules, ok as listAlerts, ol as listAttributes, om as listConfigActivity, on as listConfigVersions, oo as listConfigs, op as listConnectors, oq as listErrors, or as listEvents, os as listExperiments, ot as listGateActivity, ou as listGateTemplates, ov as listGates, ow as listI18nDraftKeys, ox as listI18nDrafts, oy as listI18nKeys, oz as listI18nProfiles, oA as listKeys, oB as listKillswitches, oC as listMetricExperiments, oD as listMetrics, oE as listOpsAgents, oF as listOpsComments, oG as listOpsInvestigations, oH as listOpsItems, oI as listSlackChannels, oJ as listUniverses, oK as notifyOps, oL as publishConfigDraft, oM as publishI18nProfile, oN as pushI18nKeys, oO as reanalyzeExperiment, oP as resolveError, oQ as revokeKey, oR as saveConfigDraft, oS as searchResources, oT as setExperimentMetrics, oU as setExperimentStatus, oV as setI18nLabel, oW as setKillswitchSwitch, oX as setKillswitchValue, oY as testConnector, oZ as unarchiveMetric, o_ as unsetKillswitchSwitch, o$ as updateAlert, p0 as updateAlertRule, p1 as updateAttribute, p2 as updateConfig, p3 as updateConfigSchema, p4 as updateConnector, p5 as updateErrorStatus, p6 as updateEvent, p7 as updateExperiment, p8 as updateGate, p9 as updateGateTemplate, pa as updateI18nDraft, pb as updateI18nKey, pc as updateKillswitch, pd as updateMetric, pe as updateOpsInvestigation, pf as updateOpsItem, pg as updateProject, ph as updateTriggerConnector, pi as updateUniverse, pj as upsertI18nDraftKey, pk as upsertI18nKeys, pl as upsertProject } from './client-ObcuFHaV.js';
package/dist/client.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import {
2
+ ackOpsItem,
2
3
  approveEvent,
3
4
  client,
4
5
  configure,
@@ -10,6 +11,7 @@ import {
10
11
  createConnector,
11
12
  createEvent,
12
13
  createExperiment,
14
+ createExperimentReadout,
13
15
  createGate,
14
16
  createGateTemplate,
15
17
  createI18nDraft,
@@ -18,6 +20,7 @@ import {
18
20
  createKillswitch,
19
21
  createMetric,
20
22
  createOpsComment,
23
+ createOpsInvestigation,
21
24
  createOpsItem,
22
25
  createTriggerConnector,
23
26
  createUniverse,
@@ -29,8 +32,12 @@ import {
29
32
  deleteExperiment,
30
33
  deleteGate,
31
34
  deleteGateTemplate,
35
+ deleteI18nDraft,
36
+ deleteI18nKey,
37
+ deleteI18nProfile,
32
38
  deleteKillswitch,
33
39
  deleteMetric,
40
+ deleteOpsItem,
34
41
  deleteUniverse,
35
42
  disableGate,
36
43
  discardConfigDraft,
@@ -45,30 +52,41 @@ import {
45
52
  getErrorSeries,
46
53
  getEvent,
47
54
  getExperiment,
55
+ getExperimentReadout,
48
56
  getExperimentResults,
49
57
  getExperimentTimeseries,
58
+ getGate,
50
59
  getGateTemplate,
51
60
  getKillswitch,
52
61
  getMetric,
62
+ getMetricSeries,
53
63
  getOpsItem,
64
+ getProject,
54
65
  linkPrToOpsItem,
55
66
  listAlertRules,
67
+ listAlerts,
56
68
  listAttributes,
57
69
  listConfigActivity,
70
+ listConfigVersions,
58
71
  listConfigs,
59
72
  listConnectors,
60
73
  listErrors,
61
74
  listEvents,
62
75
  listExperiments,
76
+ listGateActivity,
63
77
  listGateTemplates,
64
78
  listGates,
79
+ listI18nDraftKeys,
65
80
  listI18nDrafts,
66
81
  listI18nKeys,
67
82
  listI18nProfiles,
68
83
  listKeys,
69
84
  listKillswitches,
85
+ listMetricExperiments,
70
86
  listMetrics,
87
+ listOpsAgents,
71
88
  listOpsComments,
89
+ listOpsInvestigations,
72
90
  listOpsItems,
73
91
  listSlackChannels,
74
92
  listUniverses,
@@ -87,7 +105,9 @@ import {
87
105
  setKillswitchSwitch,
88
106
  setKillswitchValue,
89
107
  testConnector,
108
+ unarchiveMetric,
90
109
  unsetKillswitchSwitch,
110
+ updateAlert,
91
111
  updateAlertRule,
92
112
  updateAttribute,
93
113
  updateConfig,
@@ -102,13 +122,17 @@ import {
102
122
  updateI18nKey,
103
123
  updateKillswitch,
104
124
  updateMetric,
125
+ updateOpsInvestigation,
105
126
  updateOpsItem,
106
127
  updateProject,
107
128
  updateTriggerConnector,
108
129
  updateUniverse,
130
+ upsertI18nDraftKey,
131
+ upsertI18nKeys,
109
132
  upsertProject
110
- } from "./chunk-LYNWK55D.js";
133
+ } from "./chunk-RDMD5XQT.js";
111
134
  export {
135
+ ackOpsItem,
112
136
  approveEvent,
113
137
  client,
114
138
  configure,
@@ -120,6 +144,7 @@ export {
120
144
  createConnector,
121
145
  createEvent,
122
146
  createExperiment,
147
+ createExperimentReadout,
123
148
  createGate,
124
149
  createGateTemplate,
125
150
  createI18nDraft,
@@ -128,6 +153,7 @@ export {
128
153
  createKillswitch,
129
154
  createMetric,
130
155
  createOpsComment,
156
+ createOpsInvestigation,
131
157
  createOpsItem,
132
158
  createTriggerConnector,
133
159
  createUniverse,
@@ -139,8 +165,12 @@ export {
139
165
  deleteExperiment,
140
166
  deleteGate,
141
167
  deleteGateTemplate,
168
+ deleteI18nDraft,
169
+ deleteI18nKey,
170
+ deleteI18nProfile,
142
171
  deleteKillswitch,
143
172
  deleteMetric,
173
+ deleteOpsItem,
144
174
  deleteUniverse,
145
175
  disableGate,
146
176
  discardConfigDraft,
@@ -155,30 +185,41 @@ export {
155
185
  getErrorSeries,
156
186
  getEvent,
157
187
  getExperiment,
188
+ getExperimentReadout,
158
189
  getExperimentResults,
159
190
  getExperimentTimeseries,
191
+ getGate,
160
192
  getGateTemplate,
161
193
  getKillswitch,
162
194
  getMetric,
195
+ getMetricSeries,
163
196
  getOpsItem,
197
+ getProject,
164
198
  linkPrToOpsItem,
165
199
  listAlertRules,
200
+ listAlerts,
166
201
  listAttributes,
167
202
  listConfigActivity,
203
+ listConfigVersions,
168
204
  listConfigs,
169
205
  listConnectors,
170
206
  listErrors,
171
207
  listEvents,
172
208
  listExperiments,
209
+ listGateActivity,
173
210
  listGateTemplates,
174
211
  listGates,
212
+ listI18nDraftKeys,
175
213
  listI18nDrafts,
176
214
  listI18nKeys,
177
215
  listI18nProfiles,
178
216
  listKeys,
179
217
  listKillswitches,
218
+ listMetricExperiments,
180
219
  listMetrics,
220
+ listOpsAgents,
181
221
  listOpsComments,
222
+ listOpsInvestigations,
182
223
  listOpsItems,
183
224
  listSlackChannels,
184
225
  listUniverses,
@@ -197,7 +238,9 @@ export {
197
238
  setKillswitchSwitch,
198
239
  setKillswitchValue,
199
240
  testConnector,
241
+ unarchiveMetric,
200
242
  unsetKillswitchSwitch,
243
+ updateAlert,
201
244
  updateAlertRule,
202
245
  updateAttribute,
203
246
  updateConfig,
@@ -212,10 +255,13 @@ export {
212
255
  updateI18nKey,
213
256
  updateKillswitch,
214
257
  updateMetric,
258
+ updateOpsInvestigation,
215
259
  updateOpsItem,
216
260
  updateProject,
217
261
  updateTriggerConnector,
218
262
  updateUniverse,
263
+ upsertI18nDraftKey,
264
+ upsertI18nKeys,
219
265
  upsertProject
220
266
  };
221
267
  //# sourceMappingURL=client.js.map
package/dist/custom.d.ts CHANGED
@@ -64,11 +64,16 @@ declare const opId: (op: CustomOp) => string;
64
64
  * author a `metrics create --query "..."` string. It is intentionally
65
65
  * self-contained: the formal grammar (precise, for machine parsing), a full
66
66
  * aggregation reference (semantics + value-label rules + experiment behaviour),
67
- * the selector/filter/group-by rules, hard constraints, and glossed examples.
68
- * Keep it in sync with the parser (`cli/src/query-dsl/parse.ts`) and the IR
69
- * (`cli/src/query-dsl/ir.ts`) — they are the source of truth for semantics.
67
+ * the selector/filter/group-by/ratio rules, hard constraints, and glossed
68
+ * examples.
69
+ *
70
+ * Source of truth for semantics is the RUNTIME parser + IR that the server
71
+ * actually runs: the main repo's `packages/query-dsl` (`parse.ts` / `ir.ts`).
72
+ * The vendored `cli/src/query-dsl` copy is a lagging subset (no `ratio` yet) and
73
+ * is NOT on the create path — the CLI forwards the raw `--query` string to the
74
+ * server, which parses it. Keep this text in sync with `packages/query-dsl`.
70
75
  */
71
- declare const METRIC_GRAMMAR = "SHIPEASY METRIC QUERY DSL\n=========================\n\nA metric is ONE aggregation over ONE event stream. This DSL is the string you\npass to `metrics create --query \"...\"` (equivalently the typed `query_ir`).\nThere is no arithmetic, no formulas across events, no subqueries \u2014 one event\nselector, one aggregation, an optional group-by.\n\n\nGRAMMAR (BNF)\n-------------\n Query := AggFunc \"(\" Selector (\",\" Identifier)? \")\" GroupBy?\n AggFunc := \"count_users\" | \"count\" | \"sum\" | \"avg\" | \"min\" | \"max\"\n | \"unique\" | \"p50\" | \"p75\" | \"p90\" | \"p95\" | \"p99\" | \"p999\"\n | \"retention_\" <N> \"d\" (N = 1..90)\n Selector := Identifier (\"{\" Filter (\",\" Filter)* \",\"? \"}\")?\n Filter := Identifier MatchOp StringLiteral\n MatchOp := \"=\" | \"!=\" | \"=~\" | \"!~\"\n GroupBy := (\"by\" | \"without\") \"(\" Identifier (\",\" Identifier)* \",\"? \")\"\n StringLiteral := double-quoted; \\\" is a literal quote, \\\\ is a backslash\n Identifier := [A-Za-z_][A-Za-z0-9_]* (case-sensitive)\n\n The trailing Identifier after the selector \u2014 \"(\" Selector \",\" Identifier \")\" \u2014\n is the VALUE LABEL: the numeric event label the aggregation reduces. Whether it\n is required, optional, or forbidden depends on the aggregation (see below).\n\n\nAGGREGATIONS\n------------\n Function Value label Meaning In experiments\n -------------- ------------ ------------------------------ ---------------\n count_users forbidden distinct users who fired it exact\n count forbidden number of events (rows) exact\n sum(e, v) required \u03A3 of numeric label v exact\n avg(e, v) required mean of label v exact\n retention_Nd(e) forbidden % of users who returned exact\n within N days (N = 1..90)\n min(e, v) required minimum of label v approx \u2192 avg *\n max(e, v) required maximum of label v approx \u2192 avg *\n unique(e, v) required distinct values of label v approx \u2192 avg *\n p50 \u2026 p999(e,v) required quantile of v (50/75/90/ approx \u2192 avg *\n 95/99/99.9th percentile)\n\n * Display-only aggregations. The experiment t-test needs a per-user mean and\n variance, so min/max/unique/quantile are computed exactly on dashboards but\n collapse to a per-user `avg` when used as an experiment metric. If you need\n an exact experiment metric, pick count_users / count / sum / avg / retention.\n\n \"count\" is the DSL spelling of the internal count_events (one per matching\n row); \"count_users\" is unique-by-user. Getting these two confused is the most\n common mistake \u2014 count double-counts a user who fired the event twice.\n\n\nSELECTOR & FILTERS\n------------------\n The selector is the event name plus an optional `{ ... }` filter set that\n narrows which events count. Each filter compares an event LABEL to a quoted\n string:\n\n = equal != not equal\n =~ regex match !~ regex NOT match\n\n - Values are ALWAYS double-quoted strings, even for numeric labels \u2014 they are\n coerced on the server (`amount=~\"^1[0-9]$\"`, `status=\"200\"`).\n - Multiple filters are AND-ed: `{country=\"US\", tier!=\"free\"}`.\n - Every filter label AND the value label must be a label DECLARED on the source\n event. Undeclared labels are a validation error, not a silent no-match.\n\n\nGROUP-BY\n--------\n Splits the result into one series per label combination.\n by (a, b) one series per distinct (a, b) \u2014 keep only these labels\n without (a, b) group by all declared labels EXCEPT a, b\n\n NOTE: group-by is a DASHBOARD-only feature. In an experiment the group-by is\n ignored \u2014 a per-user reducer is applied across the whole arm instead.\n\n\nNOT SUPPORTED (will fail to parse)\n----------------------------------\n - arithmetic / ratios / formulas: `count(a) / count(b)`\n - more than one event per query\n - arbitrary quantiles (only the fixed p50/p75/p90/p95/p99/p999)\n - unquoted or numeric filter values: `{status=200}` \u2192 must be `\"200\"`\n - trailing tokens after the query\n\n\nEXAMPLES (query \u2014 what it measures)\n--------------------------------------\n count_users(checkout_completed)\n distinct users who completed checkout.\n\n count(add_to_cart{country=\"US\"})\n number of US add-to-cart events (a user can count many times).\n\n sum(purchase{country=\"US\"}, amount)\n total purchase amount from US purchase events.\n\n avg(req_dur{tier!=\"free\"}, ms) without (region)\n mean request duration for non-free tiers, one series per region-excluded\n label combination.\n\n p99(req_dur{route=~\"/api/.*\"}, ms) by (route, status)\n 99th-percentile latency of /api/* requests, split by route and status.\n\n unique(login{method=\"sso\"}, device_id)\n distinct SSO devices seen (display-only; an experiment would use per-user avg).\n\n retention_7d(session_start)\n share of users who started a new session within 7 days.\n";
76
+ declare const METRIC_GRAMMAR = "SHIPEASY METRIC QUERY DSL\n=========================\n\nA metric is an aggregation over an event stream. Almost always that is ONE\naggregation over ONE event; the single exception is the binary `ratio(...)`\nform, which divides one count-style arm by another (each arm its own event and\nfilters) in one pass. This DSL is the string you pass to\n`metrics create --query \"...\"` (equivalently the typed `query_ir`). There is\nno general arithmetic, no free-form formulas, and no subqueries \u2014 `ratio` is\nthe only division / two-event form.\n\n\nGRAMMAR (BNF)\n-------------\n Query := Agg | Ratio\n Agg := AggFunc \"(\" Selector (\",\" Identifier)? \")\" GroupBy?\n Ratio := \"ratio\" \"(\" RatioArm \",\" RatioArm \")\"\n RatioArm := (\"count_users\" | \"count\") \"(\" Selector \")\"\n AggFunc := \"count_users\" | \"count\" | \"sum\" | \"avg\" | \"min\" | \"max\"\n | \"unique\" | \"p50\" | \"p75\" | \"p90\" | \"p95\" | \"p99\" | \"p999\"\n | \"retention_\" <N> \"d\" (N = 1..90)\n Selector := Identifier (\"{\" Filter (\",\" Filter)* \",\"? \"}\")?\n Filter := Identifier MatchOp StringLiteral\n MatchOp := \"=\" | \"!=\" | \"=~\" | \"!~\"\n GroupBy := (\"by\" | \"without\") \"(\" Identifier (\",\" Identifier)* \",\"? \")\"\n StringLiteral := double-quoted; \\\" is a literal quote, \\\\ is a backslash\n Identifier := [A-Za-z_][A-Za-z0-9_]* (case-sensitive)\n\n The trailing Identifier after the selector \u2014 \"(\" Selector \",\" Identifier \")\" \u2014\n is the VALUE LABEL: the numeric event label the aggregation reduces. Whether it\n is required, optional, or forbidden depends on the aggregation (see below).\n\n\nAGGREGATIONS\n------------\n Function Value label Meaning In experiments\n -------------- ------------ ------------------------------ ---------------\n count_users forbidden distinct users who fired it exact\n count forbidden number of events (rows) exact\n sum(e, v) required \u03A3 of numeric label v exact\n avg(e, v) required mean of label v exact\n retention_Nd(e) forbidden % of users who returned exact\n within N days (N = 1..90)\n min(e, v) required minimum of label v approx \u2192 avg *\n max(e, v) required maximum of label v approx \u2192 avg *\n unique(e, v) required distinct values of label v approx \u2192 avg *\n p50 \u2026 p999(e,v) required quantile of v (50/75/90/ approx \u2192 avg *\n 95/99/99.9th percentile)\n ratio(a, b) forbidden a \u00F7 b (see RATIO below) proportion **\n\n * Display-only aggregations. The experiment t-test needs a per-user mean and\n variance, so min/max/unique/quantile are computed exactly on dashboards but\n collapse to a per-user `avg` when used as an experiment metric. If you need\n an exact experiment metric, pick count_users / count / sum / avg / retention.\n\n ** In an experiment a ratio collapses to a per-user 0/1 outcome (a proportion),\n not a ratio-of-sums \u2014 see RATIO.\n\n \"count\" is the DSL spelling of the internal count_events (one per matching\n row); \"count_users\" is unique-by-user. Getting these two confused is the most\n common mistake \u2014 count double-counts a user who fired the event twice.\n\n\nSELECTOR & FILTERS\n------------------\n The selector is the event name plus an optional `{ ... }` filter set that\n narrows which events count. Each filter compares an event LABEL to a quoted\n string:\n\n = equal != not equal\n =~ regex match !~ regex NOT match\n\n - Values are ALWAYS double-quoted strings, even for numeric labels \u2014 they are\n coerced on the server (`amount=~\"^1[0-9]$\"`, `status=\"200\"`).\n - Multiple filters are AND-ed: `{country=\"US\", tier!=\"free\"}`.\n - Every filter label AND the value label must be a label DECLARED on the source\n event. Undeclared labels are a validation error, not a silent no-match.\n\n\nGROUP-BY\n--------\n Splits the result into one series per label combination.\n by (a, b) one series per distinct (a, b) \u2014 keep only these labels\n without (a, b) group by all declared labels EXCEPT a, b\n\n NOTE: group-by is a DASHBOARD-only feature. In an experiment the group-by is\n ignored \u2014 a per-user reducer is applied across the whole arm instead. Group-by\n is NOT allowed on a `ratio` query.\n\n\nRATIO (success rate, conversion, failure %, \u2026)\n-----------------------------------------------\n `ratio(numerator, denominator)` divides one count-style arm by another. Each\n arm is an independent selector with its OWN event and filters, so the two arms\n can be different events:\n\n ratio(count(checkout_completed), count(checkout_started))\n ratio(count_users(paid), count_users(signed_up))\n ratio(count(payment{ok=\"1\"}), count(payment)) one event, filtered arm\n\n Rules:\n - Arms are limited to `count` and `count_users` \u2014 no sum/avg/quantile arm.\n - `count` arms divide event counts; `count_users` arms divide distinct-user\n counts. Pick one shape and use it on both arms unless you mean to mix them.\n - A zero denominator yields 0, not an error.\n - No `by (...)` / `without (...)`, and no retention arm, on a ratio.\n - It is a COHORT RATE over the window \u2014 numerator events \u00F7 denominator events \u2014\n NOT a per-id join. It never matches an individual attempt to its outcome by a\n correlation id; it counts each side over the window and divides. If you need a\n matched pair, resolve the outcome in your own code and emit ONE event carrying\n the result (e.g. an `ok` label), then use `ratio(count(e{ok=\"1\"}), count(e))`\n or `avg(e, ok)`.\n - In an experiment a per-user ratio is a 0/1 outcome \u2014 did the numerator happen,\n among denominator-eligible users \u2014 i.e. a proportion, not a ratio-of-sums.\n - When creating with `--event-name`, pass the NUMERATOR's event name.\n\n\nNOT SUPPORTED (will fail to parse)\n----------------------------------\n - general arithmetic / free-form formulas: `count(a) + count(b)`, `2 * sum(x)`,\n `avg(a) / avg(b)` \u2014 the ONLY division form is the fixed binary\n `ratio(arm, arm)` with count-style arms (see RATIO)\n - a ratio arm that isn't `count` / `count_users`: `ratio(sum(a, v), count(b))`\n - more than two events in one query\n - `by (...)` / `without (...)` on a ratio\n - arbitrary quantiles (only the fixed p50/p75/p90/p95/p99/p999)\n - unquoted or numeric filter values: `{status=200}` \u2192 must be `\"200\"`\n - trailing tokens after the query\n\n\nEXAMPLES (query \u2014 what it measures)\n--------------------------------------\n count_users(checkout_completed)\n distinct users who completed checkout.\n\n count(add_to_cart{country=\"US\"})\n number of US add-to-cart events (a user can count many times).\n\n sum(purchase{country=\"US\"}, amount)\n total purchase amount from US purchase events.\n\n avg(req_dur{tier!=\"free\"}, ms) without (region)\n mean request duration for non-free tiers, one series per region-excluded\n label combination.\n\n p99(req_dur{route=~\"/api/.*\"}, ms) by (route, status)\n 99th-percentile latency of /api/* requests, split by route and status.\n\n unique(login{method=\"sso\"}, device_id)\n distinct SSO devices seen (display-only; an experiment would use per-user avg).\n\n retention_7d(session_start)\n share of users who started a new session within 7 days.\n\n ratio(count(checkout_completed), count(checkout_started))\n checkout conversion rate \u2014 completions \u00F7 starts over the window.\n\n ratio(count(payment{ok=\"0\"}), count(payment))\n payment failure rate from a single event's outcome label.\n";
72
77
 
73
78
  /**
74
79
  * Every custom (non-spec) operation, in surface order. The CLI and MCP each
package/dist/custom.js CHANGED
@@ -2,15 +2,21 @@
2
2
  var METRIC_GRAMMAR = `SHIPEASY METRIC QUERY DSL
3
3
  =========================
4
4
 
5
- A metric is ONE aggregation over ONE event stream. This DSL is the string you
6
- pass to \`metrics create --query "..."\` (equivalently the typed \`query_ir\`).
7
- There is no arithmetic, no formulas across events, no subqueries \u2014 one event
8
- selector, one aggregation, an optional group-by.
5
+ A metric is an aggregation over an event stream. Almost always that is ONE
6
+ aggregation over ONE event; the single exception is the binary \`ratio(...)\`
7
+ form, which divides one count-style arm by another (each arm its own event and
8
+ filters) in one pass. This DSL is the string you pass to
9
+ \`metrics create --query "..."\` (equivalently the typed \`query_ir\`). There is
10
+ no general arithmetic, no free-form formulas, and no subqueries \u2014 \`ratio\` is
11
+ the only division / two-event form.
9
12
 
10
13
 
11
14
  GRAMMAR (BNF)
12
15
  -------------
13
- Query := AggFunc "(" Selector ("," Identifier)? ")" GroupBy?
16
+ Query := Agg | Ratio
17
+ Agg := AggFunc "(" Selector ("," Identifier)? ")" GroupBy?
18
+ Ratio := "ratio" "(" RatioArm "," RatioArm ")"
19
+ RatioArm := ("count_users" | "count") "(" Selector ")"
14
20
  AggFunc := "count_users" | "count" | "sum" | "avg" | "min" | "max"
15
21
  | "unique" | "p50" | "p75" | "p90" | "p95" | "p99" | "p999"
16
22
  | "retention_" <N> "d" (N = 1..90)
@@ -41,12 +47,16 @@ AGGREGATIONS
41
47
  unique(e, v) required distinct values of label v approx \u2192 avg *
42
48
  p50 \u2026 p999(e,v) required quantile of v (50/75/90/ approx \u2192 avg *
43
49
  95/99/99.9th percentile)
50
+ ratio(a, b) forbidden a \xF7 b (see RATIO below) proportion **
44
51
 
45
52
  * Display-only aggregations. The experiment t-test needs a per-user mean and
46
53
  variance, so min/max/unique/quantile are computed exactly on dashboards but
47
54
  collapse to a per-user \`avg\` when used as an experiment metric. If you need
48
55
  an exact experiment metric, pick count_users / count / sum / avg / retention.
49
56
 
57
+ ** In an experiment a ratio collapses to a per-user 0/1 outcome (a proportion),
58
+ not a ratio-of-sums \u2014 see RATIO.
59
+
50
60
  "count" is the DSL spelling of the internal count_events (one per matching
51
61
  row); "count_users" is unique-by-user. Getting these two confused is the most
52
62
  common mistake \u2014 count double-counts a user who fired the event twice.
@@ -75,13 +85,45 @@ GROUP-BY
75
85
  without (a, b) group by all declared labels EXCEPT a, b
76
86
 
77
87
  NOTE: group-by is a DASHBOARD-only feature. In an experiment the group-by is
78
- ignored \u2014 a per-user reducer is applied across the whole arm instead.
88
+ ignored \u2014 a per-user reducer is applied across the whole arm instead. Group-by
89
+ is NOT allowed on a \`ratio\` query.
90
+
91
+
92
+ RATIO (success rate, conversion, failure %, \u2026)
93
+ -----------------------------------------------
94
+ \`ratio(numerator, denominator)\` divides one count-style arm by another. Each
95
+ arm is an independent selector with its OWN event and filters, so the two arms
96
+ can be different events:
97
+
98
+ ratio(count(checkout_completed), count(checkout_started))
99
+ ratio(count_users(paid), count_users(signed_up))
100
+ ratio(count(payment{ok="1"}), count(payment)) one event, filtered arm
101
+
102
+ Rules:
103
+ - Arms are limited to \`count\` and \`count_users\` \u2014 no sum/avg/quantile arm.
104
+ - \`count\` arms divide event counts; \`count_users\` arms divide distinct-user
105
+ counts. Pick one shape and use it on both arms unless you mean to mix them.
106
+ - A zero denominator yields 0, not an error.
107
+ - No \`by (...)\` / \`without (...)\`, and no retention arm, on a ratio.
108
+ - It is a COHORT RATE over the window \u2014 numerator events \xF7 denominator events \u2014
109
+ NOT a per-id join. It never matches an individual attempt to its outcome by a
110
+ correlation id; it counts each side over the window and divides. If you need a
111
+ matched pair, resolve the outcome in your own code and emit ONE event carrying
112
+ the result (e.g. an \`ok\` label), then use \`ratio(count(e{ok="1"}), count(e))\`
113
+ or \`avg(e, ok)\`.
114
+ - In an experiment a per-user ratio is a 0/1 outcome \u2014 did the numerator happen,
115
+ among denominator-eligible users \u2014 i.e. a proportion, not a ratio-of-sums.
116
+ - When creating with \`--event-name\`, pass the NUMERATOR's event name.
79
117
 
80
118
 
81
119
  NOT SUPPORTED (will fail to parse)
82
120
  ----------------------------------
83
- - arithmetic / ratios / formulas: \`count(a) / count(b)\`
84
- - more than one event per query
121
+ - general arithmetic / free-form formulas: \`count(a) + count(b)\`, \`2 * sum(x)\`,
122
+ \`avg(a) / avg(b)\` \u2014 the ONLY division form is the fixed binary
123
+ \`ratio(arm, arm)\` with count-style arms (see RATIO)
124
+ - a ratio arm that isn't \`count\` / \`count_users\`: \`ratio(sum(a, v), count(b))\`
125
+ - more than two events in one query
126
+ - \`by (...)\` / \`without (...)\` on a ratio
85
127
  - arbitrary quantiles (only the fixed p50/p75/p90/p95/p99/p999)
86
128
  - unquoted or numeric filter values: \`{status=200}\` \u2192 must be \`"200"\`
87
129
  - trailing tokens after the query
@@ -110,12 +152,18 @@ EXAMPLES (query \u2014 what it measures)
110
152
 
111
153
  retention_7d(session_start)
112
154
  share of users who started a new session within 7 days.
155
+
156
+ ratio(count(checkout_completed), count(checkout_started))
157
+ checkout conversion rate \u2014 completions \xF7 starts over the window.
158
+
159
+ ratio(count(payment{ok="0"}), count(payment))
160
+ payment failure rate from a single event's outcome label.
113
161
  `;
114
162
  var grammarOp = {
115
163
  group: ["metrics"],
116
164
  name: "grammar",
117
165
  summary: "Print the metric query DSL grammar",
118
- description: "Print the full metric query DSL reference \u2014 grammar, aggregation semantics, filter/group-by rules, and glossed examples \u2014 used to author `metrics create --query`.",
166
+ description: "Print the full metric query DSL reference \u2014 grammar, aggregation semantics, filter/group-by/ratio rules, and glossed examples \u2014 used to author `metrics create --query`.",
119
167
  params: [],
120
168
  examples: [{ run: "shipeasy metrics grammar" }],
121
169
  run: () => ({ grammar: METRIC_GRAMMAR })
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/custom/grammar.ts","../src/custom/types.ts","../src/custom/docs.ts","../src/custom/index.ts"],"sourcesContent":["import type { CustomOp } from \"./types.js\";\n\n/**\n * The Shipeasy metric query DSL grammar — printed by `shipeasy metrics grammar`\n * / the `metrics_grammar` MCP tool.\n *\n * This is the one-stop reference for both humans and AI agents that need to\n * author a `metrics create --query \"...\"` string. It is intentionally\n * self-contained: the formal grammar (precise, for machine parsing), a full\n * aggregation reference (semantics + value-label rules + experiment behaviour),\n * the selector/filter/group-by rules, hard constraints, and glossed examples.\n * Keep it in sync with the parser (`cli/src/query-dsl/parse.ts`) and the IR\n * (`cli/src/query-dsl/ir.ts`) — they are the source of truth for semantics.\n */\nexport const METRIC_GRAMMAR = `SHIPEASY METRIC QUERY DSL\n=========================\n\nA metric is ONE aggregation over ONE event stream. This DSL is the string you\npass to \\`metrics create --query \"...\"\\` (equivalently the typed \\`query_ir\\`).\nThere is no arithmetic, no formulas across events, no subqueries — one event\nselector, one aggregation, an optional group-by.\n\n\nGRAMMAR (BNF)\n-------------\n Query := AggFunc \"(\" Selector (\",\" Identifier)? \")\" GroupBy?\n AggFunc := \"count_users\" | \"count\" | \"sum\" | \"avg\" | \"min\" | \"max\"\n | \"unique\" | \"p50\" | \"p75\" | \"p90\" | \"p95\" | \"p99\" | \"p999\"\n | \"retention_\" <N> \"d\" (N = 1..90)\n Selector := Identifier (\"{\" Filter (\",\" Filter)* \",\"? \"}\")?\n Filter := Identifier MatchOp StringLiteral\n MatchOp := \"=\" | \"!=\" | \"=~\" | \"!~\"\n GroupBy := (\"by\" | \"without\") \"(\" Identifier (\",\" Identifier)* \",\"? \")\"\n StringLiteral := double-quoted; \\\\\" is a literal quote, \\\\\\\\ is a backslash\n Identifier := [A-Za-z_][A-Za-z0-9_]* (case-sensitive)\n\n The trailing Identifier after the selector — \"(\" Selector \",\" Identifier \")\" —\n is the VALUE LABEL: the numeric event label the aggregation reduces. Whether it\n is required, optional, or forbidden depends on the aggregation (see below).\n\n\nAGGREGATIONS\n------------\n Function Value label Meaning In experiments\n -------------- ------------ ------------------------------ ---------------\n count_users forbidden distinct users who fired it exact\n count forbidden number of events (rows) exact\n sum(e, v) required Σ of numeric label v exact\n avg(e, v) required mean of label v exact\n retention_Nd(e) forbidden % of users who returned exact\n within N days (N = 1..90)\n min(e, v) required minimum of label v approx → avg *\n max(e, v) required maximum of label v approx → avg *\n unique(e, v) required distinct values of label v approx → avg *\n p50 … p999(e,v) required quantile of v (50/75/90/ approx → avg *\n 95/99/99.9th percentile)\n\n * Display-only aggregations. The experiment t-test needs a per-user mean and\n variance, so min/max/unique/quantile are computed exactly on dashboards but\n collapse to a per-user \\`avg\\` when used as an experiment metric. If you need\n an exact experiment metric, pick count_users / count / sum / avg / retention.\n\n \"count\" is the DSL spelling of the internal count_events (one per matching\n row); \"count_users\" is unique-by-user. Getting these two confused is the most\n common mistake — count double-counts a user who fired the event twice.\n\n\nSELECTOR & FILTERS\n------------------\n The selector is the event name plus an optional \\`{ ... }\\` filter set that\n narrows which events count. Each filter compares an event LABEL to a quoted\n string:\n\n = equal != not equal\n =~ regex match !~ regex NOT match\n\n - Values are ALWAYS double-quoted strings, even for numeric labels — they are\n coerced on the server (\\`amount=~\"^1[0-9]$\"\\`, \\`status=\"200\"\\`).\n - Multiple filters are AND-ed: \\`{country=\"US\", tier!=\"free\"}\\`.\n - Every filter label AND the value label must be a label DECLARED on the source\n event. Undeclared labels are a validation error, not a silent no-match.\n\n\nGROUP-BY\n--------\n Splits the result into one series per label combination.\n by (a, b) one series per distinct (a, b) — keep only these labels\n without (a, b) group by all declared labels EXCEPT a, b\n\n NOTE: group-by is a DASHBOARD-only feature. In an experiment the group-by is\n ignored — a per-user reducer is applied across the whole arm instead.\n\n\nNOT SUPPORTED (will fail to parse)\n----------------------------------\n - arithmetic / ratios / formulas: \\`count(a) / count(b)\\`\n - more than one event per query\n - arbitrary quantiles (only the fixed p50/p75/p90/p95/p99/p999)\n - unquoted or numeric filter values: \\`{status=200}\\` → must be \\`\"200\"\\`\n - trailing tokens after the query\n\n\nEXAMPLES (query — what it measures)\n--------------------------------------\n count_users(checkout_completed)\n distinct users who completed checkout.\n\n count(add_to_cart{country=\"US\"})\n number of US add-to-cart events (a user can count many times).\n\n sum(purchase{country=\"US\"}, amount)\n total purchase amount from US purchase events.\n\n avg(req_dur{tier!=\"free\"}, ms) without (region)\n mean request duration for non-free tiers, one series per region-excluded\n label combination.\n\n p99(req_dur{route=~\"/api/.*\"}, ms) by (route, status)\n 99th-percentile latency of /api/* requests, split by route and status.\n\n unique(login{method=\"sso\"}, device_id)\n distinct SSO devices seen (display-only; an experiment would use per-user avg).\n\n retention_7d(session_start)\n share of users who started a new session within 7 days.\n`;\n\nexport const grammarOp: CustomOp = {\n group: [\"metrics\"],\n name: \"grammar\",\n summary: \"Print the metric query DSL grammar\",\n description:\n \"Print the full metric query DSL reference — grammar, aggregation semantics, \" +\n \"filter/group-by rules, and glossed examples — used to author `metrics create --query`.\",\n params: [],\n examples: [{ run: \"shipeasy metrics grammar\" }],\n run: () => ({ grammar: METRIC_GRAMMAR }),\n};\n","/**\n * The custom-operations registry — hand-written operations that are NOT admin\n * API endpoints (so they can't be generated from the spec): the metric-DSL\n * grammar (pure) and the SDK-docs fetcher (outbound `fetch` to GitHub Pages).\n *\n * It mirrors the old operations registry in spirit, but is deliberately tiny and\n * framework-agnostic: each `CustomOp` carries its command path, params, and a\n * self-contained `run`. Both consumers project it with a thin adapter — the CLI\n * (`commands/custom.ts`) and, later, the MCP server — so the surface stays in\n * sync across them. `run` never touches the filesystem, so it's worker-safe.\n */\nexport type CustomParamType = \"string\" | \"number\" | \"boolean\";\n\nexport interface CustomParam {\n name: string;\n type: CustomParamType;\n description?: string;\n required?: boolean;\n /** Render as a positional CLI argument (vs a `--flag`). */\n positional?: boolean;\n enum?: string[];\n}\n\nexport interface CustomOp {\n /** Command path, e.g. `[\"docs\"]` → `shipeasy docs <name>`, `[\"metrics\"]` → `metrics <name>`. */\n group: string[];\n /** Verb. */\n name: string;\n summary: string;\n description?: string;\n params: CustomParam[];\n examples?: { note?: string; run: string }[];\n /** Self-contained handler. `args` are the coerced param values. */\n run: (args: Record<string, unknown>) => Promise<unknown> | unknown;\n}\n\n/**\n * Group-level help for a custom-op command group (e.g. `docs`). Mirrors what an\n * OpenAPI tag carries for the generated tree — a short `summary` (shown in the\n * parent `--help` list) and a long `description` (shown on the group's own\n * `--help`). Consumers look this up by `group` path when creating the group so a\n * custom group reads as richly as a spec-backed one, instead of the bare\n * `\"<name> commands\"` fallback.\n */\nexport interface CustomGroup {\n /** Group path, e.g. `[\"docs\"]`. */\n group: string[];\n summary: string;\n description?: string;\n aliases?: string[];\n}\n\n/** Thrown by a custom op for a user-facing error with an HTTP-ish status. */\nexport class CustomOpError extends Error {\n constructor(\n message: string,\n public status = 400,\n ) {\n super(message);\n this.name = \"CustomOpError\";\n }\n}\n\nexport const opId = (op: CustomOp): string => [...op.group, op.name].join(\".\");\n","import { type CustomOp, CustomOpError } from \"./types.js\";\n\n/**\n * SDK-docs operations — `docs list/get/skill`. A generic fetcher over each SDK\n * repo's `/docs/` folder published to plain GitHub Pages (latest only, no\n * backend, no version arg). The `run`s do outbound `fetch` (worker-safe — not\n * the fs/AST boundary) and fall back to a built-in default doc set for repos\n * that haven't adopted the doc standard yet.\n */\nconst SDK_REPOS: Record<string, { owner: string; repo: string }> = {\n typescript: { owner: \"shipeasy-ai\", repo: \"sdk-ts\" },\n javascript: { owner: \"shipeasy-ai\", repo: \"sdk-ts\" },\n node: { owner: \"shipeasy-ai\", repo: \"sdk-ts\" },\n ts: { owner: \"shipeasy-ai\", repo: \"sdk-ts\" },\n python: { owner: \"shipeasy-ai\", repo: \"sdk-python\" },\n go: { owner: \"shipeasy-ai\", repo: \"sdk-go\" },\n java: { owner: \"shipeasy-ai\", repo: \"sdk-java\" },\n kotlin: { owner: \"shipeasy-ai\", repo: \"sdk-kotlin\" },\n php: { owner: \"shipeasy-ai\", repo: \"sdk-php\" },\n swift: { owner: \"shipeasy-ai\", repo: \"sdk-swift\" },\n ruby: { owner: \"shipeasy-ai\", repo: \"sdk-ruby\" },\n};\nconst SDK_ENUM = Object.keys(SDK_REPOS);\n\ninterface DocManifest {\n schemaVersion: number;\n sdk: string;\n placeholders?: string[];\n skill?: string;\n pages?: Record<string, string>;\n snippets?: Record<string, Record<string, string>>;\n}\n\nconst DEFAULT_MANIFEST: DocManifest = {\n schemaVersion: 2,\n sdk: \"default\",\n placeholders: [\"RESOURCE_NAME\"],\n skill: \"skill/SKILL.md\",\n pages: { overview: \"pages/overview.md\", installation: \"pages/installation.md\" },\n snippets: { release: { flags: \"snippets/release/flags.md\" } },\n};\nconst DEFAULT_CONTENT: Record<string, string> = {\n \"pages/overview.md\":\n \"# Shipeasy SDK\\n\\nThis SDK has not published its `/docs/` folder yet. See https://docs.shipeasy.ai for the full reference.\",\n \"pages/installation.md\":\n \"# Installation\\n\\nInstall the Shipeasy SDK for your language, then call `shipeasy({ serverKey })` (server) or `shipeasy({ clientKey })` (browser).\",\n \"snippets/release/flags.md\": \"```\\nconst on = await client.checkGate(user, '{{RESOURCE_NAME}}');\\n```\",\n \"skill/SKILL.md\":\n \"---\\nname: shipeasy-sdk\\ndescription: Use the Shipeasy SDK for flags, experiments, configs, and i18n.\\n---\\n\\nThis SDK has not published a skill yet. See https://docs.shipeasy.ai.\",\n};\n\n/**\n * Resolve the caller-supplied `sdk`. The adapters (CLI/MCP) default it from the\n * nearest `.shipeasy` before we get here, so an empty value means neither a flag\n * nor a bound project supplied one — surface that explicitly.\n */\nfunction requireSdk(sdk: unknown): string {\n const s = typeof sdk === \"string\" ? sdk.trim() : \"\";\n if (!s)\n throw new CustomOpError(\n `No SDK given and none recorded in .shipeasy. Pass --sdk <lang> (one of: ${SDK_ENUM.join(\", \")}), ` +\n `or run \\`shipeasy detect\\` in the project to record its sdk.`,\n 400,\n );\n return s;\n}\nfunction repoFor(sdk: string): { owner: string; repo: string } {\n const r = SDK_REPOS[sdk.toLowerCase()];\n if (!r) throw new CustomOpError(`Unknown SDK '${sdk}'. One of: ${SDK_ENUM.join(\", \")}`, 400);\n return r;\n}\nfunction pagesBase(sdk: string): string {\n const { owner, repo } = repoFor(sdk);\n return `https://${owner}.github.io/${repo}`;\n}\nasync function fetchText(url: string): Promise<string | null> {\n const res = await fetch(url);\n if (!res.ok) return null;\n return res.text();\n}\nasync function loadManifest(sdk: string): Promise<{ manifest: DocManifest; fallback: boolean }> {\n const text = await fetchText(`${pagesBase(sdk)}/manifest.json`);\n if (!text) return { manifest: DEFAULT_MANIFEST, fallback: true };\n try {\n return { manifest: JSON.parse(text) as DocManifest, fallback: false };\n } catch {\n return { manifest: DEFAULT_MANIFEST, fallback: true };\n }\n}\nfunction resolvePath(manifest: DocManifest, handle: string): string {\n if (handle.endsWith(\".md\")) return handle;\n if (manifest.pages?.[handle]) return manifest.pages[handle];\n if (handle.includes(\"/\")) {\n const [group, resource] = handle.split(\"/\", 2);\n const p = manifest.snippets?.[group]?.[resource];\n if (p) return p;\n }\n throw new CustomOpError(\n `Doc '${handle}' not found. Run 'docs list --sdk <lang>' to see available pages and snippets.`,\n 404,\n );\n}\nfunction substitute(body: string, manifest: DocManifest, args: Record<string, unknown>): string {\n let out = body;\n for (const ph of manifest.placeholders ?? []) {\n const value =\n (args[ph] as string | undefined) ??\n (ph === \"RESOURCE_NAME\" ? (args.name as string | undefined) : undefined) ??\n (ph === \"FRAMEWORK\" ? (args.framework as string | undefined) : undefined);\n if (value !== undefined) out = out.replaceAll(`{{${ph}}}`, value);\n }\n return out;\n}\n\nexport const docsOps: CustomOp[] = [\n {\n group: [\"docs\"],\n name: \"list\",\n summary: \"List an SDK's documentation tree\",\n description:\n \"Fetch an SDK's `/docs/manifest.json` and return the doc tree — feature pages, nested snippet groups, and whether an installable skill exists.\",\n params: [{ name: \"sdk\", type: \"string\", description: \"SDK language. Defaults to the `sdk` recorded in the nearest `.shipeasy` when omitted.\", required: false, enum: SDK_ENUM }],\n examples: [{ run: \"shipeasy docs list --sdk python\" }],\n run: async (args) => {\n const sdk = requireSdk(args.sdk);\n const { manifest, fallback } = await loadManifest(sdk);\n return {\n sdk,\n fallback,\n pages: Object.keys(manifest.pages ?? {}),\n snippets: Object.fromEntries(\n Object.entries(manifest.snippets ?? {}).map(([g, r]) => [g, Object.keys(r)]),\n ),\n skill: Boolean(manifest.skill),\n };\n },\n },\n {\n group: [\"docs\"],\n name: \"get\",\n summary: \"Fetch one SDK doc page or snippet\",\n description:\n \"Fetch one feature page (`flags`, `experiments`, …) or nested snippet (`release/experiments`, …), substituting declared {{placeholders}} from caller args.\",\n params: [\n { name: \"sdk\", type: \"string\", description: \"SDK language. Defaults to the `sdk` recorded in the nearest `.shipeasy` when omitted.\", required: false, enum: SDK_ENUM },\n { name: \"path\", type: \"string\", description: \"Page key or snippet 'group/resource'.\", required: true, positional: true },\n { name: \"framework\", type: \"string\", description: \"Framework hint (substitutes {{FRAMEWORK}}).\" },\n { name: \"name\", type: \"string\", description: \"Resource name (substitutes {{RESOURCE_NAME}}).\" },\n ],\n examples: [\n { note: \"A feature page\", run: \"shipeasy docs get --sdk python experiments\" },\n { note: \"A nested snippet\", run: \"shipeasy docs get --sdk typescript release/flags --name checkout_v2\" },\n ],\n run: async (args) => {\n const sdk = requireSdk(args.sdk);\n const { manifest, fallback } = await loadManifest(sdk);\n const path = resolvePath(manifest, args.path as string);\n const body = fallback ? DEFAULT_CONTENT[path] : ((await fetchText(`${pagesBase(sdk)}/${path}`)) ?? undefined);\n if (body === undefined) throw new CustomOpError(`Doc '${path}' could not be fetched for '${sdk}'.`, 404);\n return { sdk, path, fallback, content: substitute(body, manifest, args) };\n },\n },\n {\n group: [\"docs\"],\n name: \"skill\",\n summary: \"Fetch an SDK's installable LLM skill\",\n description:\n \"Fetch the SDK's `skill/SKILL.md` (frontmatter intact) so an agent can install it verbatim. The CLI `--install` writes it locally (a consumer fs side-effect).\",\n params: [\n { name: \"sdk\", type: \"string\", description: \"SDK language. Defaults to the `sdk` recorded in the nearest `.shipeasy` when omitted.\", required: false, enum: SDK_ENUM },\n { name: \"install\", type: \"boolean\", description: \"CLI only: write the skill to the local agent skills dir.\" },\n ],\n examples: [{ run: \"shipeasy docs skill --sdk python\" }],\n run: async (args) => {\n const sdk = requireSdk(args.sdk);\n const { manifest, fallback } = await loadManifest(sdk);\n const skillPath = manifest.skill ?? \"skill/SKILL.md\";\n const content = fallback ? DEFAULT_CONTENT[skillPath] : ((await fetchText(`${pagesBase(sdk)}/${skillPath}`)) ?? undefined);\n if (content === undefined) throw new CustomOpError(`No skill published for '${sdk}'.`, 404);\n return { sdk, skillPath, fallback, content };\n },\n },\n];\n","import { grammarOp } from \"./grammar.js\";\nimport { docsOps } from \"./docs.js\";\nimport type { CustomOp, CustomGroup } from \"./types.js\";\n\nexport type { CustomOp, CustomGroup, CustomParam, CustomParamType } from \"./types.js\";\nexport { CustomOpError, opId } from \"./types.js\";\nexport { METRIC_GRAMMAR } from \"./grammar.js\";\n\n/**\n * Every custom (non-spec) operation, in surface order. The CLI and MCP each\n * project this with a thin adapter so the sugar commands stay in sync.\n */\nexport const customOperations: CustomOp[] = [grammarOp, ...docsOps];\n\n/**\n * Group-level help for custom-op groups that aren't backed by an OpenAPI tag.\n * `metrics` is a spec tag (its group is created — and described — by the\n * generated tree), so only `docs` needs an entry here; without it the CLI would\n * label the group the bare `\"docs commands\"`.\n */\nexport const customGroups: CustomGroup[] = [\n {\n group: [\"docs\"],\n summary:\n \"SDK docs: fetch a language SDK's published docs — its page tree, feature pages, code snippets, and installable agent skill.\",\n description:\n \"SDK docs — a read-only fetcher over each language SDK's `/docs/` folder, \" +\n \"published to plain GitHub Pages (latest only; no backend, no version arg). \" +\n \"Point it at an SDK with `--sdk <lang>` (one of: typescript, python, go, \" +\n \"java, kotlin, php, swift, ruby) or let it default to the `sdk` recorded in \" +\n \"the nearest `.shipeasy`.\\n\\n\" +\n \"**Commands.**\\n\" +\n \"- `list` — the SDK's doc tree: feature pages, nested snippet groups, and whether an installable skill exists.\\n\" +\n \"- `get <path>` — one feature page (`flags`, `experiments`, …) or nested snippet (`release/flags`), with `{{placeholders}}` substituted from `--name`/`--framework`.\\n\" +\n \"- `skill` — the SDK's installable LLM skill (`SKILL.md`); `--install` writes it into your agent's skills dir.\\n\\n\" +\n \"SDKs that haven't adopted the doc standard yet fall back to a built-in \" +\n \"default doc set, flagged `fallback: true` in the output.\",\n },\n];\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/custom/grammar.ts","../src/custom/types.ts","../src/custom/docs.ts","../src/custom/index.ts"],"sourcesContent":["import type { CustomOp } from \"./types.js\";\n\n/**\n * The Shipeasy metric query DSL grammar — printed by `shipeasy metrics grammar`\n * / the `metrics_grammar` MCP tool.\n *\n * This is the one-stop reference for both humans and AI agents that need to\n * author a `metrics create --query \"...\"` string. It is intentionally\n * self-contained: the formal grammar (precise, for machine parsing), a full\n * aggregation reference (semantics + value-label rules + experiment behaviour),\n * the selector/filter/group-by/ratio rules, hard constraints, and glossed\n * examples.\n *\n * Source of truth for semantics is the RUNTIME parser + IR that the server\n * actually runs: the main repo's `packages/query-dsl` (`parse.ts` / `ir.ts`).\n * The vendored `cli/src/query-dsl` copy is a lagging subset (no `ratio` yet) and\n * is NOT on the create path — the CLI forwards the raw `--query` string to the\n * server, which parses it. Keep this text in sync with `packages/query-dsl`.\n */\nexport const METRIC_GRAMMAR = `SHIPEASY METRIC QUERY DSL\n=========================\n\nA metric is an aggregation over an event stream. Almost always that is ONE\naggregation over ONE event; the single exception is the binary \\`ratio(...)\\`\nform, which divides one count-style arm by another (each arm its own event and\nfilters) in one pass. This DSL is the string you pass to\n\\`metrics create --query \"...\"\\` (equivalently the typed \\`query_ir\\`). There is\nno general arithmetic, no free-form formulas, and no subqueries — \\`ratio\\` is\nthe only division / two-event form.\n\n\nGRAMMAR (BNF)\n-------------\n Query := Agg | Ratio\n Agg := AggFunc \"(\" Selector (\",\" Identifier)? \")\" GroupBy?\n Ratio := \"ratio\" \"(\" RatioArm \",\" RatioArm \")\"\n RatioArm := (\"count_users\" | \"count\") \"(\" Selector \")\"\n AggFunc := \"count_users\" | \"count\" | \"sum\" | \"avg\" | \"min\" | \"max\"\n | \"unique\" | \"p50\" | \"p75\" | \"p90\" | \"p95\" | \"p99\" | \"p999\"\n | \"retention_\" <N> \"d\" (N = 1..90)\n Selector := Identifier (\"{\" Filter (\",\" Filter)* \",\"? \"}\")?\n Filter := Identifier MatchOp StringLiteral\n MatchOp := \"=\" | \"!=\" | \"=~\" | \"!~\"\n GroupBy := (\"by\" | \"without\") \"(\" Identifier (\",\" Identifier)* \",\"? \")\"\n StringLiteral := double-quoted; \\\\\" is a literal quote, \\\\\\\\ is a backslash\n Identifier := [A-Za-z_][A-Za-z0-9_]* (case-sensitive)\n\n The trailing Identifier after the selector — \"(\" Selector \",\" Identifier \")\" —\n is the VALUE LABEL: the numeric event label the aggregation reduces. Whether it\n is required, optional, or forbidden depends on the aggregation (see below).\n\n\nAGGREGATIONS\n------------\n Function Value label Meaning In experiments\n -------------- ------------ ------------------------------ ---------------\n count_users forbidden distinct users who fired it exact\n count forbidden number of events (rows) exact\n sum(e, v) required Σ of numeric label v exact\n avg(e, v) required mean of label v exact\n retention_Nd(e) forbidden % of users who returned exact\n within N days (N = 1..90)\n min(e, v) required minimum of label v approx → avg *\n max(e, v) required maximum of label v approx → avg *\n unique(e, v) required distinct values of label v approx → avg *\n p50 … p999(e,v) required quantile of v (50/75/90/ approx → avg *\n 95/99/99.9th percentile)\n ratio(a, b) forbidden a ÷ b (see RATIO below) proportion **\n\n * Display-only aggregations. The experiment t-test needs a per-user mean and\n variance, so min/max/unique/quantile are computed exactly on dashboards but\n collapse to a per-user \\`avg\\` when used as an experiment metric. If you need\n an exact experiment metric, pick count_users / count / sum / avg / retention.\n\n ** In an experiment a ratio collapses to a per-user 0/1 outcome (a proportion),\n not a ratio-of-sums — see RATIO.\n\n \"count\" is the DSL spelling of the internal count_events (one per matching\n row); \"count_users\" is unique-by-user. Getting these two confused is the most\n common mistake — count double-counts a user who fired the event twice.\n\n\nSELECTOR & FILTERS\n------------------\n The selector is the event name plus an optional \\`{ ... }\\` filter set that\n narrows which events count. Each filter compares an event LABEL to a quoted\n string:\n\n = equal != not equal\n =~ regex match !~ regex NOT match\n\n - Values are ALWAYS double-quoted strings, even for numeric labels — they are\n coerced on the server (\\`amount=~\"^1[0-9]$\"\\`, \\`status=\"200\"\\`).\n - Multiple filters are AND-ed: \\`{country=\"US\", tier!=\"free\"}\\`.\n - Every filter label AND the value label must be a label DECLARED on the source\n event. Undeclared labels are a validation error, not a silent no-match.\n\n\nGROUP-BY\n--------\n Splits the result into one series per label combination.\n by (a, b) one series per distinct (a, b) — keep only these labels\n without (a, b) group by all declared labels EXCEPT a, b\n\n NOTE: group-by is a DASHBOARD-only feature. In an experiment the group-by is\n ignored — a per-user reducer is applied across the whole arm instead. Group-by\n is NOT allowed on a \\`ratio\\` query.\n\n\nRATIO (success rate, conversion, failure %, …)\n-----------------------------------------------\n \\`ratio(numerator, denominator)\\` divides one count-style arm by another. Each\n arm is an independent selector with its OWN event and filters, so the two arms\n can be different events:\n\n ratio(count(checkout_completed), count(checkout_started))\n ratio(count_users(paid), count_users(signed_up))\n ratio(count(payment{ok=\"1\"}), count(payment)) one event, filtered arm\n\n Rules:\n - Arms are limited to \\`count\\` and \\`count_users\\` — no sum/avg/quantile arm.\n - \\`count\\` arms divide event counts; \\`count_users\\` arms divide distinct-user\n counts. Pick one shape and use it on both arms unless you mean to mix them.\n - A zero denominator yields 0, not an error.\n - No \\`by (...)\\` / \\`without (...)\\`, and no retention arm, on a ratio.\n - It is a COHORT RATE over the window — numerator events ÷ denominator events —\n NOT a per-id join. It never matches an individual attempt to its outcome by a\n correlation id; it counts each side over the window and divides. If you need a\n matched pair, resolve the outcome in your own code and emit ONE event carrying\n the result (e.g. an \\`ok\\` label), then use \\`ratio(count(e{ok=\"1\"}), count(e))\\`\n or \\`avg(e, ok)\\`.\n - In an experiment a per-user ratio is a 0/1 outcome — did the numerator happen,\n among denominator-eligible users — i.e. a proportion, not a ratio-of-sums.\n - When creating with \\`--event-name\\`, pass the NUMERATOR's event name.\n\n\nNOT SUPPORTED (will fail to parse)\n----------------------------------\n - general arithmetic / free-form formulas: \\`count(a) + count(b)\\`, \\`2 * sum(x)\\`,\n \\`avg(a) / avg(b)\\` — the ONLY division form is the fixed binary\n \\`ratio(arm, arm)\\` with count-style arms (see RATIO)\n - a ratio arm that isn't \\`count\\` / \\`count_users\\`: \\`ratio(sum(a, v), count(b))\\`\n - more than two events in one query\n - \\`by (...)\\` / \\`without (...)\\` on a ratio\n - arbitrary quantiles (only the fixed p50/p75/p90/p95/p99/p999)\n - unquoted or numeric filter values: \\`{status=200}\\` → must be \\`\"200\"\\`\n - trailing tokens after the query\n\n\nEXAMPLES (query — what it measures)\n--------------------------------------\n count_users(checkout_completed)\n distinct users who completed checkout.\n\n count(add_to_cart{country=\"US\"})\n number of US add-to-cart events (a user can count many times).\n\n sum(purchase{country=\"US\"}, amount)\n total purchase amount from US purchase events.\n\n avg(req_dur{tier!=\"free\"}, ms) without (region)\n mean request duration for non-free tiers, one series per region-excluded\n label combination.\n\n p99(req_dur{route=~\"/api/.*\"}, ms) by (route, status)\n 99th-percentile latency of /api/* requests, split by route and status.\n\n unique(login{method=\"sso\"}, device_id)\n distinct SSO devices seen (display-only; an experiment would use per-user avg).\n\n retention_7d(session_start)\n share of users who started a new session within 7 days.\n\n ratio(count(checkout_completed), count(checkout_started))\n checkout conversion rate — completions ÷ starts over the window.\n\n ratio(count(payment{ok=\"0\"}), count(payment))\n payment failure rate from a single event's outcome label.\n`;\n\nexport const grammarOp: CustomOp = {\n group: [\"metrics\"],\n name: \"grammar\",\n summary: \"Print the metric query DSL grammar\",\n description:\n \"Print the full metric query DSL reference — grammar, aggregation semantics, \" +\n \"filter/group-by/ratio rules, and glossed examples — used to author `metrics create --query`.\",\n params: [],\n examples: [{ run: \"shipeasy metrics grammar\" }],\n run: () => ({ grammar: METRIC_GRAMMAR }),\n};\n","/**\n * The custom-operations registry — hand-written operations that are NOT admin\n * API endpoints (so they can't be generated from the spec): the metric-DSL\n * grammar (pure) and the SDK-docs fetcher (outbound `fetch` to GitHub Pages).\n *\n * It mirrors the old operations registry in spirit, but is deliberately tiny and\n * framework-agnostic: each `CustomOp` carries its command path, params, and a\n * self-contained `run`. Both consumers project it with a thin adapter — the CLI\n * (`commands/custom.ts`) and, later, the MCP server — so the surface stays in\n * sync across them. `run` never touches the filesystem, so it's worker-safe.\n */\nexport type CustomParamType = \"string\" | \"number\" | \"boolean\";\n\nexport interface CustomParam {\n name: string;\n type: CustomParamType;\n description?: string;\n required?: boolean;\n /** Render as a positional CLI argument (vs a `--flag`). */\n positional?: boolean;\n enum?: string[];\n}\n\nexport interface CustomOp {\n /** Command path, e.g. `[\"docs\"]` → `shipeasy docs <name>`, `[\"metrics\"]` → `metrics <name>`. */\n group: string[];\n /** Verb. */\n name: string;\n summary: string;\n description?: string;\n params: CustomParam[];\n examples?: { note?: string; run: string }[];\n /** Self-contained handler. `args` are the coerced param values. */\n run: (args: Record<string, unknown>) => Promise<unknown> | unknown;\n}\n\n/**\n * Group-level help for a custom-op command group (e.g. `docs`). Mirrors what an\n * OpenAPI tag carries for the generated tree — a short `summary` (shown in the\n * parent `--help` list) and a long `description` (shown on the group's own\n * `--help`). Consumers look this up by `group` path when creating the group so a\n * custom group reads as richly as a spec-backed one, instead of the bare\n * `\"<name> commands\"` fallback.\n */\nexport interface CustomGroup {\n /** Group path, e.g. `[\"docs\"]`. */\n group: string[];\n summary: string;\n description?: string;\n aliases?: string[];\n}\n\n/** Thrown by a custom op for a user-facing error with an HTTP-ish status. */\nexport class CustomOpError extends Error {\n constructor(\n message: string,\n public status = 400,\n ) {\n super(message);\n this.name = \"CustomOpError\";\n }\n}\n\nexport const opId = (op: CustomOp): string => [...op.group, op.name].join(\".\");\n","import { type CustomOp, CustomOpError } from \"./types.js\";\n\n/**\n * SDK-docs operations — `docs list/get/skill`. A generic fetcher over each SDK\n * repo's `/docs/` folder published to plain GitHub Pages (latest only, no\n * backend, no version arg). The `run`s do outbound `fetch` (worker-safe — not\n * the fs/AST boundary) and fall back to a built-in default doc set for repos\n * that haven't adopted the doc standard yet.\n */\nconst SDK_REPOS: Record<string, { owner: string; repo: string }> = {\n typescript: { owner: \"shipeasy-ai\", repo: \"sdk-ts\" },\n javascript: { owner: \"shipeasy-ai\", repo: \"sdk-ts\" },\n node: { owner: \"shipeasy-ai\", repo: \"sdk-ts\" },\n ts: { owner: \"shipeasy-ai\", repo: \"sdk-ts\" },\n python: { owner: \"shipeasy-ai\", repo: \"sdk-python\" },\n go: { owner: \"shipeasy-ai\", repo: \"sdk-go\" },\n java: { owner: \"shipeasy-ai\", repo: \"sdk-java\" },\n kotlin: { owner: \"shipeasy-ai\", repo: \"sdk-kotlin\" },\n php: { owner: \"shipeasy-ai\", repo: \"sdk-php\" },\n swift: { owner: \"shipeasy-ai\", repo: \"sdk-swift\" },\n ruby: { owner: \"shipeasy-ai\", repo: \"sdk-ruby\" },\n};\nconst SDK_ENUM = Object.keys(SDK_REPOS);\n\ninterface DocManifest {\n schemaVersion: number;\n sdk: string;\n placeholders?: string[];\n skill?: string;\n pages?: Record<string, string>;\n snippets?: Record<string, Record<string, string>>;\n}\n\nconst DEFAULT_MANIFEST: DocManifest = {\n schemaVersion: 2,\n sdk: \"default\",\n placeholders: [\"RESOURCE_NAME\"],\n skill: \"skill/SKILL.md\",\n pages: { overview: \"pages/overview.md\", installation: \"pages/installation.md\" },\n snippets: { release: { flags: \"snippets/release/flags.md\" } },\n};\nconst DEFAULT_CONTENT: Record<string, string> = {\n \"pages/overview.md\":\n \"# Shipeasy SDK\\n\\nThis SDK has not published its `/docs/` folder yet. See https://docs.shipeasy.ai for the full reference.\",\n \"pages/installation.md\":\n \"# Installation\\n\\nInstall the Shipeasy SDK for your language, then call `shipeasy({ serverKey })` (server) or `shipeasy({ clientKey })` (browser).\",\n \"snippets/release/flags.md\": \"```\\nconst on = await client.checkGate(user, '{{RESOURCE_NAME}}');\\n```\",\n \"skill/SKILL.md\":\n \"---\\nname: shipeasy-sdk\\ndescription: Use the Shipeasy SDK for flags, experiments, configs, and i18n.\\n---\\n\\nThis SDK has not published a skill yet. See https://docs.shipeasy.ai.\",\n};\n\n/**\n * Resolve the caller-supplied `sdk`. The adapters (CLI/MCP) default it from the\n * nearest `.shipeasy` before we get here, so an empty value means neither a flag\n * nor a bound project supplied one — surface that explicitly.\n */\nfunction requireSdk(sdk: unknown): string {\n const s = typeof sdk === \"string\" ? sdk.trim() : \"\";\n if (!s)\n throw new CustomOpError(\n `No SDK given and none recorded in .shipeasy. Pass --sdk <lang> (one of: ${SDK_ENUM.join(\", \")}), ` +\n `or run \\`shipeasy detect\\` in the project to record its sdk.`,\n 400,\n );\n return s;\n}\nfunction repoFor(sdk: string): { owner: string; repo: string } {\n const r = SDK_REPOS[sdk.toLowerCase()];\n if (!r) throw new CustomOpError(`Unknown SDK '${sdk}'. One of: ${SDK_ENUM.join(\", \")}`, 400);\n return r;\n}\nfunction pagesBase(sdk: string): string {\n const { owner, repo } = repoFor(sdk);\n return `https://${owner}.github.io/${repo}`;\n}\nasync function fetchText(url: string): Promise<string | null> {\n const res = await fetch(url);\n if (!res.ok) return null;\n return res.text();\n}\nasync function loadManifest(sdk: string): Promise<{ manifest: DocManifest; fallback: boolean }> {\n const text = await fetchText(`${pagesBase(sdk)}/manifest.json`);\n if (!text) return { manifest: DEFAULT_MANIFEST, fallback: true };\n try {\n return { manifest: JSON.parse(text) as DocManifest, fallback: false };\n } catch {\n return { manifest: DEFAULT_MANIFEST, fallback: true };\n }\n}\nfunction resolvePath(manifest: DocManifest, handle: string): string {\n if (handle.endsWith(\".md\")) return handle;\n if (manifest.pages?.[handle]) return manifest.pages[handle];\n if (handle.includes(\"/\")) {\n const [group, resource] = handle.split(\"/\", 2);\n const p = manifest.snippets?.[group]?.[resource];\n if (p) return p;\n }\n throw new CustomOpError(\n `Doc '${handle}' not found. Run 'docs list --sdk <lang>' to see available pages and snippets.`,\n 404,\n );\n}\nfunction substitute(body: string, manifest: DocManifest, args: Record<string, unknown>): string {\n let out = body;\n for (const ph of manifest.placeholders ?? []) {\n const value =\n (args[ph] as string | undefined) ??\n (ph === \"RESOURCE_NAME\" ? (args.name as string | undefined) : undefined) ??\n (ph === \"FRAMEWORK\" ? (args.framework as string | undefined) : undefined);\n if (value !== undefined) out = out.replaceAll(`{{${ph}}}`, value);\n }\n return out;\n}\n\nexport const docsOps: CustomOp[] = [\n {\n group: [\"docs\"],\n name: \"list\",\n summary: \"List an SDK's documentation tree\",\n description:\n \"Fetch an SDK's `/docs/manifest.json` and return the doc tree — feature pages, nested snippet groups, and whether an installable skill exists.\",\n params: [{ name: \"sdk\", type: \"string\", description: \"SDK language. Defaults to the `sdk` recorded in the nearest `.shipeasy` when omitted.\", required: false, enum: SDK_ENUM }],\n examples: [{ run: \"shipeasy docs list --sdk python\" }],\n run: async (args) => {\n const sdk = requireSdk(args.sdk);\n const { manifest, fallback } = await loadManifest(sdk);\n return {\n sdk,\n fallback,\n pages: Object.keys(manifest.pages ?? {}),\n snippets: Object.fromEntries(\n Object.entries(manifest.snippets ?? {}).map(([g, r]) => [g, Object.keys(r)]),\n ),\n skill: Boolean(manifest.skill),\n };\n },\n },\n {\n group: [\"docs\"],\n name: \"get\",\n summary: \"Fetch one SDK doc page or snippet\",\n description:\n \"Fetch one feature page (`flags`, `experiments`, …) or nested snippet (`release/experiments`, …), substituting declared {{placeholders}} from caller args.\",\n params: [\n { name: \"sdk\", type: \"string\", description: \"SDK language. Defaults to the `sdk` recorded in the nearest `.shipeasy` when omitted.\", required: false, enum: SDK_ENUM },\n { name: \"path\", type: \"string\", description: \"Page key or snippet 'group/resource'.\", required: true, positional: true },\n { name: \"framework\", type: \"string\", description: \"Framework hint (substitutes {{FRAMEWORK}}).\" },\n { name: \"name\", type: \"string\", description: \"Resource name (substitutes {{RESOURCE_NAME}}).\" },\n ],\n examples: [\n { note: \"A feature page\", run: \"shipeasy docs get --sdk python experiments\" },\n { note: \"A nested snippet\", run: \"shipeasy docs get --sdk typescript release/flags --name checkout_v2\" },\n ],\n run: async (args) => {\n const sdk = requireSdk(args.sdk);\n const { manifest, fallback } = await loadManifest(sdk);\n const path = resolvePath(manifest, args.path as string);\n const body = fallback ? DEFAULT_CONTENT[path] : ((await fetchText(`${pagesBase(sdk)}/${path}`)) ?? undefined);\n if (body === undefined) throw new CustomOpError(`Doc '${path}' could not be fetched for '${sdk}'.`, 404);\n return { sdk, path, fallback, content: substitute(body, manifest, args) };\n },\n },\n {\n group: [\"docs\"],\n name: \"skill\",\n summary: \"Fetch an SDK's installable LLM skill\",\n description:\n \"Fetch the SDK's `skill/SKILL.md` (frontmatter intact) so an agent can install it verbatim. The CLI `--install` writes it locally (a consumer fs side-effect).\",\n params: [\n { name: \"sdk\", type: \"string\", description: \"SDK language. Defaults to the `sdk` recorded in the nearest `.shipeasy` when omitted.\", required: false, enum: SDK_ENUM },\n { name: \"install\", type: \"boolean\", description: \"CLI only: write the skill to the local agent skills dir.\" },\n ],\n examples: [{ run: \"shipeasy docs skill --sdk python\" }],\n run: async (args) => {\n const sdk = requireSdk(args.sdk);\n const { manifest, fallback } = await loadManifest(sdk);\n const skillPath = manifest.skill ?? \"skill/SKILL.md\";\n const content = fallback ? DEFAULT_CONTENT[skillPath] : ((await fetchText(`${pagesBase(sdk)}/${skillPath}`)) ?? undefined);\n if (content === undefined) throw new CustomOpError(`No skill published for '${sdk}'.`, 404);\n return { sdk, skillPath, fallback, content };\n },\n },\n];\n","import { grammarOp } from \"./grammar.js\";\nimport { docsOps } from \"./docs.js\";\nimport type { CustomOp, CustomGroup } from \"./types.js\";\n\nexport type { CustomOp, CustomGroup, CustomParam, CustomParamType } from \"./types.js\";\nexport { CustomOpError, opId } from \"./types.js\";\nexport { METRIC_GRAMMAR } from \"./grammar.js\";\n\n/**\n * Every custom (non-spec) operation, in surface order. The CLI and MCP each\n * project this with a thin adapter so the sugar commands stay in sync.\n */\nexport const customOperations: CustomOp[] = [grammarOp, ...docsOps];\n\n/**\n * Group-level help for custom-op groups that aren't backed by an OpenAPI tag.\n * `metrics` is a spec tag (its group is created — and described — by the\n * generated tree), so only `docs` needs an entry here; without it the CLI would\n * label the group the bare `\"docs commands\"`.\n */\nexport const customGroups: CustomGroup[] = [\n {\n group: [\"docs\"],\n summary:\n \"SDK docs: fetch a language SDK's published docs — its page tree, feature pages, code snippets, and installable agent skill.\",\n description:\n \"SDK docs — a read-only fetcher over each language SDK's `/docs/` folder, \" +\n \"published to plain GitHub Pages (latest only; no backend, no version arg). \" +\n \"Point it at an SDK with `--sdk <lang>` (one of: typescript, python, go, \" +\n \"java, kotlin, php, swift, ruby) or let it default to the `sdk` recorded in \" +\n \"the nearest `.shipeasy`.\\n\\n\" +\n \"**Commands.**\\n\" +\n \"- `list` — the SDK's doc tree: feature pages, nested snippet groups, and whether an installable skill exists.\\n\" +\n \"- `get <path>` — one feature page (`flags`, `experiments`, …) or nested snippet (`release/flags`), with `{{placeholders}}` substituted from `--name`/`--framework`.\\n\" +\n \"- `skill` — the SDK's installable LLM skill (`SKILL.md`); `--install` writes it into your agent's skills dir.\\n\\n\" +\n \"SDKs that haven't adopted the doc standard yet fall back to a built-in \" +\n \"default doc set, flagged `fallback: true` in the output.\",\n },\n];\n"],"mappings":";AAmBO,IAAM,iBAAiB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiKvB,IAAM,YAAsB;AAAA,EACjC,OAAO,CAAC,SAAS;AAAA,EACjB,MAAM;AAAA,EACN,SAAS;AAAA,EACT,aACE;AAAA,EAEF,QAAQ,CAAC;AAAA,EACT,UAAU,CAAC,EAAE,KAAK,2BAA2B,CAAC;AAAA,EAC9C,KAAK,OAAO,EAAE,SAAS,eAAe;AACxC;;;ACzIO,IAAM,gBAAN,cAA4B,MAAM;AAAA,EACvC,YACE,SACO,SAAS,KAChB;AACA,UAAM,OAAO;AAFN;AAGP,SAAK,OAAO;AAAA,EACd;AAAA,EAJS;AAKX;AAEO,IAAM,OAAO,CAAC,OAAyB,CAAC,GAAG,GAAG,OAAO,GAAG,IAAI,EAAE,KAAK,GAAG;;;ACtD7E,IAAM,YAA6D;AAAA,EACjE,YAAY,EAAE,OAAO,eAAe,MAAM,SAAS;AAAA,EACnD,YAAY,EAAE,OAAO,eAAe,MAAM,SAAS;AAAA,EACnD,MAAM,EAAE,OAAO,eAAe,MAAM,SAAS;AAAA,EAC7C,IAAI,EAAE,OAAO,eAAe,MAAM,SAAS;AAAA,EAC3C,QAAQ,EAAE,OAAO,eAAe,MAAM,aAAa;AAAA,EACnD,IAAI,EAAE,OAAO,eAAe,MAAM,SAAS;AAAA,EAC3C,MAAM,EAAE,OAAO,eAAe,MAAM,WAAW;AAAA,EAC/C,QAAQ,EAAE,OAAO,eAAe,MAAM,aAAa;AAAA,EACnD,KAAK,EAAE,OAAO,eAAe,MAAM,UAAU;AAAA,EAC7C,OAAO,EAAE,OAAO,eAAe,MAAM,YAAY;AAAA,EACjD,MAAM,EAAE,OAAO,eAAe,MAAM,WAAW;AACjD;AACA,IAAM,WAAW,OAAO,KAAK,SAAS;AAWtC,IAAM,mBAAgC;AAAA,EACpC,eAAe;AAAA,EACf,KAAK;AAAA,EACL,cAAc,CAAC,eAAe;AAAA,EAC9B,OAAO;AAAA,EACP,OAAO,EAAE,UAAU,qBAAqB,cAAc,wBAAwB;AAAA,EAC9E,UAAU,EAAE,SAAS,EAAE,OAAO,4BAA4B,EAAE;AAC9D;AACA,IAAM,kBAA0C;AAAA,EAC9C,qBACE;AAAA,EACF,yBACE;AAAA,EACF,6BAA6B;AAAA,EAC7B,kBACE;AACJ;AAOA,SAAS,WAAW,KAAsB;AACxC,QAAM,IAAI,OAAO,QAAQ,WAAW,IAAI,KAAK,IAAI;AACjD,MAAI,CAAC;AACH,UAAM,IAAI;AAAA,MACR,2EAA2E,SAAS,KAAK,IAAI,CAAC;AAAA,MAE9F;AAAA,IACF;AACF,SAAO;AACT;AACA,SAAS,QAAQ,KAA8C;AAC7D,QAAM,IAAI,UAAU,IAAI,YAAY,CAAC;AACrC,MAAI,CAAC,EAAG,OAAM,IAAI,cAAc,gBAAgB,GAAG,cAAc,SAAS,KAAK,IAAI,CAAC,IAAI,GAAG;AAC3F,SAAO;AACT;AACA,SAAS,UAAU,KAAqB;AACtC,QAAM,EAAE,OAAO,KAAK,IAAI,QAAQ,GAAG;AACnC,SAAO,WAAW,KAAK,cAAc,IAAI;AAC3C;AACA,eAAe,UAAU,KAAqC;AAC5D,QAAM,MAAM,MAAM,MAAM,GAAG;AAC3B,MAAI,CAAC,IAAI,GAAI,QAAO;AACpB,SAAO,IAAI,KAAK;AAClB;AACA,eAAe,aAAa,KAAoE;AAC9F,QAAM,OAAO,MAAM,UAAU,GAAG,UAAU,GAAG,CAAC,gBAAgB;AAC9D,MAAI,CAAC,KAAM,QAAO,EAAE,UAAU,kBAAkB,UAAU,KAAK;AAC/D,MAAI;AACF,WAAO,EAAE,UAAU,KAAK,MAAM,IAAI,GAAkB,UAAU,MAAM;AAAA,EACtE,QAAQ;AACN,WAAO,EAAE,UAAU,kBAAkB,UAAU,KAAK;AAAA,EACtD;AACF;AACA,SAAS,YAAY,UAAuB,QAAwB;AAClE,MAAI,OAAO,SAAS,KAAK,EAAG,QAAO;AACnC,MAAI,SAAS,QAAQ,MAAM,EAAG,QAAO,SAAS,MAAM,MAAM;AAC1D,MAAI,OAAO,SAAS,GAAG,GAAG;AACxB,UAAM,CAAC,OAAO,QAAQ,IAAI,OAAO,MAAM,KAAK,CAAC;AAC7C,UAAM,IAAI,SAAS,WAAW,KAAK,IAAI,QAAQ;AAC/C,QAAI,EAAG,QAAO;AAAA,EAChB;AACA,QAAM,IAAI;AAAA,IACR,QAAQ,MAAM;AAAA,IACd;AAAA,EACF;AACF;AACA,SAAS,WAAW,MAAc,UAAuB,MAAuC;AAC9F,MAAI,MAAM;AACV,aAAW,MAAM,SAAS,gBAAgB,CAAC,GAAG;AAC5C,UAAM,QACH,KAAK,EAAE,MACP,OAAO,kBAAmB,KAAK,OAA8B,YAC7D,OAAO,cAAe,KAAK,YAAmC;AACjE,QAAI,UAAU,OAAW,OAAM,IAAI,WAAW,KAAK,EAAE,MAAM,KAAK;AAAA,EAClE;AACA,SAAO;AACT;AAEO,IAAM,UAAsB;AAAA,EACjC;AAAA,IACE,OAAO,CAAC,MAAM;AAAA,IACd,MAAM;AAAA,IACN,SAAS;AAAA,IACT,aACE;AAAA,IACF,QAAQ,CAAC,EAAE,MAAM,OAAO,MAAM,UAAU,aAAa,yFAAyF,UAAU,OAAO,MAAM,SAAS,CAAC;AAAA,IAC/K,UAAU,CAAC,EAAE,KAAK,kCAAkC,CAAC;AAAA,IACrD,KAAK,OAAO,SAAS;AACnB,YAAM,MAAM,WAAW,KAAK,GAAG;AAC/B,YAAM,EAAE,UAAU,SAAS,IAAI,MAAM,aAAa,GAAG;AACrD,aAAO;AAAA,QACL;AAAA,QACA;AAAA,QACA,OAAO,OAAO,KAAK,SAAS,SAAS,CAAC,CAAC;AAAA,QACvC,UAAU,OAAO;AAAA,UACf,OAAO,QAAQ,SAAS,YAAY,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,OAAO,KAAK,CAAC,CAAC,CAAC;AAAA,QAC7E;AAAA,QACA,OAAO,QAAQ,SAAS,KAAK;AAAA,MAC/B;AAAA,IACF;AAAA,EACF;AAAA,EACA;AAAA,IACE,OAAO,CAAC,MAAM;AAAA,IACd,MAAM;AAAA,IACN,SAAS;AAAA,IACT,aACE;AAAA,IACF,QAAQ;AAAA,MACN,EAAE,MAAM,OAAO,MAAM,UAAU,aAAa,yFAAyF,UAAU,OAAO,MAAM,SAAS;AAAA,MACrK,EAAE,MAAM,QAAQ,MAAM,UAAU,aAAa,yCAAyC,UAAU,MAAM,YAAY,KAAK;AAAA,MACvH,EAAE,MAAM,aAAa,MAAM,UAAU,aAAa,8CAA8C;AAAA,MAChG,EAAE,MAAM,QAAQ,MAAM,UAAU,aAAa,iDAAiD;AAAA,IAChG;AAAA,IACA,UAAU;AAAA,MACR,EAAE,MAAM,kBAAkB,KAAK,6CAA6C;AAAA,MAC5E,EAAE,MAAM,oBAAoB,KAAK,sEAAsE;AAAA,IACzG;AAAA,IACA,KAAK,OAAO,SAAS;AACnB,YAAM,MAAM,WAAW,KAAK,GAAG;AAC/B,YAAM,EAAE,UAAU,SAAS,IAAI,MAAM,aAAa,GAAG;AACrD,YAAM,OAAO,YAAY,UAAU,KAAK,IAAc;AACtD,YAAM,OAAO,WAAW,gBAAgB,IAAI,IAAM,MAAM,UAAU,GAAG,UAAU,GAAG,CAAC,IAAI,IAAI,EAAE,KAAM;AACnG,UAAI,SAAS,OAAW,OAAM,IAAI,cAAc,QAAQ,IAAI,+BAA+B,GAAG,MAAM,GAAG;AACvG,aAAO,EAAE,KAAK,MAAM,UAAU,SAAS,WAAW,MAAM,UAAU,IAAI,EAAE;AAAA,IAC1E;AAAA,EACF;AAAA,EACA;AAAA,IACE,OAAO,CAAC,MAAM;AAAA,IACd,MAAM;AAAA,IACN,SAAS;AAAA,IACT,aACE;AAAA,IACF,QAAQ;AAAA,MACN,EAAE,MAAM,OAAO,MAAM,UAAU,aAAa,yFAAyF,UAAU,OAAO,MAAM,SAAS;AAAA,MACrK,EAAE,MAAM,WAAW,MAAM,WAAW,aAAa,2DAA2D;AAAA,IAC9G;AAAA,IACA,UAAU,CAAC,EAAE,KAAK,mCAAmC,CAAC;AAAA,IACtD,KAAK,OAAO,SAAS;AACnB,YAAM,MAAM,WAAW,KAAK,GAAG;AAC/B,YAAM,EAAE,UAAU,SAAS,IAAI,MAAM,aAAa,GAAG;AACrD,YAAM,YAAY,SAAS,SAAS;AACpC,YAAM,UAAU,WAAW,gBAAgB,SAAS,IAAM,MAAM,UAAU,GAAG,UAAU,GAAG,CAAC,IAAI,SAAS,EAAE,KAAM;AAChH,UAAI,YAAY,OAAW,OAAM,IAAI,cAAc,2BAA2B,GAAG,MAAM,GAAG;AAC1F,aAAO,EAAE,KAAK,WAAW,UAAU,QAAQ;AAAA,IAC7C;AAAA,EACF;AACF;;;AC1KO,IAAM,mBAA+B,CAAC,WAAW,GAAG,OAAO;AAQ3D,IAAM,eAA8B;AAAA,EACzC;AAAA,IACE,OAAO,CAAC,MAAM;AAAA,IACd,SACE;AAAA,IACF,aACE;AAAA,EAWJ;AACF;","names":[]}