Skip to content

✨ Validate a supplied document without executing it - #654

Merged
taras merged 6 commits into
mainfrom
agent/issue-653-document-validation
Aug 30, 2026
Merged

✨ Validate a supplied document without executing it#654
taras merged 6 commits into
mainfrom
agent/issue-653-document-validation

Conversation

@taras

@taras taras commented Aug 30, 2026

Copy link
Copy Markdown
Owner

Closes #653.

Why

xmd prompt (#260) has to reject a generated program with a missing component
or an invalid invocation before asking a person to approve it. Today those
failures surface only when execution begins. Core's inspection reads the root
and validates its metadata, but resolves nothing in the body.

This adds one reusable core boundary so the CLI, and any other host, reads one
versioned answer instead of reproducing expansion's rules.

What changes

Before:

  • A document's component failures were discoverable only by running it.
  • inspectDocument() answered for the root's declaration and stopped there.

After:

  • validateDocument() reads a supplied root and the Markdown source closure
    ordinary component selection discovers, and returns deterministic version-1
    data: one closed code per diagnostic, the normalized Ajv issues execution
    already produces, and one record per authored invocation reporting valid,
    invalid, or not-statically-checkable.
  • Nothing in the document runs to produce that answer.
const result = yield* validateDocument({
  ...inlineSource(source),
  props: {},
  includes: ["components", "."],
  components: hostDeclarations,
});

How it works

validateDocument(options)
  → admit host declarations, merge the declaration-only registry
  → read + parse the selected root projection
  → FIFO walk: scan each source in order, selecting every authored name
  → per site: resolution, form, capture, return usage, structural rules, props
  → sort diagnostics once, rewrite tokens into indexes
  → DocumentValidation

Traversal follows source, not execution reachability. An invocation inside
<If>, <Loop> or <Each> is authored program structure and is checked like
any other; no branch is evaluated, and no projection is predicted. Discovered
definitions enter one FIFO queue in first-invocation order.

Reading and parsing are separate concerns. A path's bytes are read once,
whatever views the walk needs. The full-definition view — what an ordinary
component selection asks for — is cached per path. An untargeted root's body is
the whole file, so it registers as that view and a name resolving there finds it
already scanned; that is how a root invoking itself terminates. A targeted
root registers no view: its body is one section and stands in for nothing, so a
component selecting the same path parses the full definition from the bytes
already read.

Full props-schema validation runs only when every schema-visible prop is static.
A missing required key is still definite beside a dynamic prop, because presence
needs no value; every other schema conclusion waits for the one full-schema
call. No partial JSON Schema solving.

Review guide

Start with: specs/executable-mdx-spec.md §5.3 "Validating a supplied
document: validateDocument()" — the whole contract in one place.

Then review:

  1. packages/core/src/document-validation.ts — the public types, then
    ValidationState.run, #loadSource, #visit, #checkContract, finish.
  2. packages/core/src/body-structure.ts and structural-rules.ts — the rules
    extracted out of expand.ts so both callers read one catalog.
  3. packages/core/src/expand.ts — the same rules, now consumed rather than
    owned. Messages are byte-identical.
  4. packages/core/src/definition.ts / frontmatter.ts — phased parsing.

Look carefully at:

  • finish() — the sort is the public ordering contract: source ordinal,
    unpositioned before positioned, authored offset, closed code order, then
    discovery as the non-public tie breaker.
  • #deferred — a fact one construct discovers about a descendant (a misplaced
    <Else> found through its <If>) reaches both records.
  • The import list at the top of document-validation.ts. It is the enforceable
    no-execution seam: no execute.ts, no expandComponent, no
    importComponent, no journal, command, eval, Agent, Elicitation or provider.

What must stay true

  • Nothing the document authored runs. Enforced by the validator's import
    list and by taking no stream or installation; checked by DV13, which
    installs a refusal at every API.Fs write, API.Process, API.Fetch and
    API.Env.compile, an identity factory that throws, and a registered
    component whose body throws.
  • One parser, one rule catalog. Enforced by extraction rather than
    duplication — expand.ts calls the same functions; checked by DV14 and by
    the existing structural suites, which are unchanged.
  • One read per path. Enforced by the content cache; checked by DV5.
  • A version-1 answer is deterministic. Enforced by sorting once at the end
    over tokens rather than assigning indexes during the walk; checked by DV11.

