@warble/ir-spec 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * `docs/spec/ir-schema.md` (bundled here as `ir-schema.md`) for why this is exported without being
4
4
  * meant to be imported by a dispatcher.
5
5
  */
6
- export declare const IR_VERSION: "0.7";
6
+ export declare const IR_VERSION: "0.8";
7
7
 
8
- declare const _default: { IR_VERSION: "0.7" };
8
+ declare const _default: { IR_VERSION: "0.8" };
9
9
  export default _default;
package/index.js CHANGED
@@ -8,6 +8,6 @@
8
8
  * exported for tooling that wants the value without re-parsing `package.json` (e.g. a script
9
9
  * checking a dispatcher's declared peer range against the spec it names).
10
10
  */
11
- export const IR_VERSION = "0.7";
11
+ export const IR_VERSION = "0.8";
12
12
 
13
13
  export default { IR_VERSION };
package/ir-schema.md CHANGED
@@ -1,4 +1,4 @@
1
- # Warble IR — the compile contract (`warble_ir_version: 0.7`)
1
+ # Warble IR — the compile contract (`warble_ir_version: 0.8`)
2
2
 
3
3
  The IR is the **language-neutral seam** between the Warble front-end (`warble compile`) and any
4
4
  back-end. The v1 reference back-end is the Claude Code CLI target (`warble dispatch`, Rust); other
@@ -6,26 +6,33 @@ runtimes are other thin back-ends. Both sides depend only on this document — n
6
6
  internals.
7
7
 
8
8
  `warble compile <project-dir> -o ir.json` reads a Warble project (profile + components +
9
- context binding) and emits **one** IR JSON document with `"warble_ir_version": "0.7"` — the
9
+ context binding) and emits **one** IR JSON document with `"warble_ir_version": "0.8"` — the
10
10
  current, live contract the compiler emits today. (Earlier drafts of this doc kept the per-step-tier
11
11
  shape in a separate "v0.2 (proposed)" section; that has been folded into the contract below now
12
12
  that it is implemented and wired into the built core/dispatcher.) The shape below is what the
13
13
  dispatcher consumes.
14
14
 
15
+ > **Composition boundary:** v0.8 adds the resolved `components[].entrypoint` and optional
16
+ > `llm_calls[].component_calls` facets specified by
17
+ > [`component-composition.md`](./component-composition.md). Every shipped reader retains them. The
18
+ > Agent SDK and composed Codex local orchestrate targets realize narrow slices with dispatcher-owned
19
+ > isolated child runs and their documented runtime budgets; unsupported shapes and other targets preflight wall-hit instead of
20
+ > dropping or inlining an edge.
21
+
15
22
  > Scope note (v0.3+): context binding is **fine-grained**. The host injects a `ContextLoader`
16
23
  > selected for the binding kind, and the compiler **evaluates** every `context_precondition`
17
24
  > against that bound context — not merely validates vocabulary membership. The IR records passing
18
- > checks in `precondition_result.checks`; a Wren-project adapter also fills
19
- > `context_binding.resolved` with metrics/dimensions/grains and lineage, while a raw-source adapter
20
- > answers the constitutive probes and an external adapter emits no resolved block. A precondition
21
- > that is answerable-and-false, or that the adapter **cannot answer** at all, is a
25
+ > checks in `precondition_result.checks`; a `prepared` binding also fills
26
+ > `context_binding.resolved` with metrics/dimensions/grains and lineage, while a raw-source binding
27
+ > answers the constitutive probes and an external one emits no resolved block. A precondition
28
+ > that is answerable-and-false, or that the loader **cannot answer** at all, is a
22
29
  > loud compile-time fail — so an emitted IR only ever contains passing checks. See
