@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 +2 -2
- package/index.js +1 -1
- package/ir-schema.md +165 -49
- package/package.json +1 -1
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.
|
|
6
|
+
export declare const IR_VERSION: "0.8";
|
|
7
7
|
|
|
8
|
-
declare const _default: { IR_VERSION: "0.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
19
|
-
> `context_binding.resolved` with metrics/dimensions/grains and lineage, while a raw-source
|
|
20
|
-
> answers the constitutive probes and an external
|
|
21
|
-
> that is answerable-and-false, or that the
|
|
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**:
|
|
27
|
-
>
|
|
28
|
-
>
|
|
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.
|
|
45
|
-
| `dispatcher/claude-code-cli` | `0.
|
|
46
|
-
| `dispatcher/vercel` | `0.
|
|
47
|
-
| `dispatcher/claude-agent-sdk` | `0.
|
|
48
|
-
| `dispatcher/codex-local` | `0.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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": [
|
|
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" }
|
|
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": [
|
|
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": "
|
|
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": ["
|
|
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
|
-
|
|
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": [ //
|
|
320
|
-
"
|
|
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": "
|
|
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.
|
|
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
|
-
|
|
885
|
-
|
|
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
|
|
909
|
-
references)
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
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.
|
|
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.",
|