@warble/ir-spec 0.6.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/README.md +34 -0
- package/index.d.ts +9 -0
- package/index.js +13 -0
- package/ir-schema.md +820 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# `@warble/ir-spec`
|
|
2
|
+
|
|
3
|
+
The Warble IR (`warble_ir_version`) as a resolvable npm package.
|
|
4
|
+
|
|
5
|
+
**This package's version *is* the payload.** `warble_ir_version 0.6` publishes as npm version
|
|
6
|
+
`0.6.0` — IR `x.y` maps to npm `x.y.0`, with the patch component always zero. Everything else in
|
|
7
|
+
the package — the bundled [`ir-schema.md`](./ir-schema.md) (a copy of
|
|
8
|
+
[`docs/spec/ir-schema.md`](https://github.com/Canner/Warble/blob/main/docs/spec/ir-schema.md)) and
|
|
9
|
+
the `IR_VERSION` constant exported from `index.js` — is documentation, not enforcement.
|
|
10
|
+
|
|
11
|
+
## Why this package exists
|
|
12
|
+
|
|
13
|
+
Before this package, the IR version a published dispatcher (`@warble/claude-agent-sdk`,
|
|
14
|
+
`@warble/codex-local`) accepts was discoverable only by reading its source. Both dispatchers now
|
|
15
|
+
declare `@warble/ir-spec` as a `peerDependencies` range (`0.6.x` today) plus an advisory
|
|
16
|
+
`"warble": { "irVersion": "0.6" }` field, so a consumer — or npm's own resolver — can see which IR a
|
|
17
|
+
dispatcher speaks without opening it.
|
|
18
|
+
|
|
19
|
+
**This package is not meant to be imported.** A `peerDependency` is a declaration, not a dependency
|
|
20
|
+
edge: each dispatcher keeps its own independently declared `SUPPORTED_IR_VERSION` (or
|
|
21
|
+
`SUPPORTED_IR_VERSIONS`) constant rather than importing this package's `IR_VERSION`. See
|
|
22
|
+
[`ir-schema.md`](./ir-schema.md#ir-version-compatibility) for why independent copies are what make
|
|
23
|
+
the core-owned lockstep test a real check rather than a formality.
|
|
24
|
+
|
|
25
|
+
## What this package does *not* do
|
|
26
|
+
|
|
27
|
+
- It does not ship a machine-readable IR JSON Schema — `ir-schema.md` is prose. See
|
|
28
|
+
`docs/spec/ir-schema.md` in the main repository for the rationale.
|
|
29
|
+
- It does not, by itself, reject a mismatched dispatcher/IR pair at install time. A missing peer
|
|
30
|
+
(this package absent from the registry at the named version) fails installation under every
|
|
31
|
+
package manager's default configuration; a *mismatched* peer range across two npm packages is
|
|
32
|
+
enforced only once a second party (e.g. a future `@warble/cli`) also declares the peer.
|
|
33
|
+
- It is not locked to the Warble workspace/crate version (`0.4.0` and friends). It moves only when
|
|
34
|
+
the IR moves, on its own release line.
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Warble IR version this package's own npm version encodes. See `index.js` and
|
|
3
|
+
* `docs/spec/ir-schema.md` (bundled here as `ir-schema.md`) for why this is exported without being
|
|
4
|
+
* meant to be imported by a dispatcher.
|
|
5
|
+
*/
|
|
6
|
+
export declare const IR_VERSION: "0.6";
|
|
7
|
+
|
|
8
|
+
declare const _default: { IR_VERSION: "0.6" };
|
|
9
|
+
export default _default;
|
package/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Warble IR version this package's own npm version encodes.
|
|
3
|
+
*
|
|
4
|
+
* This package is not meant to be imported by a dispatcher — see `docs/spec/ir-schema.md` (bundled
|
|
5
|
+
* here as `ir-schema.md`) for why: a dispatcher keeps its own independently declared
|
|
6
|
+
* `SUPPORTED_IR_VERSION`(S) constant instead, so this package's role is to exist as a resolvable
|
|
7
|
+
* npm node whose *version* is the contract, not to be depended on at runtime. This constant is
|
|
8
|
+
* exported for tooling that wants the value without re-parsing `package.json` (e.g. a script
|
|
9
|
+
* checking a dispatcher's declared peer range against the spec it names).
|
|
10
|
+
*/
|
|
11
|
+
export const IR_VERSION = "0.6";
|
|
12
|
+
|
|
13
|
+
export default { IR_VERSION };
|
package/ir-schema.md
ADDED
|
@@ -0,0 +1,820 @@
|
|
|
1
|
+
# Warble IR — the compile contract (`warble_ir_version: 0.6`)
|
|
2
|
+
|
|
3
|
+
The IR is the **language-neutral seam** between the Warble front-end (`warble compile`) and any
|
|
4
|
+
back-end. The v1 reference back-end is the Claude Code CLI target (`warble dispatch`, Rust); other
|
|
5
|
+
runtimes are other thin back-ends. Both sides depend only on this document — not on each other's
|
|
6
|
+
internals.
|
|
7
|
+
|
|
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.6"` — the
|
|
10
|
+
current, live contract the compiler emits today. (Earlier drafts of this doc kept the per-step-tier
|
|
11
|
+
shape in a separate "v0.2 (proposed)" section; that has been folded into the contract below now
|
|
12
|
+
that it is implemented and wired into the built core/dispatcher.) The shape below is what the
|
|
13
|
+
dispatcher consumes.
|
|
14
|
+
|
|
15
|
+
> Scope note (v0.3+): context binding is **fine-grained**. The host injects a `ContextLoader`
|
|
16
|
+
> selected for the binding kind, and the compiler **evaluates** every `context_precondition`
|
|
17
|
+
> 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
|
|
22
|
+
> loud compile-time fail — so an emitted IR only ever contains passing checks. See
|
|
23
|
+
> [`context_precondition`](#context_precondition-closed-predicate-vocabulary) and the
|
|
24
|
+
> [v0.3 binding](#v03--fine-grained-context-binding) section below.
|
|
25
|
+
>
|
|
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.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
> **Umbrella model:** how the IR's declared capabilities are matched against a target runtime at
|
|
33
|
+
> dispatch (native / realize-via / degrade / fail) is defined in `capability-model.md`. The v0.3
|
|
34
|
+
> section below is a specific capability resolved under that model.
|
|
35
|
+
|
|
36
|
+
## IR version compatibility
|
|
37
|
+
|
|
38
|
+
`warble_ir_version` is a closed, exact-match contract, not a semver range: every back-end accepts
|
|
39
|
+
**only** the version(s) listed below and loud-fails naming both the rejected and the supported
|
|
40
|
+
version on anything else — there is no best-effort or partial parse of an unrecognized version.
|
|
41
|
+
|
|
42
|
+
| Consumer | Accepted `warble_ir_version` | Where the accepted version is declared |
|
|
43
|
+
| --- | --- | --- |
|
|
44
|
+
| `core` (`warble compile`) | emits `0.6` | the `"warble_ir_version"` literal in `core/src/compile.rs` |
|
|
45
|
+
| `dispatcher/claude-code-cli` | `0.6` | `SUPPORTED_IR_VERSION` in `dispatcher/claude-code-cli/src/ir.rs` |
|
|
46
|
+
| `dispatcher/vercel` | `0.6` | `SUPPORTED_IR_VERSION` in `dispatcher/vercel/src/emit.rs` |
|
|
47
|
+
| `dispatcher/claude-agent-sdk` | `0.6` | `SUPPORTED_IR_VERSIONS` in `dispatcher/claude-agent-sdk/src/ir.ts` |
|
|
48
|
+
| `dispatcher/codex-local` | `0.6` | `SUPPORTED_IR_VERSION` in `dispatcher/codex-local/src/ir.ts` |
|
|
49
|
+
|
|
50
|
+
Each back-end copies this value rather than importing it from `core` or from another back-end: a
|
|
51
|
+
back-end shouldn't need a Rust dependency edge just to know a version string, and independent copies
|
|
52
|
+
are what make the core-owned lockstep test below a meaningful check rather than a formality. (This is in the
|
|
53
|
+
same spirit as invariant 2 (zero-wren) in
|
|
54
|
+
[`CONTRIBUTING.md`](../../CONTRIBUTING.md#invariants--preserve-these), though that invariant itself
|
|
55
|
+
only constrains what `core` and the components may depend on, not what depends on them.) Three of
|
|
56
|
+
the four back-ends (`vercel`, `claude-agent-sdk`, `codex-local`) also stamp the version onto emitted
|
|
57
|
+
artifacts as advisory `min`/`max` metadata — for example, the `vercel` bundle's own
|
|
58
|
+
`compat.min_ir_version` / `compat.max_ir_version`. The Codex manifest derives that advisory pair
|
|
59
|
+
directly from its enforcement constant instead of copying the value again. Artifact compatibility
|
|
60
|
+
is informational, not itself an input enforcement check.
|
|
61
|
+
|
|
62
|
+
`@warble/claude-agent-sdk` and `@warble/codex-local` additionally each declare a `peerDependencies`
|
|
63
|
+
entry on [`@warble/ir-spec`](../../packages/ir-spec) — a dedicated npm package whose own version *is*
|
|
64
|
+
the IR version (see [IR version to npm version mapping](#ir-version-to-npm-version-mapping) below) —
|
|
65
|
+
plus an advisory `"warble": { "irVersion": "0.6" }` field in the same `package.json`. This makes the
|
|
66
|
+
IR version a dispatcher speaks visible in the npm dependency graph without opening the package.
|
|
67
|
+
**Neither dispatcher imports `@warble/ir-spec`** — the peer is a declaration, not a dependency edge,
|
|
68
|
+
and each dispatcher keeps enforcing its own copy of `SUPPORTED_IR_VERSION`(S) above. `@warble/ir-spec`
|
|
69
|
+
also bundles this document itself (as `ir-schema.md`, alongside `index.js`/`index.d.ts`) as a frozen
|
|
70
|
+
snapshot for the IR version it publishes — a published npm version is immutable, so a snapshot as of
|
|
71
|
+
that version is worth more than a live link back to this file on `main`, which points at whatever the
|
|
72
|
+
spec later became. `just publish-check` fails if `packages/ir-spec/ir-schema.md` and this document
|
|
73
|
+
ever diverge, since a publish is exactly the point that drift becomes irreversible; re-sync it with
|
|
74
|
+
`cp docs/spec/ir-schema.md packages/ir-spec/ir-schema.md` whenever this document changes. Counting the
|
|
75
|
+
producer (what `core` actually emits) alongside every independent consumer/advisory copy, the
|
|
76
|
+
`@warble/ir-spec` package's own version, its `index.js` and `index.d.ts` version constants/literals,
|
|
77
|
+
both dispatchers' peer declarations, both dispatchers' advisory `warble.irVersion` fields, and the
|
|
78
|
+
spec title, there are **eighteen** locations that must agree:
|
|
79
|
+
|
|
80
|
+
| # | Location | Kind | Checked by |
|
|
81
|
+
| --- | --- | --- | --- |
|
|
82
|
+
| 1 | `core/src/compile.rs` — the `"warble_ir_version"` literal it emits | Producer | `core/tests/ir_version_lockstep_tests.rs` |
|
|
83
|
+
| 2 | `dispatcher/claude-code-cli/src/ir.rs` `SUPPORTED_IR_VERSION` | Enforcement | `core/tests/ir_version_lockstep_tests.rs` |
|
|
84
|
+
| 3 | `dispatcher/vercel/src/emit.rs` `SUPPORTED_IR_VERSION` | Enforcement | `core/tests/ir_version_lockstep_tests.rs` |
|
|
85
|
+
| 4 | `dispatcher/claude-agent-sdk/src/ir.ts` `SUPPORTED_IR_VERSIONS` | Enforcement | `core/tests/ir_version_lockstep_tests.rs` |
|
|
86
|
+
| 5 | `dispatcher/codex-local/src/ir.ts` `SUPPORTED_IR_VERSION` | Enforcement + manifest advisory source | `core/tests/ir_version_lockstep_tests.rs` |
|
|
87
|
+
| 6 | `dispatcher/vercel/src/emit.rs` `MIN_SUPPORTED_IR_VERSION` | Advisory | `core/tests/ir_version_lockstep_tests.rs` |
|
|
88
|
+
| 7 | `dispatcher/vercel/src/emit.rs` `MAX_SUPPORTED_IR_VERSION` | Advisory | `core/tests/ir_version_lockstep_tests.rs` |
|
|
89
|
+
| 8 | `dispatcher/claude-agent-sdk/src/manifest.ts` `MIN_SUPPORTED_IR_VERSION` | Advisory | `core/tests/ir_version_lockstep_tests.rs` |
|
|
90
|
+
| 9 | `dispatcher/claude-agent-sdk/src/manifest.ts` `MAX_SUPPORTED_IR_VERSION` | Advisory | `core/tests/ir_version_lockstep_tests.rs` |
|
|
91
|
+
| 10 | This document's title (`warble_ir_version: 0.6`) | Spec | `core/tests/ir_version_lockstep_tests.rs` |
|
|
92
|
+
| 11 | `packages/ir-spec/package.json` `"version"` (mapped `x.y` -> `x.y.0`) | Producer (npm) | `core/tests/ir_version_lockstep_tests.rs` |
|
|
93
|
+
| 12 | `packages/ir-spec/index.js` `IR_VERSION` | Advisory | `core/tests/ir_version_lockstep_tests.rs` |
|
|
94
|
+
| 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` |
|
|
95
|
+
| 14 | `dispatcher/claude-agent-sdk/package.json` `warble.irVersion` | Advisory | `core/tests/ir_version_lockstep_tests.rs` |
|
|
96
|
+
| 15 | `dispatcher/codex-local/package.json` `peerDependencies["@warble/ir-spec"]` (mapped `x.y` -> `x.y.x`) | Declaration | `core/tests/ir_version_lockstep_tests.rs` |
|
|
97
|
+
| 16 | `dispatcher/codex-local/package.json` `warble.irVersion` | Advisory | `core/tests/ir_version_lockstep_tests.rs` |
|
|
98
|
+
| 17 | `packages/ir-spec/index.d.ts` `export declare const IR_VERSION` type literal | Advisory (type) | `core/tests/ir_version_lockstep_tests.rs` |
|
|
99
|
+
| 18 | `packages/ir-spec/index.d.ts` default-export `IR_VERSION` type literal | Advisory (type) | `core/tests/ir_version_lockstep_tests.rs` |
|
|
100
|
+
|
|
101
|
+
This table's scope is contract-bearing declarations — constants and literals something actually
|
|
102
|
+
compares against — not every place `warble_ir_version` appears in prose. Each back-end's `ir` module
|
|
103
|
+
doc comment also mentions the current version for a human skimming the file (e.g. `//! Typed view of
|
|
104
|
+
the Warble IR (warble_ir_version: 0.6)`); nothing checks those comments, and a version bump can leave
|
|
105
|
+
them stale without breaking anything. They are deliberately not extra rows — update them as a
|
|
106
|
+
courtesy to the reader, not because a test requires it.
|
|
107
|
+
|
|
108
|
+
`core/tests/ir_version_lockstep_tests.rs` is the sole cross-target lockstep owner: it text-parses
|
|
109
|
+
every contract-bearing declaration above and asserts that each equals the version core emits (rows
|
|
110
|
+
11–15 first pass the emitted version through the mapping below, since they are npm identifiers, not
|
|
111
|
+
copies of the raw `x.y` string). The target-local `ir_version_tests.rs` files only exercise their own
|
|
112
|
+
unsupported-version rejection and no-partial-output behavior; they do not scrape other targets'
|
|
113
|
+
sources. (`core/src/lib.rs`'s doctest and `core/tests/compile_tests.rs` also assert row 1's literal
|
|
114
|
+
directly, but aren't listed as separate lockstep-tested locations — they self-guard, failing the
|
|
115
|
+
moment `compile.rs` changes without a matching update there.) When `warble_ir_version` changes,
|
|
116
|
+
update all eighteen rows in the same change.
|
|
117
|
+
|
|
118
|
+
### IR version to npm version mapping
|
|
119
|
+
|
|
120
|
+
`@warble/ir-spec` is **not** version-locked to the Cargo workspace version (`0.4.0` and friends,
|
|
121
|
+
tracked separately — see [`RELEASING.md`](../../RELEASING.md#ir-version-vs-crate-version)); it moves
|
|
122
|
+
only when `warble_ir_version` moves, on its own release line. The mapping from IR version to npm
|
|
123
|
+
version is fixed and mechanical:
|
|
124
|
+
|
|
125
|
+
- IR version `x.y` maps to npm version `x.y.0` — the patch component is **always** zero.
|
|
126
|
+
- A dispatcher's `peerDependencies["@warble/ir-spec"]` range is `x.y.x` — e.g. IR `0.6` is npm
|
|
127
|
+
version `0.6.0` and peer range `0.6.x`.
|
|
128
|
+
- An IR version with anything other than exactly two dot-separated numeric components (a three-part
|
|
129
|
+
`x.y.z`, or a non-numeric component) has **no defined mapping** and is rejected by
|
|
130
|
+
`core/tests/ir_version_lockstep_tests.rs` at the point it tries to compute rows 11, 13, and 15 — it
|
|
131
|
+
panics rather than guessing a truncated or extended version.
|
|
132
|
+
|
|
133
|
+
This mapping is what a future releaser bumping `warble_ir_version` needs to reproduce by hand when
|
|
134
|
+
publishing the next `@warble/ir-spec` version (row 11) and updating both dispatchers' peer ranges
|
|
135
|
+
(rows 13 and 15) — see the bump procedure in [`RELEASING.md`](../../RELEASING.md).
|
|
136
|
+
|
|
137
|
+
### When `warble_ir_version` must change
|
|
138
|
+
|
|
139
|
+
Any change to the IR shape requires a version bump — **including a purely additive one** (a new
|
|
140
|
+
optional field, a new enum arm nothing yet emits, a key that quietly defaults when absent). This is
|
|
141
|
+
not a formality; it is the precondition for the exact-match policy above to mean anything.
|
|
142
|
+
|
|
143
|
+
An additive field shipped without a version bump is invisible to enforcement. A back-end built
|
|
144
|
+
against the previous shape keeps declaring the same `warble_ir_version`, so the version check passes
|
|
145
|
+
and the back-end never even notices a new field exists; an unrecognized key is dropped during
|
|
146
|
+
deserialization and the consumer behaves exactly as it did before. Nothing fails, loud or otherwise,
|
|
147
|
+
and there is no signal anywhere that the wire contract moved. The version bump *is* the mechanism
|
|
148
|
+
that turns "the IR grew" into an observable event. Skip it, and additive growth becomes undetectable
|
|
149
|
+
growth — the exact-match check has nothing left to compare against, because both sides still agree on
|
|
150
|
+
a version number that no longer describes the same contract.
|
|
151
|
+
|
|
152
|
+
Bumping unconditionally also keeps a retreat path open. Widening the accepted-version policy later —
|
|
153
|
+
for example, moving from a single exact version to an accepted range — is a non-breaking relaxation
|
|
154
|
+
of what is enforced today. Starting from a wide range and later narrowing it is a breaking change for
|
|
155
|
+
whoever came to depend on the wider behavior in the meantime. Exact-match is the strictest available
|
|
156
|
+
starting point, so keeping it strict now is what preserves the option to loosen it later without
|
|
157
|
+
having already given up the ability to say no.
|
|
158
|
+
|
|
159
|
+
None of this is free: a bump touches all eighteen places above — held together by the core-owned Rust
|
|
160
|
+
lockstep test — *and* it invalidates any artifact a consumer has already stored from a previous IR version — a
|
|
161
|
+
committed bundle or compiled snapshot built against the old version now names a version no current
|
|
162
|
+
back-end accepts, and must be regenerated rather than merely re-read.
|
|
163
|
+
|
|
164
|
+
## Top-level shape
|
|
165
|
+
|
|
166
|
+
```jsonc
|
|
167
|
+
{
|
|
168
|
+
"warble_ir_version": "0.6",
|
|
169
|
+
"profile": "orders-analytics", // profile.yml `profile:`
|
|
170
|
+
"context_binding": { // resolved from profile `context:` + context/binding.yml
|
|
171
|
+
"project": "examples/jaffle-wren", // coarse path to a wren project (retained for back-ends)
|
|
172
|
+
"binding_mode": "runtime_selected",
|
|
173
|
+
"resolved": { // v0.3 fine-grained binding — what the ContextLoader learned
|
|
174
|
+
"metrics": [ // declared cube measures + implicit numeric columns
|
|
175
|
+
{ "name": "total_revenue", "declared": true, "additivity": "additive" },
|
|
176
|
+
{ "name": "avg_order_value", "declared": true, "additivity": "non_additive" },
|
|
177
|
+
{ "name": "amount", "declared": false } // implicit column: additivity not expressible
|
|
178
|
+
],
|
|
179
|
+
"dimensions": [ { "name": "status", "temporal": false }, { "name": "order_date", "temporal": true } ],
|
|
180
|
+
"time_dimensions": ["order_date"],
|
|
181
|
+
"models": ["customers", "orders", /* … */],
|
|
182
|
+
"lineage": { "nodes": 15, "edges": 12, "resolvable": true } // summary only; full DAG stays in the adapter
|
|
183
|
+
// when the project carries consumer artifacts, `lineage` additionally reports
|
|
184
|
+
// "consumers": { "queries": 2, "dashboards": 1 } // query:/dashboard: node counts
|
|
185
|
+
// and, when construction had to degrade (e.g. a consumer's SQL didn't parse),
|
|
186
|
+
// "diagnostics": ["query:broken: statement did not parse as SQL; …"] // no silent caps
|
|
187
|
+
// — both keys are ABSENT (not empty) on a project without consumers/degradations,
|
|
188
|
+
// so pre-consumer IRs are byte-identical.
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
"config": {}, // reserved profile-level config block; no fields today
|
|
192
|
+
"components": [ /* one resolved component node, see below */ ]
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### `config` — emptied in 0.6
|
|
197
|
+
|
|
198
|
+
`config` carried one field, `tier_policy`, from the first IR through `0.5`. `0.6` removes it, and
|
|
199
|
+
the block is now emitted as `{}`.
|
|
200
|
+
|
|
201
|
+
`tier_policy` was a profile-wide tier stance (`cost_sensitive`) that the compiler was meant to
|
|
202
|
+
resolve into per-step tiers. Only the field ever landed. No back-end read it, its value was never
|
|
203
|
+
validated against any vocabulary, and compiling the same profile with `cost_sensitive`, `null`, or
|
|
204
|
+
an invented string produced byte-identical dispatch output — so a profile declaring it advertised
|
|
205
|
+
cost control it did not have.
|
|
206
|
+
|
|
207
|
+
It was removed rather than implemented because the rule it needs does not exist and the obvious
|
|
208
|
+
rule is measurably wrong: eval puts a blanket downgrade of `answer_query` at no accuracy cost and
|
|
209
|
+
~3× cheaper on a clean schema, and at 0.93 → 0.60 execution accuracy on a messy one. Which steps
|
|
210
|
+
are safe to downgrade is a property of the **bound context**, not of the profile, so a static
|
|
211
|
+
profile-level string cannot express it. Per-step `tier_overrides` on a mount remains the honest
|
|
212
|
+
control (see [`profile-schema.md`](./authoring.md#61-tiers-not-model-names)).
|
|
213
|
+
|
|
214
|
+
The block itself stays so that profile-level config which *can* be honored is an additive change
|
|
215
|
+
rather than the reintroduction of a removed key.
|
|
216
|
+
|
|
217
|
+
## Component node (resolved: component fields ⊕ supported profile mount fields)
|
|
218
|
+
|
|
219
|
+
```jsonc
|
|
220
|
+
{
|
|
221
|
+
"id": "generate_dashboard",
|
|
222
|
+
"verb": "generate_dashboard",
|
|
223
|
+
"type": "analytical", // analytical | assertive | mutating | constitutive | orchestrating
|
|
224
|
+
"realization_kind": "skill", // required in component.yml; a profile mount may replace it
|
|
225
|
+
"context_binding": { // per-node; POC = same coarse project as top-level
|
|
226
|
+
"project": "examples/jaffle-wren",
|
|
227
|
+
"binding_mode": "runtime_selected"
|
|
228
|
+
},
|
|
229
|
+
"context_requirements": [ // human-readable shape strings — always emitted, may be []
|
|
230
|
+
"a wren project (semantic layer) to build dashboards over"
|
|
231
|
+
],
|
|
232
|
+
"context_precondition": [ // structured predicates — always emitted, may be []
|
|
233
|
+
{ "predicate": "has_metric" },
|
|
234
|
+
{ "predicate": "has_groupable_dimension" }
|
|
235
|
+
// "args" is optional per entry, e.g. { "predicate": "has_metric", "args": { "name": "revenue" } }
|
|
236
|
+
// predicate must be from the closed vocabulary — see below. Compile validates membership AND
|
|
237
|
+
// evaluates each predicate against the bound context via the injected ContextLoader.
|
|
238
|
+
],
|
|
239
|
+
"params": [ // always emitted, may be []
|
|
240
|
+
{ "name": "topic_default", "bind": "optional", "default": "overview" }, // profile-bound (bind)
|
|
241
|
+
{ "name": "connection", "source": "runtime-injected" } // runtime-injected, not in git
|
|
242
|
+
],
|
|
243
|
+
"binds": { // additive; present only when >=1 bind-family param has a value
|
|
244
|
+
"topic_default": "overview" // mount didn't supply one, so this is the declared default
|
|
245
|
+
},
|
|
246
|
+
"precondition_result": { // per-predicate evaluation outcome (v0.3, see §checks)
|
|
247
|
+
"status": "pass", // always "pass" in emitted IR — a failing predicate loud-fails
|
|
248
|
+
"checks": [ // one entry per declared context_precondition, in order
|
|
249
|
+
{ "predicate": "has_metric", "outcome": "pass" },
|
|
250
|
+
{ "predicate": "has_groupable_dimension", "outcome": "pass" }
|
|
251
|
+
]
|
|
252
|
+
},
|
|
253
|
+
"brief": "…shared framing for every step, placeholders substituted…", // additive; present only when authored — see below
|
|
254
|
+
"prompt_fragment": "…rendered skill instructions…", // see §prompt rendering
|
|
255
|
+
"llm_calls": [ // per-step tier, order preserved from component llm_steps
|
|
256
|
+
{ "name": "plan_dashboard", "tier": "strong", "conditional": false, "when": null,
|
|
257
|
+
"consumes": [], "produces": "query_plan",
|
|
258
|
+
"prompt": "<plan_dashboard.md rendered, placeholders substituted, no ## header>" },
|
|
259
|
+
{ "name": "compose_layout", "tier": "cheap", "conditional": false, "when": null,
|
|
260
|
+
"consumes": ["query_plan"], "produces": "dashboard_summary",
|
|
261
|
+
"prompt": "<compose_layout.md rendered>" }
|
|
262
|
+
// a conditional step instead carries e.g. "conditional": true, "when": { "guard": "on_failure", "target": "generate_sql" }
|
|
263
|
+
// — see `llm_calls[].when` below
|
|
264
|
+
],
|
|
265
|
+
"guardrails": [ // resolved; `locked` is the normalized lock-state
|
|
266
|
+
{ "name": "read_only_execution", "locked": true }
|
|
267
|
+
// `scope`/`threshold` appear only when authored; their meaning is guardrail/target-specific,
|
|
268
|
+
// e.g. another component may emit { "name": "artifact_write", "locked": true, "scope": "." }
|
|
269
|
+
],
|
|
270
|
+
"trigger": { "kind": "one_shot" }, // one_shot | scheduled | event
|
|
271
|
+
"required_capabilities": [ // union of component declarations
|
|
272
|
+
"sql_execution:read_only", "genbi_build", "llm:strong", "llm:cheap"
|
|
273
|
+
],
|
|
274
|
+
"borrowed_actions": [],
|
|
275
|
+
"eval_ref": "generate_dashboard.eval", // legacy reference string; retained for back-compat
|
|
276
|
+
"eval": { // structured form; present only when authored
|
|
277
|
+
"template_ref": "eval/",
|
|
278
|
+
"metrics": ["answer_relevance", "chart_appropriateness"]
|
|
279
|
+
},
|
|
280
|
+
"effect": {
|
|
281
|
+
"render_blocks": [
|
|
282
|
+
{ "type": "chart", "fields": {} }, { "type": "table", "fields": {} }, { "type": "kpi_card", "fields": {} }
|
|
283
|
+
],
|
|
284
|
+
"outcome": {
|
|
285
|
+
"kind": "none" // none | assertion | mutation | dispatch — stays this 4-value union
|
|
286
|
+
// optional facets below are parsed today but not yet consumed by the MVP (analytical) back-ends
|
|
287
|
+
// — forward-declared, not silently dropped. See §effect.outcome facets.
|
|
288
|
+
// "verdict_type", "emits" (assertive)
|
|
289
|
+
// "target", "change_type" (mutating)
|
|
290
|
+
// "routable_scope" (orchestrating)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### New/expanded fields, one by one
|
|
297
|
+
|
|
298
|
+
#### `context_requirements`
|
|
299
|
+
|
|
300
|
+
An array of human-readable shape strings — what "shape" of context this component needs, in
|
|
301
|
+
prose (not a binding). **Always emitted, may be `[]`** (as it is on the `dashboard` component in
|
|
302
|
+
`examples/render-demo/ir.golden.json`, which declares no requirements). Not machine-checked today; it exists
|
|
303
|
+
for discoverability (Hub listings, docs) alongside the machine-checked `context_precondition`.
|
|
304
|
+
|
|
305
|
+
#### `context_precondition` (closed predicate vocabulary)
|
|
306
|
+
|
|
307
|
+
An array of structured predicates, each `{ "predicate": <name>, "args"?: {…} }`. **Always emitted,
|
|
308
|
+
may be `[]`.** `predicate` must be one of exactly eleven names:
|
|
309
|
+
|
|
310
|
+
| Predicate |
|
|
311
|
+
| --- |
|
|
312
|
+
| `mdl_parseable` |
|
|
313
|
+
| `has_metric` |
|
|
314
|
+
| `has_queryable_dimension` |
|
|
315
|
+
| `has_time_dimension` |
|
|
316
|
+
| `has_groupable_dimension` |
|
|
317
|
+
| `metric_additive` |
|
|
318
|
+
| `model_has_timestamp` |
|
|
319
|
+
| `lineage_resolvable` |
|
|
320
|
+
| `wren_project_exists` |
|
|
321
|
+
| `source_introspectable` |
|
|
322
|
+
| `raw_docs_readable` |
|
|
323
|
+
|
|
324
|
+
`args` is optional per entry (predicate-specific, e.g. a metric/dimension name to check). An arg
|
|
325
|
+
value may be the literal to check, or a **bind reference** `"$param:<name>"` naming one of the
|
|
326
|
+
component's own `params[]` entries — compile substitutes it with that param's *effective* value
|
|
327
|
+
(the profile mount's supplied bind, or else the param's declared `default`; see
|
|
328
|
+
[`params`](#params) and [Resolution rules](#resolution-rules)) before evaluating the predicate.
|
|
329
|
+
`$param:<name>` naming a param the component doesn't declare is a loud compile-time fail (an
|
|
330
|
+
authoring bug, caught immediately rather than surfacing later as a confusing "unanswerable").
|
|
331
|
+
A declared param with no effective value (an unsupplied `bind: optional` param with no `default`)
|
|
332
|
+
makes the referencing precondition **unanswerable** — the same refuse-rather-than-guess outcome as
|
|
333
|
+
an unanswerable predicate, not a silent skip. The IR always carries the **resolved** value in
|
|
334
|
+
`context_precondition[].args`, never the unresolved `"$param:<name>"` template — a back-end reading
|
|
335
|
+
the IR never needs to know binding happened. Compile validates vocabulary membership (an unknown
|
|
336
|
+
predicate name is a loud fail) **and evaluates each predicate against the bound context** through
|
|
337
|
+
the injected `ContextLoader`. Evaluation has three outcomes:
|
|
338
|
+
|
|
339
|
+
- **pass** — the predicate holds; recorded in `precondition_result.checks`.
|
|
340
|
+
- **fail (answerable-and-false)** — the predicate is answerable but does not hold → loud compile
|
|
341
|
+
fail (`… not satisfied by the bound semantic layer`).
|
|
342
|
+
- **unanswerable** — the active adapter cannot express the answer → a *different* loud fail
|
|
343
|
+
(`… cannot be evaluated …`), never a silent false. `metric_additive` is unanswerable without a
|
|
344
|
+
declared metric; a pinned `model_has_timestamp` is unanswerable when its model is undeclared; and
|
|
345
|
+
the two raw-shape predicates are unanswerable on adapters that cannot probe raw input.
|
|
346
|
+
|
|
347
|
+
`metric_additive` is the one semantic predicate. **Existential by default** (no `args`): it passes
|
|
348
|
+
iff the layer declares at least one additive metric, fails if declared metrics exist but none are
|
|
349
|
+
additive, and is unanswerable if no declared metric exists at all. **Pinned** (`args: { metric:
|
|
350
|
+
<name> }`): the named metric must be a declared measure — additive → pass, non-additive → fail, not
|
|
351
|
+
declared → unanswerable. The per-metric decision a general component needs at *run* time (which
|
|
352
|
+
metric did the user pick?) stays a runtime guard; compile time proves a valid target exists and that
|
|
353
|
+
additivity is decidable in this Context.
|
|
354
|
+
|
|
355
|
+
`model_has_timestamp` follows the same existential/pinned shape. **Existential by default** (no
|
|
356
|
+
`args`): passes iff *any* declared model has a timestamp column, never unanswerable (a project with
|
|
357
|
+
zero models still has a well-defined — false — answer). **Pinned** (`args: { model: <name> }`,
|
|
358
|
+
typically via `$param:` against a `bind`-family param naming a model): the named model must be
|
|
359
|
+
declared — has a timestamp → pass, no timestamp → fail, not a declared model at all →
|
|
360
|
+
unanswerable. This is what makes `binding_mode: pinned` meaningful for a component like
|
|
361
|
+
`monitor_freshness`: binding the component to a specific, timestampless model is caught at compile
|
|
362
|
+
time instead of failing confusingly at run time.
|
|
363
|
+
|
|
364
|
+
`source_introspectable` and `raw_docs_readable` are the constitutive raw-shape predicates. A
|
|
365
|
+
`RawSourceContext` answers them with `Some(true)` (pass) or `Some(false)` (answerable fail);
|
|
366
|
+
MDL-only and external adapters return `None` (unanswerable), which is a loud compile failure rather
|
|
367
|
+
than a guessed false. They are used with a `kind: raw_source` binding before an MDL exists.
|
|
368
|
+
|
|
369
|
+
#### `params`
|
|
370
|
+
|
|
371
|
+
An array, **always emitted, may be `[]`.** Each entry is exactly one of two shapes:
|
|
372
|
+
|
|
373
|
+
- **Profile-bound**: `{ "name", "bind": "required" | "optional", "default"? }` — supplied (or
|
|
374
|
+
defaulted) by the profile at compile time; see [Resolution rules](#resolution-rules).
|
|
375
|
+
- **Runtime-injected**: `{ "name", "source": "runtime-injected" }` — supplied by the runtime at
|
|
376
|
+
dispatch/run time, never committed to git (e.g. a database `connection`, or `model_binding`,
|
|
377
|
+
the tier→concrete-model binding).
|
|
378
|
+
|
|
379
|
+
An entry must declare **exactly one** of `bind` or `source` — declaring both, or neither, is a loud
|
|
380
|
+
compile-time fail. The only accepted `source` value today is `"runtime-injected"`; any other value
|
|
381
|
+
is also a loud compile-time fail.
|
|
382
|
+
|
|
383
|
+
#### `binds`
|
|
384
|
+
|
|
385
|
+
An object, **additive: present only when the component has at least one `bind`-family param with
|
|
386
|
+
an effective value; omitted entirely otherwise** (no empty `{}`). Keys are param names; values are
|
|
387
|
+
each param's *effective* value — the profile mount's supplied bind, or else the param's declared
|
|
388
|
+
`default` when the mount didn't supply one. `source: runtime-injected` params are never included
|
|
389
|
+
(their value doesn't exist until dispatch/run time). This is the one place in the IR a back-end (or
|
|
390
|
+
a human reading `ir.json`) can see, without cross-referencing `profile.yml`, exactly what a
|
|
391
|
+
component was bound to at compile time — the same map compile itself uses to resolve `$param:`
|
|
392
|
+
references in `context_precondition[].args` (see above), so the two are always consistent with each
|
|
393
|
+
other by construction.
|
|
394
|
+
|
|
395
|
+
```jsonc
|
|
396
|
+
"params": [
|
|
397
|
+
{ "name": "model", "bind": "required" },
|
|
398
|
+
{ "name": "expected_cadence", "bind": "optional", "default": "24h" }
|
|
399
|
+
],
|
|
400
|
+
"binds": { "model": "orders", "expected_cadence": "24h" }
|
|
401
|
+
// "model" came from the profile mount's `bind:`; "expected_cadence" fell back to its default
|
|
402
|
+
// because the mount didn't supply one.
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
#### `llm_calls[].conditional`
|
|
406
|
+
|
|
407
|
+
A boolean, **always emitted, defaults to `false`.** It marks a step that only runs sometimes; *why*
|
|
408
|
+
it's conditional is carried separately in `when` (below), not in this flag. The composition layer
|
|
409
|
+
stays declarative: `conditional`/`when` name a closed-vocabulary guard, never a condition
|
|
410
|
+
expression — the actual mechanics of what to do (retry, skip, escalate) live inside the step's own
|
|
411
|
+
hook/prompt, never in the profile/composition layer. This keeps invariant #3 (the composition layer
|
|
412
|
+
never grows a data-flow DSL) intact.
|
|
413
|
+
|
|
414
|
+
#### `llm_calls[].when` (closed guard vocabulary, additive since v0.3)
|
|
415
|
+
|
|
416
|
+
`{ "guard": <name>, "target": <string> }`, or `null` — **always emitted as a key** (present with a
|
|
417
|
+
`null` value when the step isn't conditional, mirroring `produces`'s always-present-key style; never
|
|
418
|
+
omitted). `guard` must be one of exactly three names:
|
|
419
|
+
|
|
420
|
+
| `guard` | `target` | Meaning |
|
|
421
|
+
| --- | --- | --- |
|
|
422
|
+
| `on_failure` | an upstream step name | Runs only if that step failed |
|
|
423
|
+
| `on_flag` | a dotted `artifact.field` | Runs only if that boolean field on a produced artifact is true |
|
|
424
|
+
| `on_missing` | an artifact name | Runs only if that artifact was not produced |
|
|
425
|
+
|
|
426
|
+
Compile enforces the full `(conditional, when)` matrix as a loud fail:
|
|
427
|
+
|
|
428
|
+
- `conditional: true` with no `when` — refused; bare `conditional: true` no longer implies a
|
|
429
|
+
condition.
|
|
430
|
+
- `when` present without `conditional: true` — refused; a guard with nothing to guard is refused
|
|
431
|
+
rather than silently ignored.
|
|
432
|
+
- an unknown `guard` name, an empty `target`, or an `on_flag` target with no `.` — all refused.
|
|
433
|
+
|
|
434
|
+
This shape was introduced as part of the `0.3` IR contract. Whether — and how — a back-end realizes
|
|
435
|
+
`when` is a per-back-end decision, not a schema requirement; any future shape change still requires
|
|
436
|
+
an IR version bump under the policy above. As of this writing:
|
|
437
|
+
|
|
438
|
+
- `dispatcher/vercel` realizes it as one of two well-defined shapes: an `on_failure` guard
|
|
439
|
+
targeting the immediately-preceding call folds into that call's own bounded repair loop; every
|
|
440
|
+
other guard in the closed vocabulary is an independent step whose guard the bundle *consumer*
|
|
441
|
+
evaluates deterministically at runtime (see `dispatcher/vercel/src/classify.rs`). A `when`/
|
|
442
|
+
`conditional` shape outside the closed vocabulary — an unrecognized guard name, or `conditional`
|
|
443
|
+
and `when` disagreeing about whether a guard exists — fails loudly at emit time rather than
|
|
444
|
+
being silently folded into either shape.
|
|
445
|
+
- `dispatcher/claude-agent-sdk` realizes it at runtime via a hybrid-staged executor (see
|
|
446
|
+
`dispatcher/claude-agent-sdk/src/run.ts` / `conditional.ts`).
|
|
447
|
+
- `dispatcher/claude-code-cli` does not yet realize it — this back-end has no deterministic
|
|
448
|
+
runtime to evaluate a guard against, so it still treats `conditional` as an opaque flag and
|
|
449
|
+
tolerates `when` without acting on it (see `dispatcher/claude-code-cli/src/ir.rs`).
|
|
450
|
+
|
|
451
|
+
A back-end that ignores `when` must do so as a documented, deliberate choice (as `claude-code-cli`
|
|
452
|
+
does above) — never as a silent fallback for a guard shape it was simply never taught to recognize.
|
|
453
|
+
|
|
454
|
+
#### `guardrails[].scope` / `threshold` and the `locked`/`overridable` normalization
|
|
455
|
+
|
|
456
|
+
`scope` and `threshold` are passthrough fields — each is present in the resolved IR **only when
|
|
457
|
+
authored** on the component. Their meaning is selected by `name` and the target: for example,
|
|
458
|
+
`artifact_write.scope` and `context_write_authz.scope` define distinct path boundaries, while a
|
|
459
|
+
threshold is guardrail-specific structured policy. The compiler preserves these values but does
|
|
460
|
+
not itself validate path containment or assign a universal meaning to every scope; a target that
|
|
461
|
+
claims support must consume the relevant field at its enforcement seam. Dropping a scope from a
|
|
462
|
+
scoped write guardrail widens the represented write boundary and is not a semantics-preserving
|
|
463
|
+
fallback. Both fields are omitted when not authored.
|
|
464
|
+
|
|
465
|
+
Authoring may declare `locked` and/or `overridable` on a guardrail, but the **IR only ever emits
|
|
466
|
+
`locked`** as its normalized lock-state. It is the single source of truth for whether a guardrail
|
|
467
|
+
is locked, while downstream consumers may also inspect the guardrail's `name`, `scope`, and
|
|
468
|
+
`threshold` to select and configure enforcement. At compile:
|
|
469
|
+
|
|
470
|
+
- authoring may declare exactly **one** of `locked` or `overridable` (`overridable: true` normalizes
|
|
471
|
+
to `locked: false`);
|
|
472
|
+
- declaring **both**, if they agree (`locked: true` + `overridable: false`, or `locked: false` +
|
|
473
|
+
`overridable: true`), is accepted and normalized the same way;
|
|
474
|
+
- declaring **both** with a contradiction, or declaring **neither**, is a loud compile-time fail.
|
|
475
|
+
|
|
476
|
+
#### `eval` and `eval_ref`
|
|
477
|
+
|
|
478
|
+
`eval_ref` is the legacy synthesized reference string (`"<id>.eval"`) — kept for back-compat with
|
|
479
|
+
tooling that only needs a pointer. `eval` is the newer **structured form**, `{ "template_ref",
|
|
480
|
+
"metrics": [...] }`, present **only when authored** on the component (see `dashboard` in
|
|
481
|
+
`examples/render-demo/ir.golden.json`, which has no `eval` block and only `eval_ref`, vs. `generate_dashboard`
|
|
482
|
+
in `examples/demo-agent/ir.golden.json`, which has both). `eval` is what can actually drive an eval loop
|
|
483
|
+
(concrete metrics + template); `eval_ref` remains only a reference string.
|
|
484
|
+
|
|
485
|
+
#### `effect.outcome` facets
|
|
486
|
+
|
|
487
|
+
`outcome.kind` stays the stable 4-value union (`none | assertion | mutation | dispatch`) — the spine
|
|
488
|
+
does not grow a new arm. On top of `kind`, authoring may declare type-specific facets that are
|
|
489
|
+
**parsed and passed through the IR today but not yet consumed by the MVP (analytical) back-ends**:
|
|
490
|
+
they are forward-declared, not silently dropped.
|
|
491
|
+
|
|
492
|
+
| Facet | For `type` | Meaning |
|
|
493
|
+
| --- | --- | --- |
|
|
494
|
+
| `verdict_type` | `assertive` | the shape of the assertion's verdict |
|
|
495
|
+
| `emits` | `assertive` | events this outcome may publish (routing) |
|
|
496
|
+
| `target` | `mutating` | what's being mutated, e.g. `data` vs `context` |
|
|
497
|
+
| `change_type` | `mutating` | the kind of mutation |
|
|
498
|
+
| `routable_scope` | `orchestrating` | what this dispatch may route to |
|
|
499
|
+
|
|
500
|
+
#### `brief` (additive since v0.5)
|
|
501
|
+
|
|
502
|
+
Optional free-form text, authored on the component (or replaced wholesale by a profile mount, see
|
|
503
|
+
[`profile-schema.md`](./authoring.md#3-profile--bind-a-harness-to-a-context)) and rendered with the
|
|
504
|
+
same `{{project}}` / `{{project_name}}` placeholder substitution as step prompts (§Prompt
|
|
505
|
+
rendering). Emitted **once, on the node itself** — never per-step — and present in the IR only when
|
|
506
|
+
authored; a component with no `brief` produces IR byte-identical to before this field existed.
|
|
507
|
+
Every back-end that assembles a system prompt places it in the same position: after the
|
|
508
|
+
machine-generated preamble, before the body, on both the driver and every subagent. See
|
|
509
|
+
[`profile-schema.md`](./authoring.md#brief--authored-framing-shared-across-every-step) for the
|
|
510
|
+
authoring rule, the token-cost note, and the eval-invalidation note.
|
|
511
|
+
|
|
512
|
+
#### `description` / `examples` (additive, optional)
|
|
513
|
+
|
|
514
|
+
`description` is a string and `examples` a list of strings, both authored on the component and both
|
|
515
|
+
emitted onto the node only when authored and non-empty. They are part of the first released `0.5`
|
|
516
|
+
contract; a component with neither omits both fields. That omission preserves the compact serialized
|
|
517
|
+
shape, but does not relax the rule above: any future change to these fields' IR shape requires a
|
|
518
|
+
version bump.
|
|
519
|
+
|
|
520
|
+
Unlike `brief` they take **no placeholder substitution**: they describe the component to whoever is
|
|
521
|
+
choosing between components, and a description that only reads correctly once a project is bound
|
|
522
|
+
cannot serve a skill list published to another agent.
|
|
523
|
+
|
|
524
|
+
Both are validated at compile time rather than shipped broken: a `{{...}}` placeholder in either
|
|
525
|
+
field, or `examples` with no `description` (which every consumer reaches *through* the description),
|
|
526
|
+
is a loud failure.
|
|
527
|
+
|
|
528
|
+
Consumers are selectors, never the running agent's behavior: a back-end applies `description` to the
|
|
529
|
+
agent that *is* the component (including a per-step-split driver, whose internal subdivision is not
|
|
530
|
+
what the component is for) and leaves everything inside the component — per-step subagents and a
|
|
531
|
+
context-isolation child alike — its own internally-scoped line. Absent, a back-end synthesizes a line
|
|
532
|
+
from the node's shape instead. See
|
|
533
|
+
[`profile-schema.md`](./authoring.md#description--examples--what-the-component-is-for).
|
|
534
|
+
|
|
535
|
+
---
|
|
536
|
+
|
|
537
|
+
## Resolution rules (front-end `warble compile` must implement)
|
|
538
|
+
|
|
539
|
+
1. **Parse** `profile.yml`, each mounted `components/<id>/component.yml`, and `context/binding.yml`.
|
|
540
|
+
Each `component.yml` is checked against `deny_unknown_fields` (applies to `component.yml` only,
|
|
541
|
+
not `profile.yml` / `context/binding.yml`): an authoring field the schema
|
|
542
|
+
does not recognize is a loud compile-time fail (never silently ignored).
|
|
543
|
+
2. **Merge** `IR.node = component fields ⊕ supported profile mount fields`:
|
|
544
|
+
- `profile.components[].config` is accepted by the parser but ignored by the compiler; it does
|
|
545
|
+
not override defaults, cadence, thresholds, or any other behavior.
|
|
546
|
+
- `profile.components[].tier_overrides.{step}` overrides that step's `tier` in `llm_calls`.
|
|
547
|
+
- `realization_kind`: the component's required authored value unless the profile mount replaces it.
|
|
548
|
+
3. **Fill required binds**: every component `params[].bind: required` must be supplied by
|
|
549
|
+
`profile.components[].bind`. Missing → **compile error** (loud fail). Then **resolve effective
|
|
550
|
+
binds**: for every `bind`-family param (required or optional), its effective value is the
|
|
551
|
+
mount-supplied bind, or else the param's declared `default`, or else absent (only possible for
|
|
552
|
+
`bind: optional` with no `default`). This effective-binds map feeds both the IR's additive
|
|
553
|
+
`binds` facet (§`binds`, emitted only when non-empty) and the next step.
|
|
554
|
+
4. **Resolve `$param:<name>` references and evaluate `context_precondition`**: every entry's
|
|
555
|
+
`predicate` must be a member of the closed eleven-name vocabulary (unknown → loud fail). Before
|
|
556
|
+
evaluation, any `args` value of the form `"$param:<name>"` is substituted with that param's
|
|
557
|
+
effective value from step 3 — `<name>` not naming a declared param → **compile error** (loud
|
|
558
|
+
fail); naming a declared param with no effective value → the precondition is **unanswerable**
|
|
559
|
+
(below), not silently evaluated against a missing value. The IR's `context_precondition[].args`
|
|
560
|
+
always carries the **resolved** value, never the `"$param:<name>"` template. The predicate is
|
|
561
|
+
then **evaluated** against the bound context via the injected `ContextLoader`:
|
|
562
|
+
answerable-and-false → loud fail; unanswerable (`can_answer=false`) → a distinct loud fail; pass
|
|
563
|
+
→ recorded in `precondition_result.checks`.
|
|
564
|
+
5. **Validate `params` shape**: each entry must declare exactly one of `bind`/`source`; `source`, if
|
|
565
|
+
present, must be `"runtime-injected"`. Violations → **compile error** (loud fail).
|
|
566
|
+
6. **Normalize `guardrails[].locked`**: resolve authored `locked`/`overridable` down to a single
|
|
567
|
+
`locked` boolean per the rule above; contradictory or absent declarations → **compile error**
|
|
568
|
+
(loud fail).
|
|
569
|
+
7. **context_binding**: `project` = resolved path from `context/binding.yml` `project:`
|
|
570
|
+
(kept as-authored: relative paths stay relative to the project-dir). `binding_mode` from component.
|
|
571
|
+
(v0.3) `resolved` = the fine-grained block the `ContextLoader` produces from MDL introspection
|
|
572
|
+
(metrics/dimensions/grains + lineage summary). The coarse `project` path is retained alongside it.
|
|
573
|
+
8. **prompt rendering** (see below) → `prompt_fragment` and per-step `llm_calls[].prompt`.
|
|
574
|
+
9. **tier**: carry the step's tier **name** as a string in `llm_calls` (the standard core is
|
|
575
|
+
`strong`/`cheap`, but the vocabulary is open — a component may use custom tier names); do **not**
|
|
576
|
+
resolve it to a concrete model (that is the dispatcher's runtime-injected job — see the
|
|
577
|
+
`ModelConfig` / `warble dispatch --models-config` binding in `authoring.md` §6.1.1).
|
|
578
|
+
|
|
579
|
+
## Compile-time checks — all loud-fail (non-zero exit + clear message)
|
|
580
|
+
|
|
581
|
+
| Check | Trigger | Error |
|
|
582
|
+
| --- | --- | --- |
|
|
583
|
+
| bind-required | a `params[].bind: required` not supplied by profile | `missing required bind '<name>' for component '<id>'` |
|
|
584
|
+
| locked-guardrail override | profile tries to remove/weaken a `guardrails[].locked: true` | `cannot override locked guardrail '<name>' on component '<id>'` |
|
|
585
|
+
| unparseable context | the bound project does not assemble/parse (coarse floor) | `context precondition failed: bound project '<path>' is not a parseable wren project …` |
|
|
586
|
+
| unknown precondition predicate | `context_precondition[].predicate` not in the closed 11-name vocabulary | `unknown context_precondition predicate '<name>' on component '<id>' …` |
|
|
587
|
+
| precondition not satisfied | a predicate is answerable but evaluates false against the bound context | `context precondition '<name>' not satisfied by the bound semantic layer for component '<id>'` |
|
|
588
|
+
| precondition unanswerable | the adapter cannot express the answer (e.g. `metric_additive` with no declared metric, a raw-shape predicate on an MDL-only adapter, or a `$param:` reference with no effective value) | `context precondition '<name>' … cannot be evaluated … Refusing rather than answering wrongly.` |
|
|
589
|
+
| `$param:` references an undeclared param | a `context_precondition[].args` value is `"$param:<name>"` and `<name>` is not one of the component's own `params[]` | `precondition arg '<key>' on component '<id>' references '$param:<name>', but '<name>' is not a declared param of this component` |
|
|
590
|
+
| param bind/source exclusion | a `params[]` entry declares both `bind` and `source`, or neither | `param '<name>' must declare exactly one of 'bind' or 'source' for component '<id>'` |
|
|
591
|
+
| unknown param source | `params[].source` present but not `"runtime-injected"` | `unknown param source '<value>' for param '<name>' on component '<id>'` |
|
|
592
|
+
| contradictory/absent guardrail lock state | a `guardrails[]` entry declares neither `locked` nor `overridable`, or declares both with conflicting values | `guardrail '<name>' on component '<id>' must declare exactly one (agreeing) of 'locked'/'overridable'` |
|
|
593
|
+
| unknown authoring field | `component.yml` (the `ComponentFile` and its nested structs) contains a field the schema does not recognize | `unknown field '<name>'` (serde `deny_unknown_fields`) — note: applies to `component.yml` only in this phase, not `profile.yml` / `context/binding.yml` |
|
|
594
|
+
| conditional step missing `when` | `llm_steps[].conditional: true` with no `when` | `conditional step '<name>' on component '<id>' has no 'when' guard — …` |
|
|
595
|
+
| `when` guard without `conditional` | `llm_steps[].when` present but `conditional` is not `true` | `step '<name>' on component '<id>' declares a 'when' guard but is not 'conditional: true' …` |
|
|
596
|
+
| unknown `when` guard name | `llm_steps[].when.guard` not in the closed 3-name vocabulary (`on_failure`/`on_flag`/`on_missing`) | `unknown guard '<name>' in step '<step>' of component '<id>' …` |
|
|
597
|
+
| `when` guard invalid target | `llm_steps[].when.target` is empty, or `guard: on_flag` with a non-dotted target | `guard '<name>' in step '<step>' of component '<id>' has an empty target` / `… expects a dotted 'artifact.field' target …` |
|
|
598
|
+
|
|
599
|
+
`required_capabilities` is **declared only** in this POC (not enforced by the compiler;
|
|
600
|
+
enforcement is the dispatcher/runtime's job).
|
|
601
|
+
|
|
602
|
+
## Prompt rendering
|
|
603
|
+
|
|
604
|
+
For `realization_kind: skill`, `prompt_fragment` is a single instruction block the dispatcher
|
|
605
|
+
drops into the agent's system prompt. The front-end builds it by rendering each
|
|
606
|
+
`llm_steps[].prompt_ref` markdown file **in declared order**, joined under `##`-level headers
|
|
607
|
+
named by step, with placeholders substituted from coarse context:
|
|
608
|
+
|
|
609
|
+
- `{{project}}` → `context_binding.project`
|
|
610
|
+
- `{{project_name}}` → basename of the project path
|
|
611
|
+
|
|
612
|
+
Each `llm_calls[]` entry also carries its own **per-step rendered `prompt`** — the same
|
|
613
|
+
substitution as `prompt_fragment`, but rendered **per step and without** the `## <name>` header.
|
|
614
|
+
This exists because a step must be realizable in isolation on any runtime: isolation severs shared
|
|
615
|
+
context, so both the step's own instructions and its named `consumes`/`produces` I/O slots must be
|
|
616
|
+
explicit on the step itself. `consumes`/`produces` are named slots only — no conditionals/loops
|
|
617
|
+
(that composition-level restraint is what `llm_calls[].conditional`, above, exists to cover without
|
|
618
|
+
growing a data-flow DSL). Absent `consumes` → `[]`; absent `produces` → `null`.
|
|
619
|
+
|
|
620
|
+
**Two realizations of the same IR:**
|
|
621
|
+
|
|
622
|
+
- `prompt_fragment` (joined) is what an in-loop runtime uses — the driver runs every step against
|
|
623
|
+
one model in one context.
|
|
624
|
+
- `llm_calls[].prompt` (per-step) is what a runtime that splits into isolated calls uses — each
|
|
625
|
+
step's prompt plus its `consumes`/`produces` slots is enough to realize it as a standalone
|
|
626
|
+
invocation.
|
|
627
|
+
|
|
628
|
+
One IR feeds both realizations; nothing here is runtime-specific. `required_capabilities` carries
|
|
629
|
+
`llm:per_step_tier` — the generic requirement "every LLM call must run at its declared tier" — and
|
|
630
|
+
never names a mechanism.
|
|
631
|
+
|
|
632
|
+
### Compile-time resolution against a target runtime (loud-fail, per the checks table above)
|
|
633
|
+
|
|
634
|
+
| Runtime supports… | Realization |
|
|
635
|
+
| --- | --- |
|
|
636
|
+
| per-step tier natively (in-loop model switch) | run steps in-loop; I/O contract unused |
|
|
637
|
+
| only `isolated_invocation` (tier-bound sub-call) | realize each divergent-tier step as an isolated call; marshal via `consumes`/`produces` |
|
|
638
|
+
| neither, and the component has heterogeneous tiers | **compile-time loud fail** |
|
|
639
|
+
|
|
640
|
+
### Runtime-general realization (same IR, borrowed mechanisms)
|
|
641
|
+
|
|
642
|
+
| Runtime | Satisfies `llm:per_step_tier` by | Needs I/O contract |
|
|
643
|
+
| --- | --- | --- |
|
|
644
|
+
| Claude Code CLI (static files) | one **subagent** per divergent-tier step (own `model:`); driver marshals | ✅ |
|
|
645
|
+
| Claude Agent SDK (programmatic) | `query({options})` picks the model per step — in-loop | ❌ |
|
|
646
|
+
| LangGraph | bind each node to its own LLM | ❌ (edges carry it) |
|
|
647
|
+
| Omnigent / meta-harness | a tier-bound sub-component it manages | ✅ |
|
|
648
|
+
|
|
649
|
+
The dispatcher stays enum-keyed and thin: it reads `llm:per_step_tier` + the I/O contract and
|
|
650
|
+
maps to whatever its runtime provides. Mechanism (spawn/collect/marshal) is **borrowed**, never a
|
|
651
|
+
Warble differentiator.
|
|
652
|
+
|
|
653
|
+
---
|
|
654
|
+
|
|
655
|
+
## Golden example
|
|
656
|
+
|
|
657
|
+
`warble compile ./examples/demo-agent -o ir.json` against the demo project in this repo must produce an
|
|
658
|
+
IR equal to `examples/demo-agent/ir.golden.json` (committed alongside, used as the core's fixture test).
|
|
659
|
+
`warble compile ./examples/render-demo -o ir.json` similarly must equal
|
|
660
|
+
`examples/render-demo/ir.golden.json`. Both goldens use the current v0.6 contract:
|
|
661
|
+
`context_requirements`, `context_precondition`, and `params` are always present (possibly `[]`, as
|
|
662
|
+
on `dashboard`), while `eval` appears only on `generate_dashboard` and `scope: "."` appears only on
|
|
663
|
+
render-demo's authored `artifact_write` guardrail.
|
|
664
|
+
|
|
665
|
+
---
|
|
666
|
+
|
|
667
|
+
## v0.3 — fine-grained context binding
|
|
668
|
+
|
|
669
|
+
Where v0.2 carried a coarse project path and *declared* preconditions, v0.3 made the front-end
|
|
670
|
+
**probe the bound context**. A host injects a `ContextLoader` (the trait lives in core, sans-IO).
|
|
671
|
+
The same binding crate now supplies `MdlContext` for Wren projects and `RawSourceContext` for raw
|
|
672
|
+
constitutive input; hosts may supply other adapters.
|
|
673
|
+
|
|
674
|
+
## What lands in the IR
|
|
675
|
+
- For a Wren project, `context_binding.resolved` carries the compiler's introspection result: `metrics`
|
|
676
|
+
(`{name, declared, additivity?}` — a declared cube measure carries inferred additivity; an
|
|
677
|
+
implicit numeric column does not), `dimensions` (`{name, temporal}`), `time_dimensions`, `models`,
|
|
678
|
+
and a `lineage` summary (`{nodes, edges, resolvable}`, plus optional `consumers` counts and
|
|
679
|
+
`diagnostics` — see `blast-radius.md` §3; both keys are omitted when empty). The full lineage DAG
|
|
680
|
+
stays in the adapter; the IR carries only the summary. A raw-source adapter emits an empty
|
|
681
|
+
semantic inventory while answering its raw-shape probes; an external adapter omits `resolved`.
|
|
682
|
+
- `precondition_result.checks` — one `{predicate, outcome}` per declared precondition, all `pass`
|
|
683
|
+
(a non-pass loud-fails before emit).
|
|
684
|
+
|
|
685
|
+
## Predicate evaluation
|
|
686
|
+
The eleven predicates evaluate **loose for existence, strict for semantics**: `has_metric` /
|
|
687
|
+
`has_*_dimension` / `model_has_timestamp` are satisfied by a matching cube member *or* a plain model
|
|
688
|
+
column (so a cube-less project can still answer data questions), while `metric_additive` is
|
|
689
|
+
answerable only over a declared metric (see the `context_precondition` section above). This is why
|
|
690
|
+
`examples/jaffle-wren` gained a `revenue` cube — it gives the layer a declared, additive metric so
|
|
691
|
+
`metric_additive` is decidable. `source_introspectable` and `raw_docs_readable` instead probe a raw
|
|
692
|
+
source through `RawSourceContext`; an MDL-only adapter returns unanswerable for both.
|
|
693
|
+
|
|
694
|
+
## `blast_radius` (read path)
|
|
695
|
+
The adapter self-builds a lineage DAG (`model → relationship / cube → metric / dimension`, plus view
|
|
696
|
+
references), and core computes `LineageGraph::blast_radius(node)` = the transitive downstream closure
|
|
697
|
+
+ worst `Severity` (`Semantic > Structural > Compatibility > None`). This is exposed as read-only
|
|
698
|
+
analysis on the read path, and the same query also serves as an enforcement gate for *mutating*
|
|
699
|
+
applies. This is the one `provided_by: warble`
|
|
700
|
+
capability — see `capability-model.md` §6/§7.1, whose coarse-binding loud-fail is now lifted because
|
|
701
|
+
fine-grained binding exists.
|
|
702
|
+
|
|
703
|
+
---
|
|
704
|
+
|
|
705
|
+
## v0.3 — render contract (typed blocks + renderer registry)
|
|
706
|
+
|
|
707
|
+
> Closes wall-hit #2 (render blocks) the same way #1 was closed: the IR declares a
|
|
708
|
+
> **runtime-agnostic typed-output contract**; each runtime supplies (or overrides) a **renderer**;
|
|
709
|
+
> Warble ships a **default reference renderer (HTML)** so there is an out-of-box result even on a
|
|
710
|
+
> plain runtime. **Status:** both render flavors are implemented in the dispatcher (see §4) — the
|
|
711
|
+
> IR-side typed `render_blocks` contract is consumed today; the front-end `warble compile` still
|
|
712
|
+
> emits `render_blocks` as coarse type names + field schema (no per-runtime renderer selection at
|
|
713
|
+
> compile time — that is the dispatcher's job).
|
|
714
|
+
|
|
715
|
+
## The gap today
|
|
716
|
+
`effect.render_blocks` is just type names (`["chart","table","kpi_card"]`), and the agent returns
|
|
717
|
+
prose. Nothing downstream can render typed blocks. Two things are missing: (a) a **data contract**
|
|
718
|
+
per block type so the agent emits *structured* blocks, and (b) a **renderer** that turns those
|
|
719
|
+
blocks into an artifact.
|
|
720
|
+
|
|
721
|
+
## 1. Typed block contract (Warble stdlib, extensible)
|
|
722
|
+
`effect.render_blocks` becomes typed entries carrying each block's field schema. Warble ships a
|
|
723
|
+
small stdlib of block types; components may extend it.
|
|
724
|
+
|
|
725
|
+
```jsonc
|
|
726
|
+
"render_blocks": [
|
|
727
|
+
{ "type": "kpi_card", "fields": { "label": "string", "value": "number|string", "unit": "string?", "delta": "number?" } },
|
|
728
|
+
{ "type": "table", "fields": { "columns": "string[]", "rows": "row[]" } },
|
|
729
|
+
{ "type": "chart", "fields": { "chart_type": "bar|line|pie|area|scatter", "x": "string", "series": "string[]", "rows": "row[]" } },
|
|
730
|
+
{ "type": "narrative", "fields": { "title": "string?", "text": "string" } },
|
|
731
|
+
{ "type": "diff", "fields": { "path": "string?", "diff": "string" } }
|
|
732
|
+
]
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
The `diff` block is the stdlib block for a **mutating** component's dry-run proposal (added for
|
|
736
|
+
`edit_pipeline`): the target `path` and the raw unified-`diff` text, rendered HTML-escaped
|
|
737
|
+
inside a `<pre>` (never re-parsed as markup). It is the presentational facet of the change a reviewer
|
|
738
|
+
approves — it does not itself apply anything.
|
|
739
|
+
|
|
740
|
+
The `narrative` block is the stdlib text/prose block (added for `explain_change`, whose output is a
|
|
741
|
+
data-native explanation, not a chart). The reference renderer emits an optional `title` heading plus
|
|
742
|
+
the escaped `text` body (blank lines → paragraphs); it is deliberately minimal — prose, not a
|
|
743
|
+
rich-markdown surface. A component whose output is an explanation declares `render_blocks:
|
|
744
|
+
[narrative]`; because both back-ends render through `warble render`, no per-back-end renderer change
|
|
745
|
+
is needed.
|
|
746
|
+
|
|
747
|
+
## 2. Agent output envelope (runtime-agnostic, structured — not prose)
|
|
748
|
+
The renderable component returns a structured envelope of **block instances** conforming to the
|
|
749
|
+
contract, plus optional prose:
|
|
750
|
+
```jsonc
|
|
751
|
+
{ "blocks": [
|
|
752
|
+
{ "type": "kpi_card", "label": "Total customers", "value": 100 },
|
|
753
|
+
{ "type": "table", "columns": ["status","orders","revenue"], "rows": [["completed",67,1103], ...] }
|
|
754
|
+
],
|
|
755
|
+
"summary": "…prose…" }
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
### Provenance: `verified` and per-block `definition`
|
|
759
|
+
|
|
760
|
+
Two optional additions to the envelope let a reviewer check a rendered answer against its source
|
|
761
|
+
instead of trusting the prose alone:
|
|
762
|
+
|
|
763
|
+
- **`verified` (envelope-level boolean, optional)** — whether the agent actually ran the query it is
|
|
764
|
+
reporting on (via the `wren` CLI) versus recalling/estimating a figure. Absent means unknown, not
|
|
765
|
+
false; a component whose steps always execute before rendering may set it unconditionally.
|
|
766
|
+
- **`definition` (per-block, optional)** — attached to a data-bearing block (typically `kpi_card` or
|
|
767
|
+
`table`) to carry how the number was produced: `{ "sql": "...", "source_tables": ["..."],
|
|
768
|
+
"filters": ["..."] }`. This is presentational provenance for the renderer to show alongside the
|
|
769
|
+
block (e.g. an expandable "how was this computed" panel) — it does not feed back into computation
|
|
770
|
+
and is not itself re-executed.
|
|
771
|
+
|
|
772
|
+
Both are additive optional fields on the existing envelope/block shapes above, not a new block type;
|
|
773
|
+
a renderer or consumer that doesn't recognize them ignores them.
|
|
774
|
+
|
|
775
|
+
## 3. Renderer registry — `render(target, blocks[]) → artifact`
|
|
776
|
+
Warble owns the **contract + a reference renderer (HTML)**; runtimes register/override per target.
|
|
777
|
+
|
|
778
|
+
| target | how blocks render | provider |
|
|
779
|
+
| --- | --- | --- |
|
|
780
|
+
| `html` (**default**) | one self-contained `dashboard.html` (KPI cards, HTML tables, JS charts) | Warble reference renderer |
|
|
781
|
+
| `markdown` | markdown tables + text (the plain-CLI degrade) | Warble |
|
|
782
|
+
| `wren-genbi` | delegate to `wren genbi build/verify/open` | borrowed (wren) |
|
|
783
|
+
| `react` / IDE / web host (future) | native components | that runtime |
|
|
784
|
+
|
|
785
|
+
## 4. Two renderer flavors (default programmatic, prompt fallback) — **implemented**
|
|
786
|
+
Selected at dispatch via `warble dispatch … --render-flavor <programmatic|prompt>` (default
|
|
787
|
+
`programmatic`). The IR is flavor-agnostic; the flavor lives in the back-end.
|
|
788
|
+
|
|
789
|
+
- **programmatic (default)** — ✅ implemented: the emitted agent stays **fully read-only** (no
|
|
790
|
+
`Write` tool) and is instructed to emit the `{ blocks, summary }` envelope as its final message.
|
|
791
|
+
The back-end ships a **reference renderer** (`warble render`, `dispatcher/claude-code-cli/src/render.rs`) that
|
|
792
|
+
turns that envelope → a self-contained `dashboard.html` **deterministically** (inline SVG charts,
|
|
793
|
+
no clock/RNG, no external assets → same envelope ⇒ identical bytes). The two-step run is documented
|
|
794
|
+
in the emitted `RUN.md`:
|
|
795
|
+
```sh
|
|
796
|
+
claude -p "<data question>" --agent dashboard --output-format json > result.json
|
|
797
|
+
warble render result.json --out dashboard.html
|
|
798
|
+
```
|
|
799
|
+
`warble render` also unwraps the `--output-format json` result object and tolerates the model
|
|
800
|
+
fencing/prose-wrapping the envelope (see `parseEnvelope`).
|
|
801
|
+
- **prompt fallback** — ✅ implemented (`--render-flavor prompt`): for when there is no post-step to
|
|
802
|
+
run the renderer, the dispatcher bakes the block contract + "write `dashboard.html`" instruction
|
|
803
|
+
into the prompt and grants the agent **scoped artifact-write**. Works in the pure file model; HTML
|
|
804
|
+
is LLM-produced → non-deterministic.
|
|
805
|
+
|
|
806
|
+
## 5. Guardrail split this forces (data-write ≠ artifact-write)
|
|
807
|
+
Rendering writes a file, but the component is `read_only_execution`. These must be **separate
|
|
808
|
+
enforcement points** (see [`enforcement-seam.md`](./enforcement-seam.md)):
|
|
809
|
+
- `data:read_only` — never mutate the warehouse (wren `strict_mode`). Unchanged.
|
|
810
|
+
- `artifact:write(scoped)` — may write only the output dir (the HTML). Needed **only** on the
|
|
811
|
+
prompt-fallback path; the programmatic path keeps the agent read-only entirely.
|
|
812
|
+
|
|
813
|
+
## 6. Capability + loud-fail
|
|
814
|
+
- Component declares `required_capabilities: [render_contract]` (generic: "the declared blocks must
|
|
815
|
+
be renderable"). Never names `html`/`react` — that is the target's realization.
|
|
816
|
+
- Compile against a target: has a renderer for these block types → render; else can degrade to
|
|
817
|
+
`markdown` → warn; else → loud-fail. Same pattern as §5 / per-step-tier.
|
|
818
|
+
|
|
819
|
+
The IR names **types + data contract + the requirement** only; *how* a chart becomes pixels is the
|
|
820
|
+
renderer's job (Warble default HTML, or a runtime override). Contract owned; renderer borrowed/pluggable.
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@warble/ir-spec",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"type": "module",
|
|
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.",
|
|
7
|
+
"homepage": "https://github.com/Canner/Warble/tree/main/packages/ir-spec#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/Canner/Warble",
|
|
11
|
+
"directory": "packages/ir-spec"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/Canner/Warble/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": ["warble", "ir", "schema", "spec"],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"main": "./index.js",
|
|
24
|
+
"module": "./index.js",
|
|
25
|
+
"types": "./index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./index.d.ts",
|
|
29
|
+
"import": "./index.js"
|
|
30
|
+
},
|
|
31
|
+
"./package.json": "./package.json"
|
|
32
|
+
},
|
|
33
|
+
"files": ["index.js", "index.d.ts", "ir-schema.md"]
|
|
34
|
+
}
|