How to verify it

  • DV1/DV2 prove the two acceptance cases from the issue and fail if
    resolution or schema validation regresses.
  • DV5 proves root-then-FIFO order, one read per path, cycle termination, and
    that a targeted root's projection never answers for the full definition a
    component selected. It fails if either cache is keyed wrongly.
  • DV7/DV8 prove that opacity never swallows a definite failure and that no
    partial schema conclusion is drawn across a mixed static/dynamic object.
  • DV9 proves a repository .ts component is never imported and never given
    an invented contract.
  • DV13 fails if any execution, provider, journal, command, elicitation, agent
    or document-authored write occurs.
deno task test packages/core/tests/document-validation.test.ts
deno task test packages/core/

Scope

Included

  • validateDocument() and its version-1 types, exported from
    @executablemd/core.
  • Three extractions so validation and expansion share one rule catalog:
    body-structure.ts, structural-rules.ts, components/declared-registry.ts.
  • Phase-aware parsing in definition.ts and frontmatter.ts.
  • architecture.md and specs/executable-mdx-spec.md.

Intentionally unchanged

  • No xmd prompt CLI, ACP generation, repair, approval or execute flow — those
    are Add xmd prompt: generate an executable markdown program from a request, approve it, run it #260's.
  • No safe-component subset or generated-program admission policy.
  • No control-flow reachability or child-projection prediction.
  • No stricter MDX grammar, spread rule or component-name grammar. A spread keeps
    exactly the meaning execution's scanner gives it.
  • Expansion's first-refusal behavior. It still stops at a construct's first
    violation; validation reads all of that construct's shared facts, because an
    author reading a result is owed all of them at once.
  • component-ambiguous is deliberately dormant: main's precedence always
    selects exactly one component, so the code and its rank exist for a selector
    that can one day answer otherwise. Ambiguity is not manufactured from
    precedence tiers, include order, or a refused host declaration.

New abstractions

  • body-structure.ts exists because the <Output>/<Return> body contract has
    two consumers with different needs: expansion wants the one aggregate printed
    error it always produced (renderBodyStructure()), validation wants each
    violation separately under its own code. One walk, two renderings.
  • structural-rules.ts exists for the same reason across every construct, and
    is what keeps validation from owning a second rule catalog.
  • components/declared-registry.ts exists because inspectSyntax() and
    validateDocument() must resolve declared names identically; it was private
    to inspect.ts.
  • DefinitionPhaseError / FrontmatterPhaseError exist so a failure's phase is
    known without matching its message. The raising entry points re-throw
    original, so execution and inspection prose are unchanged.
  • Each new abstraction has multiple concrete uses or a clear justification.
  • No speculative functionality is included.

Risks and limitations

  • invocation-form-invalid carries engine prose rather than the component's own
    refuse() sentence, because calling refuse would execute component code.
    The declared forms are the no-execution answer.
  • Validation reports every independent static structural failure while expansion
    reports the first. That is deliberate and documented, but it means the two
    surfaces show different counts for one malformed construct.
  • The extraction touches expand.ts broadly (−699 lines). Messages are
    byte-identical and the existing structural suites are unchanged, which is the
    evidence for that.

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.

taras added 4 commits August 29, 2026 19:50
Add `validateDocument()`: one core operation that reads a supplied root and
the Markdown source closure ordinary component selection discovers, and
answers with deterministic version-1 data — one closed code per diagnostic,
the normalized schema issues execution already produces, and one record per
authored invocation saying whether it is valid, invalid, or not decidable
without running the document.

Traversal follows source rather than execution reachability: the selected
root projection first, then definitions in FIFO discovery order, each source
identity read once, so a cycle terminates and is not a failure. An invocation
inside authored control flow is checked like any other, and no branch is
evaluated to decide whether it would run.

The rules come from execution rather than a second catalog. The body
output/return contract moves to `body-structure.ts`, and the static parts of
every structural construct move to `structural-rules.ts`; expansion renders
the same facts as the printed errors it always produced. Frontmatter and
definition parsing report the phase a failure belongs to, so nothing
classifies an error by reading its message. The declaration-only registry
inspection built is now shared.

