Skip to content

✨ Give installed Markdown components an explicit kind (#806) - #809

Merged
taras merged 4 commits into
mainfrom
agent/issue-806-markdown-discriminant
Sep 13, 2026
Merged

taras merged 4 commits into
mainfrom
agent/issue-806-markdown-discriminant

Conversation

@taras

@taras taras commented Sep 13, 2026 •

Copy link
Copy Markdown
Owner

Why

A declaration a trusted host hands an execution says everything about exact
Markdown — its name, origin, bytes and digest — but previously did not say what
kind of declaration it was. Admitting it from its shape would become ambiguous
once #806 adds structural declarations.

This prerequisite gives installed Markdown components an explicit kind and a
canonical constructor before the installation catalog gains another arm.

What changes

Before:

const component: MarkdownComponent = {
  name: "Plan",
  origin: PLAN_ORIGIN,
  source,
  digest: sourceDigest(source),
};

Any value with the expected Markdown fields was admitted without stating its
kind.

After:

const component = Markdown({
  name: "Plan",
  origin: PLAN_ORIGIN,
  source,
  digest: sourceDigest(source),
});

Markdown({…}) returns a fresh MarkdownComponent carrying
kind: "markdown". Admission refuses a component whose kind is missing or
unknown before the root document is imported.

How it works

Markdown({…}) → ExecutionInstallation.declarations → capture before install()
→ discriminant-first admission → existing declared-Markdown behavior

The constructor writes kind after spreading the input, so an input carrying a
kind cannot override it. It performs no validation, hashing, admission, deep
copying or freezing. Capture and admission retain those existing
responsibilities.

Review guide

Start with: packages/core/src/components/declared-markdown.ts

Then review:

  1. MarkdownComponent, MarkdownComponentInput and Markdown()
  2. admitDeclaredMarkdown(), where the discriminant is read first
  3. packages/core/src/execute.ts, where the stated kind is captured once before
    installation code runs
  4. The Plan constructors and declaration fixtures migrated to Markdown({…})

Look carefully at:

  • Missing and unknown kinds must refuse without reading the value as Markdown.

What must stay true

  • Markdown() is the canonical constructor, not an enforcement gate.
    MarkdownComponent remains an ordinary structural type; admission is the
    defensive boundary. Tier MDK2 checks that boundary.
  • Capture reads kind once before any install() call. Tier MDK3 checks that
    later mutation cannot change the admitted value.
  • Existing selection, closed imports, private closures, exact output, replay,
    inspection and validation remain unchanged. Tier DM and MDK1 retain those
    paths.
  • Syntax symbols keep their single version 2. MDK4 and SYN27 retain that result.

How to verify it

  • MDK1 proves that the constructor returns a fresh shallow object, fixes the
    kind, leaves its input unchanged, preserves supplied members and neither
    hashes, clones nor freezes caller values.
  • MDK2 proves that missing and unknown kinds refuse as
    DeclaredMarkdownError before root import or output, and before any other
    member is read.
  • MDK3 proves that capture reads the kind once before installation begins.
  • MDK4 proves that repository-owned syntax fixtures use the constructor while
    live inspection continues to emit symbols version 2.
deno task test packages/core/tests/declared-markdown-component.test.ts \
  packages/core/tests/syntax-component.test.ts \
  packages/cli/tests/plan-component.test.ts

Focused evidence at e3fa5027d05455be6915fa4252bd4168f8a746e4: 18 tests,
166 steps and no failures. deno task check and git diff --check also pass.

Scope

Included

  • The required kind: "markdown" discriminant on MarkdownComponent.
  • Markdown() and MarkdownComponentInput, exported from
    @executablemd/core/host.
  • Capture of the stated kind and discriminant-first admission.
  • Migration of every repository-owned installed Markdown component to the
    constructor.
  • The corresponding architecture, specification and Tier MDK evidence.

Intentionally unchanged

  • ExecutionInstallation.declarations remains Markdown-only. This PR adds no
    ExecutionDeclaration union, structural declaration, placement analysis,
    structural origin or expansion handler.
  • SyntaxSymbols remains at version 2.
  • Selection, imports, private closures, replay, inspection and validation keep
    their existing behavior.

New abstractions

  • MarkdownComponentInput omits the discriminant hosts should not have to
    repeat.
  • Markdown() constructs the installed Markdown components used by both Plan
    surfaces and the repository's declaration fixtures.
  • Each new abstraction has multiple concrete uses or a clear justification.
  • No speculative functionality is included.

Risks and limitations

  • MarkdownComponent is intentionally structural, so callers can still write a
    value by hand. Admission validates the required discriminant regardless of
    how the value was constructed.
  • Recovery or rollback: revert this PR. It changes no persisted or published
    state.

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 focused test results.

A declared Markdown component now states `kind: "markdown"`, and canonical
core reads that before it reads anything else about the value. Everything else
a declaration carries is a statement about exact Markdown, so a value admitted
on the strength of merely having a source and a digest would be one whose shape
decided what it meant.

`MarkdownDeclaration` is the canonical type and `DeclaredMarkdownComponent`
stays as its alias, so every existing import keeps working while constructing a
declaration states its kind. Capture copies the discriminant it was given rather
than deciding one, so a declaration that says nothing, or says something this
version does not know, reaches admission as it was written and is refused there
— before the root document is imported.

No other behavior changes: selection, closed imports, private closures, exact
source, replay, inspection, validation and the syntax symbols are as they were.
`Markdown({…})` is now how a host states a declaration. It takes the
description — name, origin, source, digest, forms, schemas, privates — and
returns a fresh declaration carrying `kind: "markdown"`, written after the
input is spread so a value carrying a kind of its own is overwritten rather
than believed. There is no assembling a declaration member by member that ends
up claiming to be Markdown.

It is a constructor and nothing else: it validates nothing, computes no digest,
copies no nested schema, array or private declaration, freezes nothing and
admits nothing. Copying stays at capture, before any `install()` runs, and
checking stays at admission.

Admission now reads the discriminant before it reads any other member, because
reading a name, origin, source or digest from a value that never said it was
Markdown is already treating it as Markdown. Every later check, its order, its
error type and its diagnostic are unchanged.

Both Plan declarations and every declaration fixture in this slice are built
through the constructor, and the annotations the factory makes redundant are
gone.
The constructor documentation claimed more than the code does. `Markdown({…})`
is the canonical way to build a declaration and is what every declaration this
repository owns is built with, but `MarkdownDeclaration` is an ordinary
structural type: a caller can still write the object out by hand, and no
constructor can stop one. Saying otherwise described a defence that does not
exist.

What does defend the execution is unchanged and stays stated: admission reads
the discriminant before any other member and refuses a missing or unknown one,
before the root document is imported.

MDK1 now types its input as `MarkdownDeclarationInput` instead of asserting
`as const` on the forms it passes, which is the same claim without a cast.

No behavior changes, no new evidence rows, and the syntax symbols keep their
single version.
@github-actions

github-actions Bot commented Sep 13, 2026 •

Copy link
Copy Markdown

PR #809: ✨ Give installed Markdown components an explicit kind (#806)

16 files, +455 / -75

Scope

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

Structural

Oxlint structural signals:

  • no-unused-vars ×8: packages/cli/tests/support/plan-harness.ts, packages/cli/src/plan-writer-profile.ts, packages/core/src/execute.ts (+2)
  • no-unnecessary-type-assertion ×5: packages/core/src/execute.ts
  • no-empty-function ×2: packages/cli/src/testing-host.ts, packages/cli/src/cli.ts

Slop

Oxlint slop signals:

  • no-console ×3: packages/cli/src/cli.ts

Static Analysis

Oxlint: 66 diagnostics across 9 files (16 rules)
Density: 0.145 violations/added-line

unbound-method (9): packages/cli/src/testing-host.ts, packages/cli/tests/support/plan-harness.ts, packages/cli/src/plan-component.ts (+2)
no-unused-vars (8): packages/cli/tests/support/plan-harness.ts, packages/cli/src/plan-writer-profile.ts, packages/core/src/execute.ts (+2)
no-unsafe-type-assertion (8): packages/cli/src/plan-component.ts, packages/core/src/execute.ts, packages/cli/src/cli.ts
no-base-to-string (6): packages/cli/src/plan-component.ts
no-array-sort (5): packages/core/src/syntax-reference.ts, packages/core/src/execute.ts, packages/core/src/document-validation.ts (+1)
consistent-return (5): packages/core/src/execute.ts, packages/core/src/inspect.ts, packages/core/src/document-validation.ts (+1)
no-unnecessary-type-assertion (5): packages/core/src/execute.ts
no-unnecessary-type-conversion (4): packages/core/src/syntax-reference.ts
no-shadow (3): packages/core/src/execute.ts
no-console (3): packages/cli/src/cli.ts
consistent-function-scoping (2): packages/cli/src/plan-writer-profile.ts, packages/cli/src/plan-component.ts
no-empty-function (2): packages/cli/src/testing-host.ts, packages/cli/src/cli.ts
no-floating-promises (2): packages/core/src/execute.ts, packages/cli/src/cli.ts
no-misused-spread (2): packages/core/src/inspect.ts
no-unmodified-loop-condition (1): packages/cli/src/cli.ts
no-duplicate-type-constituents (1): packages/core/src/execute.ts

Correctness

FILE: packages/cli/src/plan-component.ts, packages/core/src/execute.ts, packages/core/src/inspect.ts
PATTERN: signal clusters (no-unused-vars, no-unsafe-type-assertion, no-array-sort)
CONCERN: Multiple Oxlint rules firing in same files suggests unreviewed code quality issues
QUESTION: Are these violations due to generated code or missed refactors?

FILE: packages/core/src/execute.ts
PATTERN: no-unused-vars (2 instances)
CONCERN: Unused variables may indicate dead code or incomplete refactors
QUESTION: Are these variables required for future extension or can they be removed?

FILE: packages/cli/src/plan-component.ts
PATTERN: unbound-method (2 instances)
CONCERN: Methods may not be bound to this correctly
QUESTION: Do these methods require binding or are they used in contexts that handle it?

FILE: packages/core/src/execute.ts
PATTERN: no-array-sort (2 instances)
CONCERN: Non-stable sort operations may introduce non-determinism
QUESTION: Are the arrays being sorted for a specific reason that requires stability?

FILE: packages/core/src/execute.ts
PATTERN: no-unsafe-type-assertion
CONCERN: Type assertions may hide missing properties or incorrect assumptions
QUESTION: Are these assertions necessary for compatibility or can they be replaced with safer patterns?

No extraneous code patterns detected.

@taras taras changed the title ✨ Declare exact Markdown with a stated kind (#806) ✨ Give installed Markdown components an explicit kind (#806) Sep 13, 2026
The type a host hands an execution is a component, not a declaration about one,
and the name now says so: `MarkdownComponent`, built from
`MarkdownComponentInput` by `Markdown({…})`.

`MarkdownDeclaration`, `MarkdownDeclarationInput` and the
`DeclaredMarkdownComponent` compatibility alias are gone rather than kept
beside the new names. Nothing outside this repository depends on them, and an
alias that exists for no caller is a second name for one thing that every later
reader has to reconcile.

Every repository-owned import, annotation, constructor input, documentation
reference and specification signature moves with it. The module keeps its
path — this renames the public types, not `components/declared-markdown.ts`.

No runtime behavior changes: the constructor still returns a fresh shallow
object and writes `kind` last, validating, hashing, copying, freezing and
admitting nothing; admission still reads the discriminant before any other
member; and the syntax symbols keep their single version.
@taras
taras marked this pull request as ready for review September 13, 2026 01:58
@taras
taras merged commit b5fde70 into main Sep 13, 2026
38 checks passed
@taras
taras deleted the agent/issue-806-markdown-discriminant branch September 13, 2026 01:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant