std/transform には3つのToolが含まれており、その3つすべてに共通する性質があります。それは capabilityを一切宣言しない ことです。どのToolの内部からも ctx.net、ctx.fs、ctx.browser、ctx.clock、ctx.random には到達できません。入力として渡したJSONだけが使えます。純粋なToolには効果がないことを証明できるため、エンジンはその結果をJournalに記録しません。リプレイ時には、そのToolを無料で再実行するだけです(標準Packを参照)。
| Tool | 目的 | 入力スロット | 出力 |
|---|---|---|---|
transform | JSONのまとまりに対してBrowser Flow式を評価する | expr: string, input?: object | 評価されたJSON値 |
jq | 小さなpath/reshapeサブセット。本物のjqではありません | filter: string, value?: object | フィルター後のJSON値 |
template | {{ name }} プレースホルダーを補間する | template: string, vars?: object | { text: string } |
packages/std/src/transform.ts で定義されています。
input、value、vars はすべて JSON envelope(z.object({}).passthrough())として型付けされています。つまり、トップレベルのJSON object であり、配列やスカラーではありません。これらのスロットに裸の配列や数値を渡すと、Tool境界でバリデーションに失敗します。トップレベル配列をreshapeする必要がある場合は、まずオブジェクトで包み({ items: [...] })、そこから参照してください。この点は、下で説明する jq で特に重要です。transform — input に対して式を評価する
transform は、flow内の他の場所で with:、when:、foreach:、output: を支えているものと同じ式パーサー/評価器(parseOrThrow / evaluate)を実行します(式言語)。ただし、expr をTool自身の run() の中で 2回目に 評価し、その際には意図的に狭められたコンテキストを使います。
steps と env は空のオブジェクトで、secret.<name> は無条件にthrowし、now/run.id は空です。実データを持つ 唯一の namespace は input.<name> です。これは、Tool自身の input スロットに渡した任意のJSONオブジェクトから束縛されます。
重要な落とし穴: 同じ構文、異なるスコープ
この2つの場所で式言語が変わるわけではありません。文法 は同一です。変わるのは、文字列が2つの異なるコンテキストで2回評価されるため、参照が何を名前解決できるか です。| レイヤー | 書く場所 | 評価するもの | 到達可能な参照 |
|---|---|---|---|
| Flowレベルのbinding | 任意の手順の with:、when:、foreach:、output: | Riderが、完全なflow EvalContext に対して評価 | input.*(flowの 宣言済み入力)、steps.<id>.output、env.<KEY>、secret.<KEY>、now、run.id |
transform 自身の expr | transform Toolの expr スロットに束縛される文字列値 | Tool自身の run() が、pureEvalContext に対して評価 | input.<name> のみ。つまり Toolの input スロット、言い換えると この 手順の with.input: に束縛したもの |
input が 同じトークン でありながら、決して同じデータではない点です。flowレベルでは input.* はflowの宣言済み入力を意味します。expr の内側では、この1つの手順のために組み立てた小さなJSONのまとまりを意味します。expr の内側には steps、env、secret、now はそもそも存在しません。fallbackも部分的な可視性もなく、存在しないかthrowします。
RiderはToolが実行される 前に with.input を解決します。これは完全なスコープを持つ、通常のFlowレベルbindingです。解決済みの値がToolの input 引数になります。その後になって初めて、transform は expr を2回目にparseして評価します。その時点では steps/env/secret/now は単に消えています。
使い方
input: は完全なflowスコープで組み立て、expr: は裸の input.* のみを使う文字列として書く。この2スロット形が、Toolの契約に一致することが保証されているパターンです。
transform の run は1行です。
expr は必須です(z.string().min(1))。input は省略すると {} がデフォルトになるため、input: bindingのない裸の transform 手順で expr: "input" を評価すると {} になります。
jq — 小さなサブセットであり、本物のjqではない
jq は本物のjqを埋め込んだもの ではありません。一般的な「レスポンスをreshapeする」ケースだけを扱う、手書きのgrammarです。それ以上のものではありません。
.keyはオブジェクトフィールドへ移動します。[N]は配列indexへ移動します。[]は配列のすべての要素に対して、pathの残りを map します。- 任意の数の
| length、| keys、| valuesstageを、前の結果に対して左から右に適用できます(.a | keys | lengthは有効です)。ただし、この3つの関数名だけが存在します。それ以外はjq: unsupported pipe "<fn>"をthrowします。
存在しないkeyや範囲外indexはnullであり、エラーにはならない
存在しないkeyや範囲外indexはnullであり、エラーにはならない
{ "does": {} } に対する .does.not.exist は、throwされるエラーではなく null を返します。navigationは undefined に当たるとすぐ停止し、そこから下は null を返します。配列の末尾を越える [5] も同じです。これは意図的に寛容な仕様です。上流のshapeが少し変わっただけで、jq 手順がRunを失敗させることはありません。非配列に対する[]はnullであり、本物のjqのerror/emptyではない
非配列に対する[]はnullであり、本物のjqのerror/emptyではない
本物のjqでは、非配列に対して
[] をmapするとエラーになります(または ? 付きなら何もyieldしません)。このサブセットは代わりに、式全体に対して null を返します。スカラーに対する [] が黙ってno-opになることを当てにしないでください。それが重要なら、上流でshapeを確認してください。keys/valuesはplain objectにだけ適用される。ただし1つ非対称性がある
keys/valuesはplain objectにだけ適用される。ただし1つ非対称性がある
| keys はplain objectでないもの(配列を含む)に対して [] を返します。そして返すkeyは挿入順ではなく sorted です。| values はオブジェクトに対してそれを反映します(keyでsort)。ただし 配列 に対しては配列を そのまま 返し、それ以外には [] を返します。つまり .list | values はno-opですが、.list | keys は [] へ潰れます。逆に覚えやすい箇所です。quoted-key構文はない。参照できるのはidentifier-styleのkeyだけ
quoted-key構文はない。参照できるのはidentifier-styleのkeyだけ
path grammarのkey tokenは
[A-Za-z_$][\w$]* です。["odd key"] / .["key-with-dash"] 形式はありません。スペース、dash、dotを含むJSON keyには、この jq ではまったく到達できません。上流のshapeを制御できるなら、keyはidentifier-safeにしてください。そうでなければ、先に transform でreshapeしてください。すべてのsegmentには先頭のdotが必要
すべてのsegmentには先頭のdotが必要
tokenは
.key、[N]、[] をscanして見つけられます。先頭dotのない裸の key(例: .items[].price ではなく items[].price)はtokenizerにmatchせず、rejectされるのではなく黙ってskipされます。path segmentは必ず . で始めてください。jq のスコープは transform よりさらに狭くなります。ここには式言語が一切なく、input.*/steps.* 参照もありません。filter は一度書くplain stringであり、対象になるデータはflowレベルで value: に束縛したJSONだけです(そこでは完全なスコープを使えます。transform の input: と同じです)。
transform(...)、http.get(...)、browser.tool(...) のような jq(...) SDK builderはありません(packages/sdk/src/index.ts はこの3つのhelperだけを定義しています)。代わりに汎用の StepDef 形を使ってください。template — {{ name }} 補間
template は、すべての {{ name }} プレースホルダーを vars[name] の文字列表現で置き換えて文字列をrenderします。
Hello , you have results. と黙ってrenderされるtemplateは、大きな音を立てて失敗するRunより悪いからです。template文字列内のすべての {{ name }} には、vars に対応するkeyがなければなりません。
解決された値の文字列化は、JSON型ごとに1つの規則に従います。
| 値の型 | render結果 |
|---|---|
string | そのものをそのまま。escapeはしません(HTML/URL/shell-safetyは下流側の責任です) |
null | literal textの null |
object / array | JSON.stringify(v) |
number / boolean | String(v) |
template(...) SDK builderもありません。理由は上の jq と同じです。汎用の StepDef 形を使ってください。関連
式言語
transform の expr(およびすべてのflowレベルbinding)が利用する、reference set、operator、組み込み関数の完全なリファレンスです。標準Pack
std/transform が他のfirst-party Packの中でどこに位置するか、そしてcapability行が「none (pure)」になっている理由です。カスタムTool
defineTool を使って、純粋な transform 形のToolを自分で作成します。この3つが実装しているものと同じ契約です。シークレット — 境界
pureEvalContext の secret() が値を返すのではなくthrowする理由です。純粋なToolは、シークレットを解決するレイヤーになってはなりません。