Observation performs no document effect: no expression is evaluated, no
component invoked, no repository module imported, no identity factory called,
no provider installed, no journal opened, and nothing the document authored is
written.
…cord (#653)

Two version-1 contracts were wrong.

The root did not enter the source cache, so a root living where a component
name resolves was scanned twice: a retained root at `components/Foo.md`
containing `<Foo />` returned the invocation names `["Foo", "Foo"]` and read
the file again. The root now registers under its own path, so a selection
finding it terminates the walk the way any other cycle does and reads nothing.

A structural fact a parent discovered about a descendant went to the parent
alone. A misplaced `<Else>` found through its `<If>` named `If`, and the
`<Else>` it was about stayed valid. Such a finding now names and is positioned
at the element it identifies, reaches that element's record, and still leaves
the `<If>` whose structure is malformed invalid. Preorder guarantees the parent
is visited first, so the child picks it up rather than losing it.

An `<If>` with an unknown prop also returned early, hiding the missing
condition and a malformed `<Else>` beside it. Every independent static check
now runs. Expansion's first-refusal behavior is unchanged: it stops at the
first violation over the same shared facts, and validation reads all of them.

Each regression was confirmed discriminating by reverting its fix.
… selected (#653)

One path is bytes; what is parsed from them depends on who asked. Caching the
root under its path conflated the two, so a targeted root's projection stood in
for the full Markdown definition an ordinary component selection asks for — and
a `<Foo />` in the selected section returned `valid` while the sections the root
never selected held a definite `body-shape-invalid` failure.

Reading and parsing are now separate. A path is read once, whatever views the
walk needs; the full-definition view is registered per path. An untargeted
root's body is the whole file, so it registers as that view and a name resolving
there finds it already scanned — the self-cycle deduplication is unchanged. A
targeted root registers no view: a component selecting that path parses the full
definition from the bytes already read and enters the FIFO queue in discovery
order like any other source.

Both halves of the regression were confirmed discriminating: substituting the
projection again, and bypassing the content cache so the second view re-reads.

@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 3 redundant comments. Inline suggestions to remove them below.

this.#draft(ordinal, "source-unreadable", { message: unreadable(path), cause: error });
return undefined;
}
// Supplied or read, these are this path's bytes for the rest of the walk.

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
// Supplied or read, these are this path's bytes for the rest of the walk.

for (const violation of this.#structuralViolations(segment, context)) {
// A violation names the construct it is about and, when the check
// walked past the element it was given, the element it is about. Both
// the diagnostic and the record that points at it follow that element.

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
// the diagnostic and the record that points at it follow that element.

}

// Presence, not the resolved value: `value={undefined}` names the direct
// source exactly as `value={42}` does, and a whitespace child is a body.

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
// source exactly as `value={42}` does, and a whitespace child is a body.

@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 16 redundant comments. Inline suggestions to remove them below.

Comment thread packages/core/mod.ts
} from "./src/inspect.ts";
export { ComponentIncludeError } from "./src/components/candidates.ts";
// Document validation — one supplied document read as authored program
// structure, with nothing in it executed.

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
// structure, with nothing in it executed.

this.#draft(ordinal, "source-unreadable", { message: unreadable(path), cause: error });
return undefined;
}
// Supplied or read, these are this path's bytes for the rest of the walk.

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
// Supplied or read, these are this path's bytes for the rest of the walk.

for (const violation of this.#structuralViolations(segment, context)) {
// A violation names the construct it is about and, when the check
// walked past the element it was given, the element it is about. Both
// the diagnostic and the record that points at it follow that element.

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
// the diagnostic and the record that points at it follow that element.

// Its contract lives on the module's exports, and reading them would run
// the module's top-level code. Nothing is assumed in its place: no forms,
// no props schema, no captures, no return mode. Only the engine-owned
// checks above applied, and what is left is unknown rather than accepted.

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
// checks above applied, and what is left is unknown rather than accepted.

const source = yield* this.#loadSource(segment.name, selected.path);
if (source.state === "failed") {
// Every invocation that selected this definition points at the one
// failure the source has, and none of them invents a second.

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
// failure the source has, and none of them invents a second.

}
// Both remaining outcomes resolved, so both report an origin. A record
// reaching here without one would mean selection answered and nothing
// wrote down what it answered.

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
// wrote down what it answered.

// Every one of these is decided from what the author wrote, so the whole
// catalog lives in `structural-rules.ts` where validation reads it too. The
// first is reported and the rest of the construct does not run, which is what
// a `<Let>` whose declaration is wrong has always done.

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
// a `<Let>` whose declaration is wrong has always done.

return [yield* raise(eachError(`Prop "as" on <Each /> ${asResult.error.message}`))];
// Decided from source alone, so the catalog is shared with validation. A
// literal `in` is checked here too; an expression is a value the document
// computes, and its answer is checked below where it arrives.

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
// computes, and its answer is checked below where it arrives.

(name) => !IF_PROPS.has(name),
);
// Decided from source alone and shared with validation: which props were
// written, and how the body splits at its `<Else>`.

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, and how the body splits at its `<Else>`.

}

