Skip to content

🧩 Admit every built-in construct to generated XMD (#762) - #790

Merged
taras merged 2 commits into
mainfrom
agent/issue-762-constructs
Sep 9, 2026
Merged

🧩 Admit every built-in construct to generated XMD (#762)#790
taras merged 2 commits into
mainfrom
agent/issue-762-constructs

Conversation

@taras

@taras taras commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Why

An information request has to be able to decide something — read a file only
if a path matched, render one shape or another, walk a list. A generated
fragment could name an admitted component and nothing else, so none of that was
expressible.

This is PR 2 of the three-PR stack for #762, on top of the merged PR 1
(#789). It adds the language; the Plan classifier and information loop are PR 3.

What changes

Before:

  • <If condition={true}><File path="a.md" /></If> was refused, and the refusal
    said the host did not admit <If>.

After:

  • Every built-in construct is available inside generated evaluation with its
    ordinary semantics: Content, Output, Return, Let, Each, If,
    Else, Switch, Case, Loop, Break, PrintErrors, Answers, Answer.
  • A construct written wrongly fails with its own structural rule, not as
    withheld authority.
<Glob include={["**/AGENTS.md"]} as="paths" />
<If condition={paths}>
  <File path="AGENTS.md" as="guide" />
  <Json value={{ paths, guide }} />
  <Else>
    <Json value={[]} />
  </Else>
</If>

How it works

scanSegments → walk() → RESERVED_STRUCTURAL? → structural() → structural-rules.ts
                     ↘ otherwise → admitted table

The load-bearing discovery: constructs are not a distinct segment kind.
<If> is a component segment whose name is reserved, and it was refused only
because table.get("If") misses. So the dispatch goes at the top of the
component case, before that lookup — mirroring expand.ts, which checks each
structural name before resolving a component. Any later and a construct is
reported as a component the host withheld, which is a different and misleading
thing to tell a candidate.

The rules themselves come from structural-rules.ts — the module ordinary
validation and expansion already read — so a construct means one thing whether a
person or an Agent wrote it. What differs is only when: every branch, every
<Case>, every body and every path the run will not take is walked before the
fragment's first effect.

Review guide

Start with: packages/core/src/generated-xmd.tsstructural()

Then review:

  1. the dispatch at the top of walk()'s "component" case
  2. alternatives() — the incoming-snapshot rule for <If>/<Switch>
  3. the <Each> arm of structural() — the fresh body scope
  4. transparent(), and the ROOT_LEXICAL reset on component children

Look carefully at:

  • Binding ownership per region. This is the part with real consequences and
    it is deliberately not uniform. See the table under What must stay true.
  • <Answers>. Its <Answer> children are matchers; everything else is the
    body region they answer for. Both are walked. An earlier revision of this
    branch refused the body outright, which would have refused every legitimate
    <Answers>.
  • What stays at runtime. Conditions, matchers, a computed max and a
    failing read are still evaluated where they always were.

What must stay true

Region Rule Why
component children do not escape a body is not a sibling
<Each> body fresh scope of enclosing + item; nothing escapes the body may run no times, so a binding it makes cannot be promised
<Each as> joins the enclosing scope it is the capture the construct was asked for
<If> arms, <Switch> <Case>s each checked from one incoming snapshot they are alternatives; one must not supply a binding to another
after those constructs union of what the alternatives can produce keeps "read a binding only one arm makes" a runtime failure, not a preflight refusal
<Loop>, <PrintErrors>, <Let> bodies read and write the enclosing environment transparent regions
a component body inside a <Loop> lexical facts reset a <Break> there cannot break the enclosing loop

Also unchanged, and checked by the existing GX tiers: the durable record shape
and version, constructs staying out of the retained named-component list, and
every effectful component reached through a construct staying held to the
selected authority.

How to verify it

deno task test packages/core/tests/generated-composition.test.ts \
  packages/core/tests/generated-xmd.test.ts \
  packages/core/tests/evaluate-component.test.ts
Command Result
the three focused files 32 passed (291 steps) | 0 failed
deno task lint 0
deno task check 0
git diff --check 0

What the new rows prove, and the defect each rejects:

  • FE35: branching, binding and bounded iteration compose with admitted reads — the positive path. Fails if a construct falls through to the
    generic structural refusal.
  • FE35: a prohibited component in an untaken branch refuses at zero reads
    the discriminating row. The taken arm holds an admitted read; the untaken arm
    holds a write. files.performed must be [], so preflight that walked only
    the selected branch fails here. Each refusal asserts its reason, so a
    refusal for an unrelated cause does not pass.
  • FE35: a body binding does not escape, and alternatives cannot feed each other — three fragments that were all admitted before the latest
    revision: an <Each> body binding read afterwards, an <Else> reading the
    true arm's binding, and a <Case default> reading an earlier <Case>'s.
  • FE35: Loop, a valid Break and PrintErrors run around an admitted read
    proves those three reach their own handlers rather than a generic refusal.
  • FE35: every nested body is preflighted — a prohibited component inside a
    paired <Answer>'s template children, and one in the ordinary <Answers>
    body. Both were holes in an earlier revision of this branch.
  • FE35: executable code and imports stay refused inside a construct — a
    construct is not a way in.

Scope

Included

  • the structural dispatch and structural() walker in generated-xmd.ts
  • the structure refusal class, distinct from component
  • alternatives(), transparent(), Lexical, stated()
  • §5.3.3 normative text, the FE35 acceptance row, and the architecture
    paragraphs

Intentionally unchanged

  • The durable record. Shape, version, and the retained named-component list
    are untouched; constructs keep their exact spelling in retained source.
  • Product behavior. Nothing about xmd run or xmd plan changes.
  • The profile. PR 1's read table is untouched; allow still selects effect
    tables only.
  • The Plan classifier and information loop (PR 3).

New abstractions

  • structural() exists because the reserved names need a dispatch point before
    the admitted table; one consumer, walk().
  • alternatives() exists because <If> and <Switch> share one rule that
    differs from a transparent region's; two consumers.
  • Lexical carries the single lexical fact a rule consults (insideLoop).
  • stated() replaced three inline copies of the two-brace-reading validation.

Risks and limitations

  • Binding ownership is a judgment surface. The per-region rules above are
    the accepted contract as revised, and the three zero-effect regressions pin
    them. Worth confirming they are the intended semantics rather than assuming.
  • Two existing rows stated the contract this replaces and were retargeted,
    not weakened. GX4 expected a structural construct to be refused as a
    component the host did not admit; it now expects the structural refusal, with
    rows added for a construct the generated root gives no context for, a stray
    branch and a stray break. FE6 listed a well-formed <If> among its
    refusals — precisely what this admits — and now uses <Content /> and a
    stray <Else>.
  • <Answer> template children are walked, so a template containing text
    that scans as a component is refused rather than treated as literal template
    text. Fail-closed, and the alternative risks a real hole.

Scope confirmation

  • Every changed file supports the purpose described above.
  • Unrelated cleanup and formatting changes are excluded.
  • Generated or mechanical changes are clearly identified.
  • The description matches the final diff and test results.

A generated fragment could name an admitted component and nothing else. Asking
one to gather information means letting it branch, bind and iterate, so every
built-in structural construct is now available inside generated evaluation with
its ordinary semantics: Content, Output, Return, Let, Each, If, Else, Switch,
Case, Loop, Break, PrintErrors, Answers and Answer.

Constructs are not a distinct segment kind. `<If>` is a component segment whose
name is reserved, and it was refused only because the admitted table misses it.
So preflight now dispatches a reserved name to the structural rules at the top
of the component case, before that lookup — mirroring expansion, which checks
each structural name before it resolves a component. That ordering is what makes
a construct fail as the source mistake it is rather than as a component the host
withheld, and the two are different things to tell a candidate: a new refusal
class says the language does not allow the construct where it was written.

The rules come from `structural-rules.ts` rather than a second copy, so a
construct means one thing whether a person or an Agent wrote it. Preflight walks
every branch, every Case, every iteration body, every Answer template and the
Answers body before the first effect, so a prohibited component in a path the
run never takes refuses the whole fragment with no read performed. What a value
decides stays at runtime: conditions, matchers, a computed `max` and a failing
read are evaluated where they always were.

Binding ownership is stated per region rather than shared by all of them. An
`<Each>` body is a fresh scope holding the enclosing bindings and the item, and
nothing it binds survives it, because that body may run no times at all — only
`<Each as>` joins the enclosing scope. The alternatives of an `<If>` and the
`<Case>` branches of a `<Switch>` are mutually exclusive, so every arm, matcher
and branch body is checked from one incoming snapshot and no alternative can
supply a binding to another; the union of what they can produce becomes visible
after the construct, which keeps reading a binding only one arm makes a runtime
failure rather than a preflight refusal. `<Loop>`, `<PrintErrors>` and `<Let>`
bodies read and write the enclosing environment. A component body resets the
lexical facts, so a `<Break>` written there cannot break an enclosing loop, and
a component's own `as` is visible only to later siblings.

Nothing durable moves. Constructs keep their exact spelling in the retained
source, never join the retained named-component list, and the record version is
unchanged. No product behavior changes.

Two existing rows stated the contract this replaces and were retargeted rather
than weakened. GX4 expected a structural construct to be refused as a component
the host did not admit; it now expects the structural refusal, with rows added
for a construct the generated root gives no context for, a stray branch and a
stray break. FE6 listed a well-formed `<If>` among the refusals, which is
precisely what this admits; it now uses `<Content />` and a stray `<Else>`.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 4 redundant comments. Inline suggestions to remove them below.

}

// The construct's own props, over the bindings in effect where it was
// written.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// written.

// Its `<Answer>` children are the matchers; everything else is the region
// whose elicitations they answer. Both are walked — the matchers because
// which one is chosen is a runtime question, and the body because it is
// ordinary segments.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// ordinary segments.

refuseStructure(letViolations(segment));
yield* transparent(segment.children, table, ceilings, named, scope, lexical);
// After its own body, like a component's `as`: a `<Let>` cannot name the
// binding it is producing.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// binding it is producing.

// iteration names. Nothing the body binds survives it: the body runs once
// per item and may run no times at all, so a binding made inside it is not
// something a later sibling can be promised. What the construct offers its
// caller is the one capture it was asked for.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// caller is the one capture it was asked for.

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

PR #790: 🧩 Admit every built-in construct to generated XMD (#762)

6 files, +801 / -28

Scope

🔴 PR has 829 lines changed. Split into focused PRs.

🟡 829 lines changed. PRs under 400 receive more thorough review.

Structural

✅ No structural bloat detected.

Slop

  • packages/core/src/generated-xmd.ts:982 (removed)
  • packages/core/src/generated-xmd.ts:133// one thing whether a person wrote it or an Agent did.
  • packages/core/src/generated-xmd.ts:209// withheld something.
  • packages/core/src/generated-xmd.ts:1011// invocation below, from the engine's own account of the form.
  • packages/core/src/generated-xmd.ts:1034// refused here exactly as it always was.
  • packages/core/src/generated-xmd.ts:1935// that encloses the invocation.
  • packages/core/src/generated-xmd.ts:2047// written.
  • packages/core/src/generated-xmd.ts:2064// enters still refuses the whole fragment.
  • packages/core/src/generated-xmd.ts:2083// would ever reach it.
  • packages/core/src/generated-xmd.ts:2098// written where no loop ever was.
  • packages/core/src/generated-xmd.ts:2117// ordinary segments.
  • packages/core/src/generated-xmd.ts:2138// binding it is producing.
  • packages/core/src/generated-xmd.ts:2157// caller is the one capture it was asked for.

Static Analysis

Oxlint: 4 diagnostics across 1 file (1 rule)
Density: 0.005 violations/added-line

no-shadow (4): packages/core/src/generated-xmd.ts

Correctness

No extraneous code patterns detected.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 5 redundant comments. Inline suggestions to remove them below.

Comment thread packages/core/src/generated-xmd.ts Outdated
// take an untaken read's plan, and a second iteration of one element would
// find nothing left. Looking the element up by where it was written is what
// makes repetition, selection and an empty iteration all correct, and it
// consumes nothing, so an untaken element's plan is still simply unused.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// consumes nothing, so an untaken element's plan is still simply unused.

}

// The construct's own props, over the bindings in effect where it was
// written.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// written.

// Its `<Answer>` children are the matchers; everything else is the region
// whose elicitations they answer. Both are walked — the matchers because
// which one is chosen is a runtime question, and the body because it is
// ordinary segments.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// ordinary segments.

refuseStructure(letViolations(segment));
yield* transparent(segment.children, table, ceilings, named, scope, lexical);
// After its own body, like a component's `as`: a `<Let>` cannot name the
// binding it is producing.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// binding it is producing.

// iteration names. Nothing the body binds survives it: the body runs once
// per item and may run no times at all, so a binding made inside it is not
// something a later sibling can be promised. What the construct offers its
// caller is the one capture it was asked for.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// caller is the one capture it was asked for.

Whole-fragment preflight reads every alternative and every authored body once.
The run does neither: an `<If>` or a `<Switch>` enters one alternative, and an
`<Each>` or a `<Loop>` may enter one authored element many times or not at all.
Runtime authorization was a per-name queue that `shift()`ed once per import, so
it assumed a cardinality and an order the run does not have.

Two consequences, both wrong for the same reason. An untaken self-closing
`<File />` written before a selected paired `<File>` write handed that write the
read's admission, which then failed the form check. And a two-item `<Each>`
around one `<File />` exhausted the queue after the first iteration, so the
second could not import File at all.

Admissions are now held by name *and* authored form, and nothing is consumed. A
name and a form select exactly one admission because the table already says so:
`admitted()` refuses one name holding two definitions, and refuses one name and
form twice. Which admission an invocation runs under is decided per invocation
from the engine's own issuance — a wrapper can mint an object carrying a
`hasContent` method and cannot mint an issuance — so an untaken alternative and
a zero-iteration body spend nothing, a repeated invocation of one element finds
its own identity and form, and an element whose form the host admitted for no
entry is refused with the diagnostic a mismatched invocation always produced.

The obvious alternative — keying by the element's authored position — is wrong,
and CI proved it. `Component.importComponent` middleware may honestly delegate
with `next(name)`, which drops the position, and the generated import then
matched nothing: WGAC7's observing handler and FE24's honest delegation both
failed. Authorization may not depend on a value a legitimate handler is free to
omit. The form arrives on the engine's issuance instead, which no handler
composes around.

The record is untouched. Identities and forms are retained exactly as before,
and preflight is unchanged: every alternative and nested body is still validated
before the first effect, so a prohibited component in a path the run never takes
still refuses the whole fragment with no effect performed.

`packages/cli/tests/plan-component.test.ts` FE9 used a `<Loop>` around an
admitted read as an example of text a fragment may not contain, which is what
this stack admits. It now uses an executable fence after an admitted read, so
the empty recorder still proves whole-fragment preflight.

The comment claiming imports happen once per element and in walk order is
replaced, because structural repetition makes both halves false.
@taras
taras force-pushed the agent/issue-762-constructs branch from 402fccf to d32141c Compare September 9, 2026 05:24

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 12 redundant comments. Inline suggestions to remove them below.

import { sourceDescription } from "./source-position.ts";
import { RESERVED_STRUCTURAL } from "./structural.ts";
// The ordinary source rules, read rather than reimplemented: a construct means
// one thing whether a person wrote it or an Agent did.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// one thing whether a person wrote it or an Agent did.

// structural construct is language rather than authority: writing one badly,
// or writing one where the generated root supplies no context for it, is a
// source error the candidate can correct — not a statement that the host
// withheld something.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// withheld something.

// Every entry under one name shares one definition — the table refuses two
// — so the implementation is the same whichever form the element turns out
// to have been written as. Which *admission* it runs under is decided per
// invocation below, from the engine's own account of the form.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// invocation below, from the engine's own account of the form.

// times and two elements of one name may be written in two forms. The
// admission is selected by the form the engine issued for *this*
// element, and an element whose form the host admitted for no entry is
// refused here exactly as it always was.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// refused here exactly as it always was.

// nothing they bind escapes back to its siblings. They are a component
// body rather than a transparent region, so the lexical facts reset:
// a `<Break>` written in a component's own body cannot break a loop
// that encloses the invocation.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// that encloses the invocation.

// Each branch is an alternative to the others, so each is checked from the
// bindings that reach the `<Switch>` — including its matcher, which is the
// branch's own expression and is validated whether or not the comparison
// would ever reach it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// would ever reach it.

if (name === "Loop") {
refuseStructure(loopViolations(segment));
// Inside for the body alone: a `<Break>` after the loop is as stray as one
// written where no loop ever was.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// written where no loop ever was.

// Its `<Answer>` children are the matchers; everything else is the region
// whose elicitations they answer. Both are walked — the matchers because
// which one is chosen is a runtime question, and the body because it is
// ordinary segments.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// ordinary segments.

refuseStructure(letViolations(segment));
yield* transparent(segment.children, table, ceilings, named, scope, lexical);
// After its own body, like a component's `as`: a `<Let>` cannot name the
// binding it is producing.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// binding it is producing.

// iteration names. Nothing the body binds survives it: the body runs once
// per item and may run no times at all, so a binding made inside it is not
// something a later sibling can be promised. What the construct offers its
// caller is the one capture it was asked for.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// caller is the one capture it was asked for.

@taras
taras merged commit 03a2168 into main Sep 9, 2026
38 checks passed
@taras
taras deleted the agent/issue-762-constructs branch September 9, 2026 05:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant