Repository navigation
Conversation
A shared `yq` attribute selects one JSON value out of what a component returned, and `<Yaml>` renders a value as a YAML document. Both are one WebAssembly engine, built once per execution and only when something asks it a question: a document that writes neither decodes and compiles nothing. The engine's bytes are committed source. `scripts/generate-yq-wasm.ts` writes `packages/core/src/yq-wasm.ts` from the pinned `yq.rs@0.1.0` package, verifying its name, version, length and digest, and emits the provenance and the vendor notices beside it. A compiled binary and an installed npm package therefore carry the engine in their own static import graph rather than reaching for a path, a fetch or a host filesystem. Selection sits in one fixed place in an invocation's life: the producer's own contract accepts what it produced, its invocation joins, and only then is the expression asked. An expression repairs no return its schema rejected, and an answer that is not one JSON value refuses with the way to ask for several. A host that evaluates generated source on somebody's behalf can own where one occurrence's selected result goes. That destination is stated once, as a value canonical execution copies before any document code runs, and is authenticated against the exact definition this execution's capture published. It is reachable through no context, no name and no metadata, and a receiver belongs to one occurrence and is revoked when that invocation settles. The REPL Sidekick uses it. A reply may write `yq` on any control it is admitted to; the intent is marked before the work, the original outcome is recorded as it always was, and a separate acknowledged `control-presentation` record carries the selected value. What a next turn is sent is that record — so an acknowledgement that never landed sends nothing rather than a raw fallback. Static document validation now takes its engine-owned props from `ENGINE_OWNED_PROPS` rather than keeping its own list, which is what lets an entry a run executes also pass the REPL's preflight.
Four reviewed contracts were not held, and one authoring rule was unfinished. A canonical `<Elicit>` the reply queried sent back the whole answer. Core opened a destination for that exact occurrence, but the question writer never received it: the intent went down unmarked and no presentation followed, so a reply that asked for one field was told both. The destination now hands its occurrence to the question provider before the asking, which marks the intent, retains the whole answer as the operation's own evidence, and files the order that carries one acknowledged selected value. A question nobody answered files nothing and keeps its marker, so an interrupted selection reads as one. A genuine uncaptured destination skipped the producer's own return schema. A destination relaxes required-as — it does not relax what the component agreed to produce — so a constant expression could turn a rejected return into a delivered answer. The uncaptured path now validates a declared `returns` after joined teardown and before the query, middleware and delivery. An admitted entry that failed still produces its Submit receipt, and querying that receipt wrote a history nothing could read back: the projector rejected the presentation its own supported path had just written. The intent's reservation is what distinguishes the two cases, and both the projector and the feedback owner now read it. A submission refused before admission reserved nothing and still keeps refused feedback. `ComponentResult` and its consumer types are exported from `packages/core/api.ts` under the unchanged name, with destination authority left on the host boundary. `yq` is now a reserved declaration name beside `slot` and `as`; static validation diagnoses a known non-string before any effect; an unresolved dynamic query reports the site as not statically checkable rather than valid; and generated preflight refuses a known-invalid query before the first effect. Four frozen assertions are completed with the fixtures that already existed: a halted initializer waited to real settlement, an evaluator that throws carrying partial output, a queried Markdown body whose later element and cleanup precede the result seam, and a cold-opened history whose marked selection was never acknowledged.
A queried `<Elicit>` written inside another queried `<Elicit>`'s content sent the outer question's whole answer back as feedback. The outer occurrence captured the field it selected, but what the next turn received was both fields, and the outer intent carried no marker at all — so a later reader could not tell that a selection had been owed there. The cause was a single shared cell. Canonical `<Elicit>` is core's own body, so the Sidekick cannot ask the occurrence lookup which invocation it is in: it held the occurrence most recently opened instead. Opening the inner destination overwrote that cell, and the inner question consumed it, leaving the outer question with nothing to file against. Core now names the occurrence it is dispatching for exactly as long as that body runs. Protected dispatch already had both the invocation and the body, so the route establishes the pairing there and restores the previous one however the body leaves. The answer therefore nests the way the bodies do: a question asked inside another's content reaches its own receiver, and the enclosing question reaches its own again when its body resumes. An invocation that never reaches its body is never the answer, and an unwinding out of order removes its own entry rather than somebody else's. The Sidekick reads that instead of keeping a cell. Nothing is consumed, nothing is keyed by name, completion order or a value a document could replace, and the receiver still belongs to the exact invocation canonical capture authenticated. Each occurrence keeps its whole original answer as its own evidence and gets one acknowledged selected presentation. Cancellation, refusal and joined lifetimes are unchanged.
Main now carries the corrected #939 and the approved #940, and the corrected #939 deleted the per-name root publication protocol #934 was built on. This merge keeps both histories and moves #934's selected capture onto the parent's own model. `root-binding-publications.ts` stays deleted. The capture no longer publishes a record per name: it validates the producer's own return, lets the invocation join, asks the query, and assigns the selected value to the actual binding environment. The execution's own computed-bindings resource carries current eligible root data onto the ordinary result from there, and the successor starts from a detached copy of what that accepted result retained. What that changes in observable behaviour, and what the evidence now states: A mutation made before the result is part of that entry's retained data, because the result carries the entry's current root rather than a snapshot taken at capture time. A later entry therefore carries its own edit onward to its successor without re-exporting the name, while the earlier entry, its cold reading and the copy it handed over all stay as they were. A rejected query assigns no binding at all. `<File>` prints the refusal, the run continues, and the earlier selected value is still in the result with no second value and no fallback to the producer's text beside it. A refused ordinary result acknowledges no computed map: the run reports the persistence failure, the handle refuses rather than answering with data no later run could start from, and nothing compensating is written. One accepted assertion changed its number rather than its claim. A bound operand with a counting getter is now read twice — once by the renderer and once by the execution copying eligible root data — because that second read is the parent's accepted behaviour for every bound value. The claim is unchanged: the renderer reads once, and three reads would mean it serialized twice. The R1 receiver correction is preserved through the generated-XMD conflict: protected dispatch still names the occurrence it is dispatching for the duration of that body, beside the parent's fragment and root semantics.
Both reviews of the integrated head found the same remaining defect, and it
is this: the occurrence a protected body is dispatching for was kept in one
array per execution, innermost last, and read as "the last one entered".
That is a nesting answer. Two admitted concurrent branches are not nested —
a branch that has entered its question's body and is waiting inside its own
content is still on that array when a sibling branch asks, and the sibling
reads the waiting one's receiver.
The retained observation: one `<All>` with two `<Spawn>` children, each
asking a queried canonical `<Elicit>`. The first branch's question content
holds an ordinary `<REPL.Bindings />` read open, so the second branch asks
first; releasing the read lets the first branch ask while the second is
still waiting to be answered. Both people's answers arrive normally and both
selected captures reach their bindings — but the first branch filed its
order against the second's receiver, so its own destination had nothing to
present, and its successful selected fact came back as incomplete.
The answer is now a fact about where the asking is, rather than about what
this execution did last. Canonical dispatch says which invocation it is
running the body of on that body's own scope, and the provider asks for it
from the scope it is asking in. Concurrent branches are concurrent scopes,
so each receives its own however their suspensions interleave, and a body
nested in another's content shadows the enclosing one only within itself.
The context is this route's own and unexported, and what it holds is still
resolved through the receivers this route opened: an invocation it opened
nothing for — including one another execution's dispatch is running —
reaches no receiver.
Nothing else moved. Authentication is still the exact definition or the
selected protected name, revocation is still per invocation, the answer is
still core's own `<Elicit>`, and each successful queried question still
writes one marked intent, one whole original answer and one acknowledged
selected presentation. No composition was restricted.
The new lifecycle row drives that overlap through the real owner, with the
real evaluator, the real form provider and the reply's own ordinary read
held by the case. It asserts both captures, both whole answers, both marked
intents, one acknowledged presentation per question order, the intervening
ordinary `{}` read that owes none, and feedback carrying the two selected
values with no incomplete result and no unselected field. The nested row
stays.
| // An occurrence whose body filed nothing recorded no intent to | ||
| // present, so there is nothing for a presentation to belong to — a | ||
| // refused original outcome and an unqueried occurrence both land | ||
| // here, and both keep the feedback they already had. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // here, and both keep the feedback they already had. |
| // its entry's receipt, and the selection is of that answer. The failure is | ||
| // still the operation's own and still reaches the reply through the outcome | ||
| // it belongs to. A submission refused before admission reserved nothing, so | ||
| // it keeps refused feedback as it always has. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // it keeps refused feedback as it always has. |
| component, | ||
| kind: "answered", | ||
| // The range outside the payload, so a reply can tell what it selected from | ||
| // how much the producer actually observed. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // how much the producer actually observed. |
| // omitted one would describe a vocabulary narrower than the one the response | ||
| // is admitted under — and a hand-kept copy would stop being true the next | ||
| // time core admits a pure component. Their returns are the reply's own to | ||
| // render: nothing is performed, so there is nothing to answer back. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // render: nothing is performed, so there is nothing to answer back. |
| // No execution terminal: an expansion driven directly resolves its imports | ||
| // through the ordinary public Component Api, exactly as it always has. | ||
| // And no destinations: a result has nowhere to go that an execution did | ||
| // not state. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // not state. |
| if (asBinding === undefined) { | ||
| // Queried and uncaptured: the selected value is what this element | ||
| // renders. A string renders as itself and anything else renders | ||
| // nothing, which is the rule an uncaptured result already follows. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // nothing, which is the rule an uncaptured result already follows. |
PR #949: ✨ Select a component's result with yq, and render a value as YAML (#934)61 files, +8866 / -185 Scope🔴 PR has 9051 lines changed. Split into focused PRs. 🟡 9051 lines changed. PRs under 400 receive more thorough review. 🟡 61 files changed. Are all changes related? 🟡 Changes span 10 directories. 🟡 PR mixes config and source changes. Structural🟡 Type declarations with no consumers: SubmitForReply.
🟡 2 console statements. Oxlint structural signals:
Slop
Oxlint slop signals:
Static AnalysisOxlint: 174 diagnostics across 19 files (25 rules) no-shadow (38): packages/cli/src/repl/sidekick/model.ts, packages/cli/tests/fixtures/repl/issue-883/recovery-phase.ts, packages/cli/tests/fixtures/repl/issue-883/mounted-program.ts (+8) CorrectnessNo extraneous code patterns detected. |
Closes #934.
Why
A document can read a value producer, but it cannot say which part of the value
it wants.
<File path="package.json" as="pkg" />binds the whole file; selecting.scriptsfrom it means writing an eval block to do by hand what the produceralready produced. The Sidekick has the sharper version of the same problem: a
reply that asks a person a question gets the whole answer back as feedback,
including the parts it never asked for.
This adds one shared query to component values —
yq, the syntaxyq.rsimplements — applied to the value a producer actually returned, after that
producer's own validation and before capture, publication and designated
feedback. It adds
<Yaml value={...} />beside<Json>so a selected value canbe rendered explicitly in either form.
What changes
Before:
binds the file's whole text, and a reply's
<Elicit>feedback carries everyfield the person filled in.
After:
binds the selected object, and a queried Sidekick question sends back only what
the reply selected while the history keeps the whole original answer.
yqis an engine-owned prop besideslotandas, so a producer never seesit and no producer has to opt in.
from_json/from_yamlin the expression.no match is
[], andnull/falseare values. An invalid expression failsvisibly and binds nothing — never the unfiltered input as a fallback.
yq, every producer behaves exactly as it did.<Yaml>renders the same JSON types<Json>does, and capturing its textdoes not change the structured binding underneath.
How it works
The engine owns a lazily-created, execution-scoped
yq.rsinstance with noambient filesystem, process, environment or clock access; the wasm bytes are
committed and verified rather than fetched.
For the Sidekick, a queried occurrence writes three things: the marked intent
before the work, the whole original outcome, and one acknowledged selected
presentation. Which occurrence a provider call belongs to is answered from the
scope that call is running in — canonical dispatch names the invocation it is
running the body of on that body's own Effection scope — so nested questions and
concurrent
<All>/<Spawn>branches each keep their own receiver, howevertheir suspensions interleave.
Review guide
Start with:
packages/core/src/value-query.tsandpackages/core/tests/yq-component.test.tsThen review:
packages/core/src/component-result.tsandpackages/core/api.ts— thepublic result/query seam a consumer sees.
packages/core/src/expand.ts— where selection sits in the invocation'sorder: validation and joined teardown first, assignment after.
packages/core/src/component-result-destinations.tsandpackages/cli/src/repl/sidekick/{controls,questions,feedback,model}.ts—per-occurrence receivers and the selected-presentation record.
packages/core/src/components/Yaml.ts,Json.ts,json-serialization.ts.packages/core/src/yq-wasm.ts,scripts/generate-yq-wasm.ts,scripts/tests/yq-distribution.test.ts— the pinned engine and itsdistribution proof.
Look carefully at:
component-result-destinations.ts— a receiver belongs to one occurrence.Nothing correlates by component name, completion order or "the last one".
what it returned.
What must stay true
yqruns once, over the value one call returned — enforced by the singlecomponentResultseam inexpand.ts, checked byyq-component.test.ts(producer-crossing counts) and
component-returns.test.ts.the two-producer and producer-once rows in
yq-component.test.ts.the
yq='!!!'row:<File>prints the refusal, the first binding stillholds, the second name stays unresolved.
answer and one acknowledged selected presentation for its own occurrence —
checked by the nested and overlapping rows in
repl-sidekick-lifecycle.test.tsand byrepl-sidekick-controls.test.ts.answer and never a silent "unknown" — checked by the same rows plus the cold
incomplete case in
repl-sidekick-recovery.test.ts.checked by
scripts/tests/yq-distribution.test.tsanddeno task verify:clean.How to verify it
deno task test packages/core/tests/yq-component.test.tsproves selection,cardinality, explicit decoding, producer-once and the query-refusal account;
it fails if a query rebinds the original text or runs a producer twice.
deno task test packages/core/tests/yaml-component.test.tsproves<Yaml>renders the same values
<Json>does and serializes once; a second rendererread would make the counting-getter row fail.
deno task test packages/cli/tests/repl-sidekick-lifecycle.test.tsproves areply's question is this owner's, and that nested and overlapping queried
questions each keep their own receiver; it fails if one occurrence files its
order against another's destination.
deno task test packages/cli/tests/repl-entries.test.tsproves a selectedcapture reaches the next entry and that an earlier entry's history does not
move.
deno task verify:cleanproves a clean checkout installs only in preparationand that the builds leave
node_modulesanddeno.lockbyte-identical.Scope
Included
yqselection on component values, with its engine, distribution andstatic validation.
<Yaml value={...} />beside<Json>.record, acknowledgment and feedback.
Intentionally unchanged
<Slice>output excerpts (Add<Slice>for bounded output excerpts #942) and saved-entry inspection (Inspect saved entry failures from the xmd repl Sidekick #932) — a queryselects before bounded presentation, and neither is duplicated here.
yq, including<File>'s existing printed-errorpolicy.
publication protocol returns.
New dependencies
yq.rs@0.1.0, as committed verified wasm bytes plus its vendoredlicence and third-party notices.
JSON query dialect, and shelling out to a binary is not available to a
document execution.
Generated or mechanical changes
packages/core/src/yq-wasm.tsandyq-wasm.provenance.jsoncome fromscripts/generate-yq-wasm.ts; the generator's--checkmode is a CI gate.packages/core/src/yq-vendor-notices/is the upstream licence text.Risks and limitations
yq.rsport, not jq: the documentation saysyqrather than advertising jq compatibility.
the live journey were produced on earlier heads of this branch. This final
commit changes five files and no dependency, loader, asset, package mapping,
build flag, runtime adapter, retained format or transport, so those results
are reused rather than re-timestamped. They are stated as historical.
Code version (2.1.238 against 2.1.251 required); it is not a successful Claude
transport proof.
Review and ownership evidence
cf0678bf23764ebc2dfb41701026bae620416d8e, tree65e1ea81d97048b0e36b9b58af038d82f85ce3be: architecture review and planningreview, each on the identical SHA.
deno task verify:cleanwas run on this committed candidate before this PRwas opened: exit 0 in 366 s, a clean checkout of
cf0678bf, with trackedfiles,
node_modulesanddeno.lockunchanged.deno check --frozenandpnpm run lintboth exit 0. The integrated parent'sgroup runs (F1 22/199, F2 40/120, F3 47/244, F4 50/258) stand for the owners
this head does not change.
Scope confirmation