1. 1つのIRを使う理由
SDKとYAMLが異なる内部形状へコンパイルされると、type checker、scheduler、inspector、diff viewer、replication verifierなど、すべてのToolが2つのコードパスを必要とし、ドリフトします。正準形が1つであることは、次を意味します。1つのvalidator / scheduler / inspector
すべてのToolが同じ正準形を読むため、コードパスは1つに保たれます。
安定したidentity
caching、pinning、replicationのためのdigest。
Round-tripping
設計上のsurfaces間で往復変換が保証されます(T1)。
Forward-compatible migration
単一の
schema_version によって実現します。2. コンパイルパイプライン
Resolve
すべての
use: は、既知の ToolContract を持つ、ピン留めされた name@version に解決されます。ピン留めされていないuse(search-price、search-price@^1)は具体的なversionに解決され、正確なversionへ書き換えられるため、FlowSpecは完全にピン留めされます。Resolutionは決定論的です。browserflow.lock が存在する場合、rangeはそこに記録された正確なversionおよびcontract digestへ解決されます。存在しない場合は、active registry内の最新のcompatible versionが選ばれ、lockへ書き戻されます。これが、CIで browserflow compile --check が安定したdigestを生成する理由です。解決を決めるのはwall-clock上のregistry stateではなく、lockです。Type-check
各binding expressionは、生成元Stepのoutput schemaと、消費側Toolのinput schemaに対してチェックされます。reference cycle、unknown step、undeclared inputは、実行時ではなくここでerrorになります。
Canonicalize
§4 を参照してください。
Digest
§5 を参照してください。
compile(source, registry) → FlowSpec | Diagnostics[]。Tool contractを(場合によってはin-memoryの)Registryから読む以外のI/Oはありません。これにより、browserflow compile は高速でcache可能で、CIで安全に実行できます。
3. schema
StepSchema、InputDeclSchema、ExprSchema は 01 — Concepts で定義されています。FlowSpecはplain JSONであり、functionもclassも含まないため、serialize、diff、移動が容易です。
4. 正規化
同じことを意味する2つのFlowは、digestが一致し、replicationがdedupeできるように、byte-identicalな正準形を生成しなければなりません。Canonicalizationは、固定され文書化されたnormalizationです。| ルール | 詳細 |
|---|---|
| Key order | すべてのobject keyを辞書順(UTF-8 code unit)にsortします。 |
| Step order | Stepsは安定したtopological key (depth, id) でsortされます。順序がschedulingに影響することはありません。順序は needs / referencesから導出されるため、sortは安全であり、diffを最小化します。 |
| Whitespace | JSONは意味のないwhitespaceなしでserializeされます。encodingは1つの正準encoding(RFC 8785 JCS)です。 |
| Expressions | ASTへparseされ、normal formで再出力されます。operatorの前後にsingle spaceを置き、parenthesesはprecedence上必要な場合だけ使い、string quotingをnormalizeします。${{ a+b }} と ${{ a + b }} は同じ形に畳み込まれます。 |
| Defaults | 省略されたoptional fieldはmaterializeされません。absentはabsentを意味します。明示的に書かれたdefault valueと省略だけが異なる2つのFlowは、省略へnormalizeされます。 |
| Versions | すべての use: は正確な name@x.y.z に書き換えられます。requires は、書き換えられたusesから { ref, contract_digest } のsortedかつdedupedされたlistとして再構築されます。 |
| Input slots | YAMLの type: shorthandは完全なJSON-Schema slot({ "type": "string" })へ展開されます。secret は省略時に false がdefaultになります。 |
| Dead noops | 効果がなくdownstream referenceも持たない noop stepはelideされるため(dead-noop elimination、§6a)、名残だけのbranch armがdigestに到達することはありません。 |
| Numbers | 正準なnumber formatting(shortest round-trip decimal)を使います。 |
Canonicalizationはidempotentです。
canon(canon(x)) == canon(x)。YAML emitterは正準形からemitするため、compile → emit → compile はfixed pointになり、Gitにcheck inされたFlowは最小で安定したdiffを持ちます(DX promise、原則 T1)。5. digest
digestfield自体はhashから除外されます(値をそれ自身へhashすることはできません)。- JCS = JSON Canonicalization Scheme(RFC 8785)であり、§4 の後に適用されます。
- digestはFlowの構造およびピン留めされた
requiresを対象にします。それぞれのrequiresは単なるversion stringではなく、解決済みToolのcontract digestを持ちます。そのため、Flow digestをピン留めすることはdependency closure全体をピン留めすることになります。bodyまたはcontractが変わったToolが再公開されると、同じversion numberであっても異なるcontract digestになり、したがって異なるFlow digestになります。Flow digestはself-certifyingです。version labelも併せて信頼する必要はありません。
flow_digest です(T3)。
6. referenceからgraphへ
schedulerはproseを見ることはありません。機械的に導出されたDAGを見ます。notify も when でgateされているため、実行時にskipされることがあります。
parallelの例: a と b は互いに独立しているため並列実行できます。両方が完了すると join が実行されます。
edgeはdata referenceから来るため、dependency graphは常に正直です。先に実行されたことを保証するedgeを作らずに、誤ってStepのoutputを読むことはできません。
6a. control-flow loweringとskip propagation
branch、when、foreach が唯一のcontrol constructであり、IRが保存するのはそのうち2つだけです。Stepごとの when predicateと、Stepごとの foreach です。authoring sugarは機械的にloweringされるため、FlowSpecにはschedulerが解釈すべき特別なcontrol nodeはありません。
branch(pred, \{ yes, no \})は、その内部に含まれるStep上のwhenへloweringされます。yes:arm内の各Stepはwhen: predを得ます(既存のwhenがあればANDされます)。no:arm内の各Stepはwhen: !(pred)を得ます。FlowSpec内にbranchnodeはありません。あるのは、それぞれgatedされたStepだけです。noop(たとえば、何もしないbranchのno:arm)は、lowered graph内では実際のStepです。効果がなくかつdownstream referenceもない場合、canonicalizationによりelideされるため(dead-noop elimination、§4)、digestに到達することはありません。何かがそれをreferenceしている場合は保持されます。
when がfalseに評価されると skipped になります。skipは1つのruleによってdata edge沿いに伝播します。このruleは静的に決まり、raceによって決まることはありません。
Xがskipされた場所でsteps.X.outputをreferenceするStepは、それ自体もskippedになります。ただし、そのようなreferenceがすべてfallback(default(steps.X.output, …)またはsteps.X.output ?? …)でguardされている場合は例外で、その場合referenceはnullを返し、Stepは実行されます。outputも同じように評価されます。条件付きでskipされたStepをfallbackなしで読むprojectionはcompile timeに拒否されます(unguarded_skip、§7)。そのため、Flowのoutputが黙ってnullになることはありません。
これによりbranchはtotalになります。どのinputに対しても、すべてのStepは
succeeded、skipped、failed のちょうど1つで終了し、各downstream Stepが見るものは、どのconcurrent Stepがたまたま先にcommitしたかではなく、compilerによって固定されます。7. validationとdiagnostics
compile-time diagnosticsは正確で、source-mappedされています(YAMLではline/column、SDKではbuilder stack frame)。catalogueには次が含まれます。| Diagnostic | 発生条件 |
|---|---|
unknown_tool / unresolved_version | use: がRegistry内で解決されません。 |
type_mismatch | bindingのtypeがToolのinput schemaを満たしていません。 |
unknown_reference | x がStepではない steps.x、またはoutput schemaが持たないpathです。 |
cycle | reference graphがacyclicではなく、cycle pathが名指しされます。 |
secret_leak | secret: referenceがoperatorまたはfunctionで変換されている、secrets を宣言していないToolにbindされている、または output へ流れています。Secretはopaqueなwhole-bindingのみです(taint rule、01 §7)。 |
unguarded_skip | bindingが、条件付きでskipされるStep X の steps.X.output を default(…) / ?? fallbackなしで読んでいます(§6a)。 |
unbound_input | 必須のTool inputにbindingもdefaultもありません。 |
8. versioningとmigration
schema_version はdomain schemaとまったく同じliteral("1.0")です。IRは3つのruleのもとで進化します。
- Schemaは
.strict()なので、Riderが認識しないfieldを黙って無視することはありません。したがってforward-compatibilityはlenient parsingではなく、明示的なversion negotiationによって実現されます。Riderは自身がsupportするschema_versionrangeを宣言します。 - Additive change(新しいoptional field)はminorを上げます。
1.0 → 1.1。Riderは、同じmajor内で自身が知る最新minor以下のschema_versionを持つ任意のFlowSpecを実行します。Riderがsupportするより高いminorが刻印されたFlowSpecは、明確に拒否されます(原則 T7)。coerceされることも、partial parseされることもありません。前進する道は、より新しいRiderを実行すれば常にあります。 - Breaking changeはmajorを上げ、保存済みFlowSpecを書き換える
migrate(1.x → 2.0)codemodとともにshipされます。Migrationは明示的で、可能な場合はreversibleです。
9. binary formatではなくJSONである理由
可読性が勝ちます(原則 T5)。FlowSpecは、PRで読まれ、grepされ、いざという時には手で編集され、人間によってdiffされることを意図しています。正準形は十分compactなのでbinary encodingとの差によるcostは無視でき、debuggabilityにはbyte数をはるかに上回る価値があります。digestはreadabilityを諦めることなく、binary formatが与えてくれたはずのintegrityを与えてくれます。SDKとYAMLオーサリング
どちらもこのFlowSpecへcompile downする2つのsurfaceです。
実行と再現性
RiderがFlowSpecをRunへ変える場所です。