// Presence, not the resolved value: `value={undefined}` names the direct
// source exactly as `value={42}` does, and a whitespace child is a body.

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
// source exactly as `value={42}` does, and a whitespace child is a body.

@github-actions

github-actions Bot commented Aug 30, 2026

Copy link
Copy Markdown

PR #654: ✨ Validate a supplied document without executing it

14 files, +4350 / -799

Scope

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

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

Structural

Oxlint structural signals:

  • no-unused-vars ×7: packages/core/src/expand.ts
  • no-unnecessary-type-assertion ×2: packages/core/src/definition.ts, packages/core/src/expand.ts

Slop

  • packages/core/src/expand.ts:1411 (removed)
  • packages/core/src/expand.ts:1553 (removed)
  • packages/core/src/expand.ts:1706 (removed)
  • packages/core/src/expand.ts:1411 (removed)
  • packages/core/src/expand.ts:1553 (removed)
  • packages/core/src/expand.ts:1706 (removed)
  • packages/core/src/document-validation.ts:720// the diagnostic and the record that points at it follow that element.
  • packages/core/src/document-validation.ts:871// A site whose as was already refused is not asked again.
  • packages/core/src/document-validation.ts:811// checks above applied, and what is left is unknown rather than accepted.
  • packages/core/src/document-validation.ts:948// an author reading a validation result is owed all of them.
  • packages/core/src/document-validation.ts:1007// source's own facts already stated.
  • packages/core/src/document-validation.ts:1088// wrote down what it answered.
  • packages/core/src/expand.ts:1417// a whose declaration is wrong has always done.
  • packages/core/src/structural-rules.ts:123// source exactly as value={42} does, and a whitespace child is a body.

Oxlint slop signals:

  • no-inferrable-types ×2: packages/core/src/expand.ts

Static Analysis

Oxlint: 31 diagnostics across 4 files (13 rules)
Density: 0.007 violations/added-line

no-unused-vars (7): packages/core/src/expand.ts
no-base-to-string (4): packages/core/src/expand.ts
no-array-sort (3): packages/core/src/document-validation.ts, packages/core/src/inspect.ts
no-unsafe-type-assertion (3): packages/core/src/expand.ts
no-inferrable-types (2): packages/core/src/expand.ts
consistent-return (2): packages/core/src/inspect.ts, packages/core/src/document-validation.ts
no-misused-spread (2): packages/core/src/inspect.ts
no-unnecessary-type-assertion (2): packages/core/src/definition.ts, packages/core/src/expand.ts
unbound-method (2): packages/core/src/expand.ts
no-shadow (1): packages/core/src/expand.ts
no-new-array (1): packages/core/src/definition.ts
restrict-template-expressions (1): packages/core/src/expand.ts
no-implied-eval (1): packages/core/src/expand.ts

Correctness

No extraneous code patterns detected.

`Array.prototype.toSorted` is ES2023 and `tsconfig.node.json` targets ES2022,
so every `test-node` shard failed at its typecheck before running a test.
Deno's own lib is newer, which is why `deno check` and the whole local suite
were green.

Both call sites already build a fresh array, so sorting in place mutates
nothing anyone else holds. That reinstates an oxlint `no-array-sort` warning —
the shape the rest of the repository is in, and a warning rather than the
error this was.

Verified with the check CI runs: `tsc --project tsconfig.node.json --noEmit`,
plus the validation suite under all three runtimes.

@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 14 redundant comments. Inline suggestions to remove them below.

this.#draft(ordinal, "source-unreadable", { message: unreadable(path), cause: error });
return undefined;
}
// Supplied or read, these are this path's bytes for the rest of the walk.

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
// Supplied or read, these are this path's bytes for the rest of the walk.

for (const violation of this.#structuralViolations(segment, context)) {
// A violation names the construct it is about and, when the check
// walked past the element it was given, the element it is about. Both
// the diagnostic and the record that points at it follow that element.

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
// the diagnostic and the record that points at it follow that element.

}

// Engine-owned and independent of any contract: `as` names a binding, and
// whether the author wrote a name at all is decided from the syntax.

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
// whether the author wrote a name at all is decided from the syntax.

const source = yield* this.#loadSource(segment.name, selected.path);
if (source.state === "failed") {
// Every invocation that selected this definition points at the one
// failure the source has, and none of them invents a second.

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
// failure the source has, and none of them invents a second.

return;
}
// A definition whose own body contract is broken is broken for every
// caller, at the position its own source states.

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, at the position its own source states.

}
// Both remaining outcomes resolved, so both report an origin. A record
// reaching here without one would mean selection answered and nothing
// wrote down what it answered.

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
// wrote down what it answered.