23
30
  > [`context_precondition`](#context_precondition-closed-predicate-vocabulary) and the
24
31
  > [v0.3 binding](#v03--fine-grained-context-binding) section below.
25
32
  >
26
- > The coarse `context_binding.project` locator is **retained**: Wren-project back-ends use it to run
27
- > `wren`, while other binding kinds give it adapter-specific meaning. Fine-grained binding is
28
- > additive, not a replacement.
33
+ > The coarse `context_binding.project` locator is **retained**: a back-end uses it to name the layer
34
+ > it queries, and each binding kind gives it its own meaning. Fine-grained binding is additive, not a
35
+ > replacement.
29
36
 
30
37
  ---
31
38
 
@@ -41,11 +48,11 @@ version on anything else — there is no best-effort or partial parse of an unre
41
48
 
42
49
  | Consumer | Accepted `warble_ir_version` | Where the accepted version is declared |
43
50
  | --- | --- | --- |
44
- | `core` (`warble compile`) | emits `0.7` | the `"warble_ir_version"` literal in `core/src/compile.rs` |
45
- | `dispatcher/claude-code-cli` | `0.7` | `SUPPORTED_IR_VERSION` in `dispatcher/claude-code-cli/src/ir.rs` |
46
- | `dispatcher/vercel` | `0.7` | `SUPPORTED_IR_VERSION` in `dispatcher/vercel/src/emit.rs` |
47
- | `dispatcher/claude-agent-sdk` | `0.7` | `SUPPORTED_IR_VERSIONS` in `dispatcher/claude-agent-sdk/src/ir.ts` |
48
- | `dispatcher/codex-local` | `0.7` | `SUPPORTED_IR_VERSION` in `dispatcher/codex-local/src/ir.ts` |
51
+ | `core` (`warble compile`) | emits `0.8` | the `"warble_ir_version"` literal in `core/src/compile.rs` |
52
+ | `dispatcher/claude-code-cli` | `0.8` | `SUPPORTED_IR_VERSION` in `dispatcher/claude-code-cli/src/ir.rs` |
53
+ | `dispatcher/vercel` | `0.8` | `SUPPORTED_IR_VERSION` in `dispatcher/vercel/src/emit.rs` |
54
+ | `dispatcher/claude-agent-sdk` | `0.8` | `SUPPORTED_IR_VERSIONS` in `dispatcher/claude-agent-sdk/src/ir.ts` |
55
+ | `dispatcher/codex-local` | `0.8` | `SUPPORTED_IR_VERSION` in `dispatcher/codex-local/src/ir.ts` |
49
56
 
50
57
  Each back-end copies this value rather than importing it from `core` or from another back-end: a
51
58
  back-end shouldn't need a Rust dependency edge just to know a version string, and independent copies
@@ -62,7 +69,7 @@ is informational, not itself an input enforcement check.
62
69
  `@warble/claude-agent-sdk` and `@warble/codex-local` additionally each declare a `peerDependencies`
63
70
  entry on [`@warble/ir-spec`](../../packages/ir-spec) — a dedicated npm package whose own version *is*
64
71
  the IR version (see [IR version to npm version mapping](#ir-version-to-npm-version-mapping) below) —
65
- plus an advisory `"warble": { "irVersion": "0.7" }` field in the same `package.json`. This makes the
72
+ plus an advisory `"warble": { "irVersion": "0.8" }` field in the same `package.json`. This makes the
66
73
  IR version a dispatcher speaks visible in the npm dependency graph without opening the package.
67
74
  **Neither dispatcher imports `@warble/ir-spec`** — the peer is a declaration, not a dependency edge,
68
75
  and each dispatcher keeps enforcing its own copy of `SUPPORTED_IR_VERSION`(S) above. `@warble/ir-spec`
@@ -92,7 +99,7 @@ spec title, there are **eighteen** locations that must agree:
92
99
  | 7 | `dispatcher/vercel/src/emit.rs` `MAX_SUPPORTED_IR_VERSION` | Advisory | `core/tests/ir_version_lockstep_tests.rs` |
93
100
  | 8 | `dispatcher/claude-agent-sdk/src/manifest.ts` `MIN_SUPPORTED_IR_VERSION` | Advisory | `core/tests/ir_version_lockstep_tests.rs` |
94
101
  | 9 | `dispatcher/claude-agent-sdk/src/manifest.ts` `MAX_SUPPORTED_IR_VERSION` | Advisory | `core/tests/ir_version_lockstep_tests.rs` |
95
- | 10 | This document's title (`warble_ir_version: 0.7`) | Spec | `core/tests/ir_version_lockstep_tests.rs` |
102
+ | 10 | This document's title (`warble_ir_version: 0.8`) | Spec | `core/tests/ir_version_lockstep_tests.rs` |
96
103
  | 11 | `packages/ir-spec/package.json` `"version"` (mapped `x.y` -> `x.y.0`) | Producer (npm) | `core/tests/ir_version_lockstep_tests.rs` |
97
104
  | 12 | `packages/ir-spec/index.js` `IR_VERSION` | Advisory | `core/tests/ir_version_lockstep_tests.rs` |
98
105
  | 13 | `dispatcher/claude-agent-sdk/package.json` `peerDependencies["@warble/ir-spec"]` (mapped `x.y` -> `x.y.x`) | Declaration | `core/tests/ir_version_lockstep_tests.rs` |
@@ -169,7 +176,7 @@ back-end accepts, and must be regenerated rather than merely re-read.
169
176
 
170
177
  ```jsonc
171
178
  {
172
- "warble_ir_version": "0.7",
179
+ "warble_ir_version": "0.8",
173
180
  "profile": "orders-analytics", // profile.yml `profile:`
174
181
  "context_binding": { // resolved from profile `context:` + context/binding.yml
175
182
  "project": "examples/jaffle-wren", // coarse path to a wren project (retained for back-ends)
@@ -183,7 +190,7 @@ back-end accepts, and must be regenerated rather than merely re-read.
183
190
  "dimensions": [ { "name": "status", "temporal": false }, { "name": "order_date", "temporal": true } ],
184
191
  "time_dimensions": ["order_date"],
185
192
  "models": ["customers", "orders", /* … */],
186
- "lineage": { "nodes": 15, "edges": 12, "resolvable": true } // summary only; full DAG stays in the adapter
193
+ "lineage": { "nodes": 15, "edges": 12, "resolvable": true } // summary only; the full DAG stays in the bound document
187
194
  // when the project carries consumer artifacts, `lineage` additionally reports
188
195
  // "consumers": { "queries": 2, "dashboards": 1 } // query:/dashboard: node counts
189
196
  // and, when construction had to degrade (e.g. a consumer's SQL didn't parse),
@@ -256,6 +263,7 @@ narrower capability must list it explicitly alongside (or instead of) the broade
256
263
  ```jsonc
257
264
  {
258
265
  "id": "generate_dashboard",
266
+ "entrypoint": true, // resolved mount eligibility; defaults true in authoring
259
267
  "verb": "generate_dashboard",
260
268
  "type": "analytical", // analytical | assertive | mutating | constitutive | orchestrating
261
269
  "realization_kind": "skill", // required in component.yml; a profile mount may replace it
@@ -266,26 +274,16 @@ narrower capability must list it explicitly alongside (or instead of) the broade
266
274
  "context_requirements": [ // human-readable shape strings — always emitted, may be []
267
275
  "a wren project (semantic layer) to build dashboards over"
268
276
  ],
269
- "context_precondition": [ // structured predicates always emitted, may be []
270
- { "predicate": "has_metric" },
271
- { "predicate": "has_groupable_dimension" }
272
- // "args" is optional per entry, e.g. { "predicate": "has_metric", "args": { "name": "revenue" } }
273
- // predicate must be from the closed vocabulary — see below. Compile validates membership AND
274
- // evaluates each predicate against the bound context via the injected ContextLoader.
275
- ],
277
+ "context_precondition": [], // dashboard delegates panel data-shape checks to answer_query
276
278
  "params": [ // always emitted, may be []
277
- { "name": "topic_default", "bind": "optional", "default": "overview" }, // profile-bound (bind)
278
- { "name": "connection", "source": "runtime-injected" } // runtime-injected, not in git
279
+ { "name": "topic_default", "bind": "optional", "default": "overview" } // profile-bound (bind)
279
280
  ],
280
281
  "binds": { // additive; present only when >=1 bind-family param has a value
281
282
  "topic_default": "overview" // mount didn't supply one, so this is the declared default
282
283
  },
283
284
  "precondition_result": { // per-predicate evaluation outcome (v0.3, see §checks)
284
285
  "status": "pass", // always "pass" in emitted IR — a failing predicate loud-fails
285
- "checks": [ // one entry per declared context_precondition, in order
286
- { "predicate": "has_metric", "outcome": "pass" },
287
- { "predicate": "has_groupable_dimension", "outcome": "pass" }
288
- ]
286
+ "checks": [] // one entry per declared context_precondition, in order
289
287
  },
290
288
  "brief": "…shared framing for every step, placeholders substituted…", // additive; present only when authored — see below
291
289
  "slots": [ // additive; present only when the component declares slots — see below
@@ -298,10 +296,11 @@ narrower capability must list it explicitly alongside (or instead of) the broade
298
296
  "prompt_fragment": "…rendered skill instructions…", // see §prompt rendering
299
297
  "llm_calls": [ // per-step tier, order preserved from component llm_steps
300
298
  { "name": "plan_dashboard", "tier": "strong", "conditional": false, "when": null,
301
- "consumes": [], "produces": "query_plan",
299
+ "consumes": [], "produces": "dashboard_plan",
302
300
  "prompt": "<plan_dashboard.md rendered, placeholders substituted, no ## header>" },
303
301
  { "name": "compose_layout", "tier": "cheap", "conditional": false, "when": null,
304
- "consumes": ["query_plan"], "produces": "dashboard_summary",
302
+ "consumes": ["dashboard_plan"], "produces": "dashboard",
303
+ "component_calls": [{ "alias": "answer", "component": "answer_query" }],
305
304
  "prompt": "<compose_layout.md rendered>" }
306
305
  // a conditional step instead carries e.g. "conditional": true, "when": { "guard": "on_failure", "target": "generate_sql" }
307
306
  // — see `llm_calls[].when` below
@@ -311,13 +310,13 @@ narrower capability must list it explicitly alongside (or instead of) the broade
311
310
  // `llm_calls[].produces_exclusive` below
312
311
  ],
313
312
  "guardrails": [ // resolved; `locked` is the normalized lock-state
314
- { "name": "read_only_execution", "locked": true }
315
- // `scope`/`threshold` appear only when authored; their meaning is guardrail/target-specific,
316
- // e.g. another component may emit { "name": "artifact_write", "locked": true, "scope": "." }
313
+ { "name": "read_only_execution", "locked": true },
314
+ { "name": "artifact_write", "locked": true, "scope": "." }
317
315
  ],
318
316
  "trigger": { "kind": "one_shot" }, // one_shot | scheduled | event
319
- "required_capabilities": [ // union of component declarations
320
- "sql_execution:read_only", "genbi_build", "llm:strong", "llm:cheap"
317
+ "required_capabilities": [ // declarations plus shape-implied requirements
318
+ "render_contract", "artifact_write", "llm:per_step_tier", "llm:strong", "llm:cheap",
319
+ "component_invocation"
321
320
  ],
322
321
  "borrowed_actions": [],
323
322
  "eval_ref": "generate_dashboard.eval", // legacy reference string; retained for back-compat
@@ -327,7 +326,10 @@ narrower capability must list it explicitly alongside (or instead of) the broade
327
326
  },
328
327
  "effect": {
329
328
  "render_blocks": [
330
- { "type": "chart", "fields": {} }, { "type": "table", "fields": {} }, { "type": "kpi_card", "fields": {} }
329
+ { "type": "kpi_card", "fields": { "label": "string", "value": "number|string", "unit": "string?", "delta": "number?" } },
330
+ { "type": "table", "fields": { "columns": "string[]", "rows": "row[]" } },
331
+ { "type": "chart", "fields": { "chart_type": "bar|line|pie|area|scatter", "x": "string", "series": "string[]", "rows": "row[]" } },
332
+ { "type": "definition", "fields": { "sql": "string", "source_tables": "string[]", "filters": "string[]" } }
331
333
  ],
332
334
  "outcome": {
333
335
  "kind": "none" // none | assertion | mutation | dispatch — stays this 4-value union
@@ -533,6 +535,36 @@ scopes a step's own call at the back-end.
533
535
  "prompt": "…" }
534
536
  ```
535
537
 
538
+ #### `entrypoint` and `llm_calls[].component_calls` (since v0.8)
539
+
540
+ The composition contract adds two protocol fields:
541
+
542
+ ```jsonc
543
+ {
544
+ "id": "answer_query",
545
+ "entrypoint": false,
546
+ "llm_calls": [
547
+ {
548
+ "name": "compose_dashboard",
549
+ "component_calls": [
550
+ { "alias": "answer", "component": "answer_query" }
551
+ ]
552
+ }
553
+ ]
554
+ }
555
+ ```
556
+
557
+ `entrypoint` is a required resolved mount property, defaulting to `true` at the authoring boundary
558
+ for profiles written before it existed. `component_calls` is omitted when empty and otherwise is a
559
+ step-local static allowlist; its aliases resolve to unique mounted component identities after
560
+ overlays. A non-empty list also adds the required runtime-provided `component_invocation`
561
+ capability to the component and the declaring step's effective requirements.
562
+
563
+ The compiler rejects duplicate mounted identities, invalid or duplicate aliases, missing targets,
564
+ self-calls, and cycles before emitting the IR. See
565
+ [`component-composition.md`](./component-composition.md) for the authoritative validation,
566
+ preparation, runtime, envelope, and compatibility rules.
567
+
536
568
  #### `guardrails[].scope` / `threshold` and the `locked`/`overridable` normalization
537
569
 
538
570
  `scope` and `threshold` are passthrough fields — each is present in the resolved IR **only when
@@ -679,6 +711,57 @@ does not hold, the slot is removed rather than filled with any variant: an instr
679
711
  withheld capability is worse than no instruction. Evaluating it is the host's job; compile only
680
712
  carries it.
681
713
 
714
+ **Who resolves a slot.** Compile never picks a variant — every one travels in the IR — so a consumer
715
+ must substitute before a prompt is sent. Two back-ends do: the Agent SDK back-end takes the host's
716
+ table through its dispatch input, and the `warble` CLI resolves the IR document once, before any
717
+ target-specific type deserializes it, from repeated `--slot NAME=VARIANT` flags (`--slot NAME=`
718
+ removes a slot whose condition does not hold). A back-end that cannot yet resolve refuses an IR
719
+ declaring slots rather than emitting the placeholder.
720
+
721
+ **A slot nobody answers takes its `default` — with one exception.** A slot carrying `present_when`
722
+ and no answer is a loud failure, not a default: `default` covers "no opinion on the wording", and it
723
+ cannot cover "no opinion on whether this exists at all". Defaulting there is precisely the failure
724
+ the field exists to prevent.
725
+
726
+ **A consumer that records an IR hash to identify a run now records something narrower than it
727
+ looks.** Before slots, compile finished every string, so an IR hash fixed the prompt as well. Now
728
+ two runs can share one and have sent different prompts, so anything that needs to identify what a
729
+ model was told has to fingerprint the assembled prompts instead — and each back-end has to produce
730
+ it, since only dispatch knows the answer.
731
+
732
+ The contract is: **a digest per named prompt surface, plus a total over all of them**, taken as late
733
+ as possible — over the text that actually reaches the runtime, not over the plan a host may then
734
+ rebuild. The question is excluded (it is the caller's text and varies per turn by design, so
735
+ including it would make every turn unique and answer nothing). Per-turn content supply is excluded
736
+ too, and deliberately: it is not an IR construct, and when it lands it needs its own decision about
737
+ whether it joins the digest.
738
+
739
+ The surfaces, per back-end:
740
+
741
+ | Back-end | Surfaces |
742
+ | --- | --- |
743
+ | Agent SDK, from a plan | the driver's `systemPrompt` and each named subagent's prompt — what the single and split paths send as built. On the single-tier collapse path a component's `prompt_fragment` is folded into `systemPrompt` and `llm_calls[].prompt` is not read, so a one-step component's text is covered through the driver surface. |
744
+ | Agent SDK, per turn at run time | the `systemPrompt` and subagent prompts of every turn the runtime sends, reported one fingerprint per turn, plus — for a step bound to a local OpenAI-compatible endpoint — the role/content messages it posts, keyed by position and role, since such a step builds no SDK options at all. This is the only truthful source for the staged and hybrid-tool paths: a staged step's options carry a runtime preamble ahead of the step prompt, and the hybrid-tool driver composes a prompt from the step list that appears in no plan field. A plan-derived digest **must not** claim those. |
745
+ | Claude Code CLI | not yet produced — filed as a follow-up |
746
+ | vercel | not yet produced — filed as a follow-up |
747
+ | codex-local | not yet produced — filed as a follow-up |
748
+
749
+ **Take it from a plan only when the host sends that plan's options as built.** Otherwise take it at
750
+ the point of send. A host that rebuilds its options — replacing the system prompt, say — and then
751
+ records a plan-derived digest has recorded a prompt nobody received, which is worse than recording
752
+ nothing: it reads as evidence.
753
+
754
+ A back-end that grows a new prompt-carrying surface and does not add it to its digest narrows the
755
+ fingerprint silently. That is the same shape as an unresolved slot placeholder, one layer out, which
756
+ is why the surfaces are listed here rather than left implicit in each implementation.
757
+
758
+ **Display paths differ, and the two manifests differ from each other.** The rule above protects a
759
+ model; a reader is not one, so a display renders an unanswered condition's default rather than
760
+ refusing — what it shows is what the default binding would say, never a promise about what will be
761
+ sent. Whether a display needs resolving at all depends on its schema: the `warble manifest` output
762
+ omits prompt text structurally and so needs none, while the Agent SDK back-end's own manifest
763
+ carries each step's prompt and therefore resolves like any other consumer of that text.
764
+
682
765
  #### `assets` (additive since v0.7)
683
766
 
684
767
  `object[]` — **omitted entirely (not present as a key, not `[]`) unless the component declares
@@ -696,6 +779,28 @@ file's bytes and `bytes` its length; **both are computed at compile and neither
696
779
  could only rot, and silently replacing one would leave the author trusting a field that means
697
780
  nothing.
698
781
 
782
+ **An IR's assets travel beside it.** Because content is not carried (below) and no consumer can
783
+ re-read a component directory — there is none at dispatch, and a Hub component was resolved over the
784
+ network at compile — `warble compile` writes each component's asset content into a sibling directory
785
+ of the IR it emits: for `<dir>/ir.json`, into `<dir>/ir.assets/<component-id>/<authored path>`.
786
+ Nothing is created for a project that declares no assets.
787
+
788
+ Dispatch lands them at the authored relative path inside the agent's working directory and verifies
789
+ every file against its `hash`. **A manifest path is re-validated on the way in, twice.** Absolute
790
+ paths and `..` segments are refused as text; then the resolved location is checked against the
791
+ canonicalized root, so a syntactically clean path that reaches outside through a symlinked directory
792
+ is refused too — on the write side and on the read side. Compile checks what an author wrote against
793
+ the component directory, but an IR is a document that can arrive from anywhere, and the working
794
+ directory it lands in may be a real project somebody else has written to. Without both checks a
795
+ manifest is an arbitrary file write. **Both failure modes are loud**: a manifest entry with no file in the
796
+ travelling directory, and one whose content no longer hashes to the recorded value, each stop the
797
+ dispatch, and nothing is landed at all rather than part of a component's set. Silence there is the
798
+ defect this closes — a component that declared its files and received none.
799
+
800
+ The consequence to know: **the artifact that travels is the IR plus that directory.** Copying an
801
+ `ir.json` on its own and dispatching it fails loudly rather than running without the files, which is
802
+ the right direction but is a new failure to recognise.
803
+
699
804
  **Content is not carried.** This is the deliberate contrast with [`slots`](#slots-additive-since-v07)
700
805
  above, and the two together define the line: a slot variant is prompt text, so its content is read
701
806
  into the IR and composed into a prompt; an asset is a file that must be present on disk when the
@@ -870,7 +975,7 @@ Warble differentiator.
870
975
  `warble compile ./examples/demo-agent -o ir.json` against the demo project in this repo must produce an
871
976
  IR equal to `examples/demo-agent/ir.golden.json` (committed alongside, used as the core's fixture test).
872
977
  `warble compile ./examples/render-demo -o ir.json` similarly must equal
873
- `examples/render-demo/ir.golden.json`. Both goldens use the current v0.7 contract:
978
+ `examples/render-demo/ir.golden.json`. Both goldens use the current v0.8 contract:
874
979
  `context_requirements`, `context_precondition`, and `params` are always present (possibly `[]`, as
875
980
  on `dashboard`), while `eval` appears only on `generate_dashboard` and `scope: "."` appears only on
876
981
  render-demo's authored `artifact_write` guardrail.
@@ -881,8 +986,9 @@ render-demo's authored `artifact_write` guardrail.
881
986
 
882
987
  Where v0.2 carried a coarse project path and *declared* preconditions, v0.3 made the front-end
883
988
  **probe the bound context**. A host injects a `ContextLoader` (the trait lives in core, sans-IO).
884
- The same binding crate now supplies `MdlContext` for Wren projects and `RawSourceContext` for raw
885
- constitutive input; hosts may supply other adapters.
989
+ Warble ships `PreparedContext` which reads a projection the layer's own owner wrote, so any
990
+ semantic format binds without Warble speaking it — plus `RawSourceContext` for raw constitutive
991
+ input and `ExternalContext` for a layer held elsewhere; a host may supply its own loader.
886
992
 
887
993
  ## What lands in the IR
888
994
  - For a Wren project, `context_binding.resolved` carries the compiler's introspection result: `metrics`
@@ -905,13 +1011,14 @@ answerable only over a declared metric (see the `context_precondition` section a
905
1011
  source through `RawSourceContext`; an MDL-only adapter returns unanswerable for both.
906
1012
 
907
1013
  ## `blast_radius` (read path)
908
- The adapter self-builds a lineage DAG (`model → relationship / cube → metric / dimension`, plus view
909
- references), and core computes `LineageGraph::blast_radius(node)` = the transitive downstream closure
910
- + worst `Severity` (`Semantic > Structural > Compatibility > None`). This is exposed as read-only
911
- analysis on the read path, and the same query also serves as an enforcement gate for *mutating*
912
- applies. This is the one `provided_by: warble`
913
- capability see `capability-model.md` §6/§7.1, whose coarse-binding loud-fail is now lifted because
914
- fine-grained binding exists.
1014
+ The bound layer's owner builds the lineage DAG (`model → relationship / cube → metric / dimension`,
1015
+ plus view references) **and** computes each node's downstream closure with a severity **rank** on its
1016
+ own scale, supplying both in the prepared-context document. Warble computes neither: it compares a
1017
+ supplied rank against an authored ceiling and decides `allow` / `escalate` / `block`, refusing
1018
+ outright when no analysis was supplied. That gate is exposed as read-only analysis on the read path
1019
+ and as the enforcement gate for *mutating* applies. It remains the one `provided_by: warble`
1020
+ capability, on those grounds rather than on computing the closure — see `capability-model.md` §6/§7.1
1021
+ and `blast-radius.md`.
915
1022
 
916
1023
  ---
917
1024
 
@@ -985,6 +1092,15 @@ instead of trusting the prose alone:
985
1092
  Both are additive optional fields on the existing envelope/block shapes above, not a new block type;
986
1093
  a renderer or consumer that doesn't recognize them ignores them.
987
1094
 
1095
+ When a render envelope crosses a future same-profile component-call boundary, it is wrapped as the
1096
+ `kind: render` success variant and validated against the **callee's** block contract. A
1097
+ non-rendering terminal JSON value (including the current tabular
1098
+ `{columns, rows, summary, verified, definition}` shape) is wrapped as `kind: value`. The child does
1099
+ not render or persist either form; only the root invocation owns those effects. The normalized
1100
+ wrapper and failure vocabulary are specified in
1101
+ [`component-composition.md`](./component-composition.md#6-target-neutral-request-and-result-envelopes),
1102
+ and are not part of the resolved v0.8 IR.
1103
+
988
1104
  ## 3. Renderer registry — `render(target, blocks[]) → artifact`
989
1105
  Warble owns the **contract + a reference renderer (HTML)**; runtimes register/override per target.
990
1106
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warble/ir-spec",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "description": "The Warble IR (warble_ir_version) as a resolvable npm package: a version constant plus the compile-contract schema document. Not a runtime dependency — its version IS the contract.",