💥 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

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: objectproperties:
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: stringloud:
type: booleandefault: 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 twinxmd 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-actionsgithub-actionsBot 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
OwnerAuthor

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
OwnerAuthor

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
OwnerAuthor

Choose a reason for hiding this comment

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

Removed.

@github-actions

github-actionsBot 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-actionsgithub-actionsBot 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 mainJul 27, 2026
9 checks passed
@taras
taras deleted the feat/concise-inputs branch August 27, 2026 01:34
Sign up for freeto 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

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
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

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: objectproperties:
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: stringloud:
type: booleandefault: 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 twinxmd 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-actionsgithub-actionsBot 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
OwnerAuthor

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
OwnerAuthor

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
OwnerAuthor

Choose a reason for hiding this comment

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

Removed.

@github-actions

github-actionsBot 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-actionsgithub-actionsBot 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 mainJul 27, 2026
9 checks passed
@taras
taras deleted the feat/concise-inputs branch August 27, 2026 01:34
Sign up for freeto 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

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

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: objectproperties:
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: stringloud:
type: booleandefault: 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 twinxmd 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-actionsgithub-actionsBot 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
OwnerAuthor

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
OwnerAuthor

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
OwnerAuthor

Choose a reason for hiding this comment

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

Removed.

@github-actions

github-actionsBot 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-actionsgithub-actionsBot 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 mainJul 27, 2026
9 checks passed
@taras
taras deleted the feat/concise-inputs branch August 27, 2026 01:34
Sign up for freeto 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

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

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: objectproperties:
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: stringloud:
type: booleandefault: 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 twinxmd 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-actionsgithub-actionsBot 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
OwnerAuthor

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
OwnerAuthor

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
OwnerAuthor

Choose a reason for hiding this comment

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

Removed.

@github-actions

github-actionsBot 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-actionsgithub-actionsBot 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 mainJul 27, 2026
9 checks passed
@taras
taras deleted the feat/concise-inputs branch August 27, 2026 01:34
Sign up for freeto 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

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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

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: objectproperties:
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: stringloud:
type: booleandefault: 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 twinxmd 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-actionsgithub-actionsBot 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
OwnerAuthor

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
OwnerAuthor

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
OwnerAuthor

Choose a reason for hiding this comment

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

Removed.

@github-actions

github-actionsBot 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-actionsgithub-actionsBot 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 mainJul 27, 2026
9 checks passed
@taras
taras deleted the feat/concise-inputs branch August 27, 2026 01:34
Sign up for freeto 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

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

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: objectproperties:
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: stringloud:
type: booleandefault: 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 twinxmd 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-actionsgithub-actionsBot 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
OwnerAuthor

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
OwnerAuthor

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
OwnerAuthor

Choose a reason for hiding this comment

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

Removed.

@github-actions

github-actionsBot 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-actionsgithub-actionsBot 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 mainJul 27, 2026
9 checks passed
@taras
taras deleted the feat/concise-inputs branch August 27, 2026 01:34
Sign up for freeto 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

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

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: objectproperties:
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: stringloud:
type: booleandefault: 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 twinxmd 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-actionsgithub-actionsBot 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
OwnerAuthor

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
OwnerAuthor

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
OwnerAuthor

Choose a reason for hiding this comment

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

Removed.

@github-actions

github-actionsBot 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-actionsgithub-actionsBot 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 mainJul 27, 2026
9 checks passed
@taras
taras deleted the feat/concise-inputs branch August 27, 2026 01:34
Sign up for freeto 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

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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

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: objectproperties:
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: stringloud:
type: booleandefault: 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 twinxmd 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-actionsgithub-actionsBot 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
OwnerAuthor

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
OwnerAuthor

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
OwnerAuthor

Choose a reason for hiding this comment

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

Removed.

@github-actions

github-actionsBot 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-actionsgithub-actionsBot 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 mainJul 27, 2026
9 checks passed
@taras
taras deleted the feat/concise-inputs branch August 27, 2026 01:34
Sign up for freeto 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

@taras