Skip to content

💥 feat(core): concise input declarations - #175

Merged
taras merged 2 commits into
mainfrom
feat/concise-inputs
Jul 27, 2026
Merged

💥 feat(core): concise input declarations#175
taras merged 2 commits into
mainfrom
feat/concise-inputs

Conversation

@taras

@taras taras commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Why

Closes #172.

A Markdown component that needs a few named inputs has to restate the enclosing object schema:

inputs:
  type: object
  properties:
    name: { type: string }
  required: [name]
  additionalProperties: false

The object shape is an implementation invariant, not information the author is choosing.

What changes

Before — the only spelling was the full schema above.

After — frontmatter also accepts a map of input names to draft-07 subschemas, with a top-level required array:

required: [name]

inputs:
  name:
    type: string
  loud:
    type: boolean
    default: false

Both spellings declare the same component. Property definitions remain draft-07 JSON Schema; only the enclosing closed object becomes implicit.

How it works

frontmatter → parseFrontmatter (normalize) → compileInputSchema → validateProps

Normalization happens during frontmatter parsing, which execution and inspection already share through parseMarkdownDefinition. Imported components, root documents, inspectDocument, and the generated --props-* options therefore all follow with no further changepackages/cli is untouched.

A type or $schema key selects the full schema, even when its value is malformed, so a broken schema is still diagnosed as one rather than read as a map of props named type. Two consequences: the map form cannot declare props named type or $schema (the full form declares them under properties like any other name), and the map form never carries $schema, because it is draft-07 by construction.

Review guide

Start with: packages/core/src/frontmatter.tsparseInputSchema and normalizeInputs.

Then review:

  1. packages/core/tests/frontmatter.test.ts — classification and normalization
  2. packages/core/tests/root-props.test.ts Tier RS — behavioral parity
  3. specs/executable-mdx-spec.md — the "Input definitions" rewrite

Look carefully at:

  • Normalization lives in frontmatter.ts, not compileInputSchema. A function component's inputs export never passes through frontmatter, so putting it in the compiler would silently extend the map form to TypeScript components.
  • The normalized schema is a fresh object per parse. compileInputSchema caches validators in a WeakMap keyed by schema identity, so a shared object would leak compiled state across definitions.

What must stay true

  • Existing full schemas are byte-identical after parsing — enforced by returning the parsed declaration unchanged, checked by the pre-existing passthrough test.
  • The map form is closed — enforced by normalizing to additionalProperties: false, checked by RS4.
  • Reserved slot/as are still rejected — enforced by enforceRootContract running after normalization, checked in validation-integration.test.ts.
  • Function components keep the full form — enforced by the .ts path not calling parseFrontmatter.

How to verify it

  • B16/B17 prove a map normalizes to the exact full form and that both spellings produce equal schemas; they fail if normalization drops required or additionalProperties.
  • RS2 runs both spellings end to end and compares output, proving required properties and recursive defaults behave identically.
  • RS6 proves an imported map-form component validates like the root.
  • RS7 proves inspectDocument returns the normalized schema, which is why the CLI needs no change.
  • The root-object contract tests pipe parseFrontmatter().inputs into compileInputSchema, asserting errors at the boundary that actually produces them.

Manual:

xmd run hello.md --help          # same --props-* options as the full-form twin
xmd run hello.md --props-name Ada

Scope

Included

  • Normalization in parseFrontmatter, reserving top-level required
  • Spec updates in executable-mdx-spec.md and root-document-inputs-spec.md

Intentionally unchanged

  • Function components keep the full inputs export; the map form is a frontmatter spelling.
  • No CLI changes — options are generated from the normalized schema.
  • Positional command-line arguments are Let root documents declare ordered positional arguments #173.
  • The 54 existing full-form components are left as they are; component-schema-conformance.test.ts walks them as the regression net.

Risks and limitations

This is a backward-incompatible frontmatter-language change. Every top-level key except inputs previously became metadata:

  • top-level required is now reserved for concise input declarations;
  • documents that used it as metadata must move it under meta.required;
  • inputs: {} changes from invalid to the empty concise declaration.

No document in this repository uses top-level required, but that only means this repository needs no migration — documents elsewhere may.

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 component that needs a few named inputs had to restate the enclosing
object schema, which is an implementation invariant rather than
something the author chooses.

Frontmatter now accepts a map of input names to draft-07 subschemas,
with a top-level `required` array naming the props a caller must supply:

    required: [name]

    inputs:
      name: { type: string }
      loud: { type: boolean, default: false }

Normalization wraps that in the closed object it implies, so everything
downstream keeps seeing one canonical draft-07 schema. It happens during
frontmatter parsing, which execution and inspection already share, so
imported components, root documents, `inspectDocument`, and the
generated `--props-*` options all follow with no further change.

A `type` or `$schema` key selects the full schema even when malformed,
so a broken schema is still diagnosed as one. Consequently the map form
cannot declare props named `type` or `$schema`; the full form declares
them under `properties` like any other name.

Top-level `required` is now reserved, so a document that used it as
metadata must move it under `meta.required`. `inputs: {}` changes from
invalid to the empty declaration.

@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.


// `type` and `$schema` mark a full schema even when their values are
// malformed, so a broken full schema is diagnosed as one rather than
// read as a map of properties named `type` or `$schema`.

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
// read as a map of properties named `type` or `$schema`.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Removed.

throw new Error('frontmatter "required" must list input names as strings');
}
// An inputs map is closed, so a name it does not declare could never be
// supplied and the schema would be impossible to satisfy.

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 and the schema would be impossible to satisfy.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Removed.

// `inputs` is the component's JSON Schema. Absent → the closed
// empty-object schema. A fresh object per component keeps the
// `inputs` is the component's JSON Schema, in either spelling. Absent →
// the closed empty-object schema. A fresh object per component keeps the

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 closed empty-object schema. A fresh object per component keeps the

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Removed.

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown

PR #175: 💥 feat(core): concise input declarations

6 files, +526 / -59

Scope

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

Structural

✅ No structural bloat detected.

Slop

✅ Slop indicators look low.

Static Analysis

✅ Oxlint found no issues.

Correctness

No extraneous code patterns detected.

The frontmatter pseudocode showed execution paths the implementation
rejects: it normalized without validating `required`, accepted any
property value, and merged a top-level `required` into a full schema.
It now expresses every rule the parser enforces.

Tier B ids match the tests that carry them: B16 normalization, B17
`required` entering the schema rather than the metadata, B18 the mixed
declaration. Concise/full parity is descriptive and claims no id.

RS6 asserts the diagnostic an omitted required property produces. A
component's prop failure is collected into the output rather than
aborting the run, so the error segment is the observable, not the
execution status — the same for both spellings.

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


// `type` and `$schema` mark a full schema even when their values are
// malformed, so a broken full schema is diagnosed as one rather than
// read as a map of properties named `type` or `$schema`.

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
// read as a map of properties named `type` or `$schema`.

@taras
taras merged commit 321b763 into main Jul 27, 2026
9 checks passed
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.

Allow concise input declarations for Markdown components

1 participant