// Every one of these is decided from what the author wrote, so the whole
// catalog lives in `structural-rules.ts` where validation reads it too. The
// first is reported and the rest of the construct does not run, which is what
// a `<Let>` whose declaration is wrong has always done.

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
// a `<Let>` whose declaration is wrong has always done.

return [yield* raise(eachError(`Prop "as" on <Each /> ${asResult.error.message}`))];
// Decided from source alone, so the catalog is shared with validation. A
// literal `in` is checked here too; an expression is a value the document
// computes, and its answer is checked below where it arrives.

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
// computes, and its answer is checked below where it arrives.

(name) => !IF_PROPS.has(name),
);
// Decided from source alone and shared with validation: which props were
// written, and how the body splits at its `<Else>`.

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, and how the body splits at its `<Else>`.

}

// Presence, not the resolved value: `value={undefined}` names the direct
// source exactly as `value={42}` does, and a whitespace child is a body.

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
// source exactly as `value={42}` does, and a whitespace child is a body.

Validation still reimplemented four authored decisions expansion owns: an
ordinary component's `as` form and binding name, the requirement that a
component declaring `returns` is invoked with `as`, and the `<Answers>` and
`<Answer>` prop, delegate, body-shape, template-form and required-value rules.
Sharing the sentences was not sharing the decisions.

New `invocation-rules.ts` owns the ordinary-component rules, and the `<Answers>`
and `<Answer>` rules in `structural-rules.ts` split into the phases their
callers ask in. Extraction is phase-appropriate rather than phase-bound: whether
`as` was written as an expression is answered from the scanned element, while
what a binding name may be is answered from whatever value each caller has —
expansion passes the prop it resolved, validation passes the literal the author
wrote. Expansion refuses at the first of them, in the phase it always did, with
byte-identical prose; validation aggregates every independent one.

The regressions run each document twice — once through `execute()`, once
through `validateDocument()` — and require one sentence from both. Each was
confirmed discriminating by giving expansion back a private copy of the rule.

The source/view contract is also corrected everywhere it was stated. Bytes are
read once per path; each semantic view of them is parsed and scanned once. An
untargeted root is that path's full-definition view as well. A targeted root's
projection and a component-selected full definition are two distinct views of
one path. The claim that a source identity is parsed or scanned once is gone
from architecture.md, the spec, the public comments and the tests.

@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 8 redundant comments. Inline suggestions to remove them below.

for (const violation of this.#structuralViolations(segment, context)) {
// A violation names the construct it is about and, when the check
// walked past the element it was given, the element it is about. Both
// the diagnostic and the record that points at it follow that element.

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
// the diagnostic and the record that points at it follow that element.

}

// The same rule expansion applies, asked about the capture this site names.
// A site whose `as` was already refused is not asked again.

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
// A site whose `as` was already refused is not asked again.

// Its contract lives on the module's exports, and reading them would run
// the module's top-level code. Nothing is assumed in its place: no forms,
// no props schema, no captures, no return mode. Only the engine-owned
// checks above applied, and what is left is unknown rather than accepted.

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
// checks above applied, and what is left is unknown rather than accepted.

// Every one of these is decided from source alone, so every one of them
// is reported. Expansion stops at the first and expands neither branch,
// which is its own established behavior over the same shared facts —
// an author reading a validation result is owed all of them.

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
// an author reading a validation result is owed all of them.

default:
// `<Return>` is the remaining case, and its whole contract belongs to
// the body that declares — or fails to declare — `returns`, which the
// source's own facts already stated.

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
// source's own facts already stated.

}
// Both remaining outcomes resolved, so both report an origin. A record
// reaching here without one would mean selection answered and nothing
// wrote down what it answered.

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
// wrote down what it answered.

// Every one of these is decided from what the author wrote, so the whole
// catalog lives in `structural-rules.ts` where validation reads it too. The
// first is reported and the rest of the construct does not run, which is what
// a `<Let>` whose declaration is wrong has always done.

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
// a `<Let>` whose declaration is wrong has always done.

}

// Presence, not the resolved value: `value={undefined}` names the direct
// source exactly as `value={42}` does, and a whitespace child is a body.

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
// source exactly as `value={42}` does, and a whitespace child is a body.

@taras
taras marked this pull request as ready for review August 30, 2026 01:30
@taras
taras merged commit a6e8bb7 into main Aug 30, 2026
30 checks passed
@taras
taras deleted the agent/issue-653-document-validation branch August 30, 2026 01:30
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.

Validate supplied document structure without executing it

1 participant