Skip to content

✨ Bootstrap npm packages as a real xmd run: elicit the OTP - #308

Draft
taras wants to merge 1 commit into
mainfrom
feat/issue-276-bootstrap-elicit
Draft

✨ Bootstrap npm packages as a real xmd run: elicit the OTP#308
taras wants to merge 1 commit into
mainfrom
feat/issue-276-bootstrap-elicit

Conversation

@taras

@taras taras commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Closes #276. Supersedes draft #154, which is closed alongside this.

Why

scripts/bootstrap-npm-package.md only ever existed on draft #154, and it is
executable Markdown that cannot execute. npm's interactive auth outlives the
fixed 30-second subprocess timeout (#153), so the document told the operator to
awk its own bash exec block out of itself, run it under bash, pass the
target package through PACKAGE_DIR, and race a 30-second OTP window across two
invocations.

<Elicit> (#273) inverts that: ask for the code at the point of use, and the npm
commands become non-interactive and fast enough to fit.

What changes

Before: a document that documents a shell script, plus a manual extraction dance.

After:

deno task xmd run scripts/bootstrap-npm-package.md --props-package packages/web

Preview renders, a browser form asks for a fresh code, the artifact publishes
under the bootstrap dist-tag, trust is configured, and the run reports.

How it works

prop validation → TempDir → seed verdict → preview block → verdict gate
  → <Elicit> → seed verdict → publish + trust → verdict gate

The engine does not stop a document when a code block fails, and this is the
central thing to understand about the diff. Measured with bare execute()
against main:

Structure Preview outcome Elicited? Publish ran? result.ok
text root exit 1, empty stdout yes yes true
text root exit 1, non-empty stdout yes yes true
<Output> region exit 1, empty stdout yes yes true
documentation exit 1, empty stdout no no false
documentation exit 1, non-empty stdout yes yes true

A failed block becomes <!-- ERROR --> under the collecting policy and the loop
continues (expand.ts:672-707). A non-zero command that printed anything raises
nothing at all, because the guard is exitCode !== 0 && output === ""
(expand.ts:675) — filed separately as #307. And the only policy that does
fail fast belongs to documentation regions, whose output is discarded — so
fail-fast and a visible preview are mutually exclusive for a code block.

So the ordering is built, not assumed. Each block writes an exact sentinel to a
file in a shared <TempDir>; a <Capture>d block reads it back; an anchored
<AssertMatch> decides. An assertion is an unmarked component, and those are
fatal even under the collecting policy — the same reason a failing <Elicit>
already aborts. The verdict is seeded with a failure before each block, so a
block that dies under set -euo pipefail before it can report leaves one
behind. Nothing consults an exec exit code, which is why #307 does not
affect this document.

Review guide

Start with: scripts/bootstrap-npm-package.md — read it top to bottom; the
gates only make sense in order.

Then review:

  1. The frontmatter pattern and the ## Run warning placement.
  2. The two verdict gates and their seed blocks.
  3. The publish block's registry recheck.
  4. scripts/tests/bootstrap-npm-package.test.ts, then the two spec diffs.

Look carefully at:

  • installTestingComponents({ verbose: false }) in the test harness. execute()
    registers no assertion components — only the CLI does — so without it the
    gates silently do not resolve and every ordering assertion passes vacuously.
    There is a test that pins this (has inert gates when the assertion components are missing) and one that pins the diagnostic never appearing in a real run.

What must stay true

  • A failed preview asks for no code and writes nothing — enforced by the
    preview verdict gate, checked by four refusal cases.
  • The code never persists or displays — the ## Run warning is before any
    block, since a warning read after the code is handled protects nothing.
  • A shell-metacharacter prop never reaches bash — enforced by the schema
    pattern, checked by asserting the Process middleware's call count is zero.
  • Preview and publish share one artifact — enforced by <TempDir>, checked
    by comparing the PWD npm recorded for pack and publish.
  • AssertMatch is anchored/^\s*ok$/, so not ok fails. A substring
    test would not be fail-closed; there is a test that mutates the document's
    seeded verdict to not ok and proves the gate still refuses.

How to verify it

20 cases drive the real document through execute(), substituting only at
contextual Api boundaries — Elicitation for the question, API.Process
delegating via next() to real bash with a fake npm on BASH_ENV. Every case
asserts the completion Result and both sides of the ordering.

Each was demonstrated red against a mutation of the document:

Mutation Reddens
neuter the preview verdict gate 4 refusal cases
delete the publish-stage recheck both across-elicitation cases, + resumable
delete the npm version guard old-npm case
drop npm_config_otp from the trust call OTP propagation
remove the fail-closed seed 4 refusal cases
per-block mktemp -d instead of <TempDir> shared artifact, lifetime, + 3

Two cases exist specifically because of #307: refuses when the preview prints before it fails and reports a publish that prints before it fails — a command
that writes to stdout and then exits non-zero, which the engine reports as
success.

Full gate, runtimes pinned and asserted (Deno 2.9.1, Node 22.23.2, Bun
1.3.14, matching CI): deno task setup · fmt · lint 0 errors · check · test
323 passed / 0 failed
· check:jsr Success · pnpm install · tsc clean ·
pnpm test:node 2189/0 · bun run test:bun 2189/0 · deno task build

  • ./dist/xmd test packages/core/src --raw exit 0 · git diff --summary empty ·
    git diff --check clean.

Scope

Included

  • The document, its 20-case suite, and the two spec corrections this change
    makes necessary.

Intentionally unchanged

Risks and limitations

  • AssertMatch, not AssertEquals. <Capture> trims only trailing
    whitespace, and a rendered code block carries a leading newline the command
    cannot control, so exact equality against ok cannot hold. The pattern is
    anchored at both ends, which is equally fail-closed — proven by the not ok
    mutation test.
  • No real bootstrap has been run. A live run needs a login, a 2FA device and
    a never-published name. Two facts stay open until then: whether npm trust
    requires an OTP at all under this account's 2FA (propagation is asserted
    regardless), and whether publish and trust fit in one 30-second block against a
    slow registry — if not, the fix is to split them into two blocks reading the
    same {otp.code}, never to reintroduce materialization.
  • §6 states that pre-existence trust is observed to be accepted, not yet
    observed to govern a first publish. That becomes an amendment, not a rewrite.

Generated or mechanical changes

  • The node -e snippets in the document are carried over verbatim from Make npm bootstrap resumable #154.
    The current Markdown surface cannot write the generated package files, so they
    are retained rather than replaced; the document does not claim to be
    JavaScript-free.

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.

Closes #276. Supersedes draft #154.

`scripts/bootstrap-npm-package.md` becomes executable Markdown that actually
executes: the one-time code arrives through `<Elicit>` at the point of use, so
the npm commands are non-interactive and fit inside the 30-second block limit.
The `awk`-yourself-out-of-the-document workaround is gone, and the target
package arrives as a root prop instead of an environment variable.

The ordering the document depends on is not something the engine provides. A
failed root code block becomes an ErrorSegment under the collecting policy and
expansion carries on to the next segment; a non-zero command that printed
anything raises nothing at all (#307). Left alone, a failed guard would still
ask for a code and still publish, and the run would exit 0.

So each block writes an exact sentinel to a file in a shared `<TempDir>`, a
captured block reads it back, and an anchored `<AssertMatch>` — an unmarked
component, fatal even under the collecting policy — turns anything else into an
abort. The verdict is seeded with a failure before each block runs, so a block
that dies before it can report leaves one behind. Nothing consults an exec exit
code, which is why #307 does not affect it.

The `package` prop is constrained by schema to `^packages/[a-z0-9][a-z0-9._-]*$`
and quoted at every use, so a value carrying shell metacharacters fails
validation before any block exists to receive it. The publish block re-checks
registry state rather than trusting the preview's, because the operator has been
away entering a code.

Specs, both required by this change:

- `executable-mdx-spec.md` §6.6 said eval binding interpolation excludes dotted
  paths. It has supported them since `eval-interpolate.ts:28`, and the document
  reads `{otp.code}`. Corrects the regex, the precedence rationale, the code
  sample, decision 39, and the Tier P table, which listed P1–P8 against a suite
  already running P1–P19.
- `release-process-spec.md` §6 opened on "npm exposes trusted-publisher settings
  only on a package that already exists". npm 11.17 accepted `npm trust github`
  for `@executablemd/web` while the registry still returned E404. §6 now
  describes this document, keeps the reservation publish on the grounds that
  survive, and states that pre-existence trust is observed to be accepted but
  not yet observed to govern a first publish.
@taras taras mentioned this pull request Aug 3, 2026
4 tasks
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

PR #308: ✨ Bootstrap npm packages as a real xmd run: elicit the OTP

4 files, +918 / -34

Scope

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

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

Structural

🟡 2 console statements.

Slop

✅ Slop indicators look low.

Static Analysis

✅ Oxlint found no issues.

Correctness

No extraneous code patterns detected.

@taras

taras commented Aug 3, 2026

Copy link
Copy Markdown
Owner Author

Two automated flags, neither of which I think warrants a change — reasoning here so a reviewer doesn't have to re-derive it.

"2 console statements" — false positive. These are node -e one-liners inside the document's bash blocks, where console.log is how the subprocess returns a value to the shell:

pkg_name="$(node -e '… console.log(JSON.parse(fs.readFileSync(process.argv[1], "utf8")).name)' "$pkg_dir/package.json")"

They are carried over verbatim from #154 and are called out under Generated or mechanical changes: the Markdown surface has no way to write the generated package files, so they stay rather than being replaced by something that only looks tidier.

"952 lines — split into focused PRs." The split the bot wants isn't available here. The breakdown:

File Lines
scripts/tests/bootstrap-npm-package.test.ts 548
scripts/bootstrap-npm-package.md 267
specs/executable-mdx-spec.md 93
specs/release-process-spec.md 44

Two-thirds is the test, and it is 20 whole-document cases rather than a broad surface — the count reflects one harness plus per-case assertions on both sides of every ordering, which is the point of the change.

Neither spec diff is separable. §6.6 currently states that eval binding interpolation excludes dotted paths; the document reads {otp.code}. Landing the document first would ship a document whose central mechanism the spec forbids, and landing the spec first would describe a document that doesn't exist. release-process-spec.md §6 is the same shape — its opening premise ("npm exposes trusted-publisher settings only on a package that already exists") is what this document disproves, and #276 lists it as an acceptance criterion.

What was split out is the engine defect this work uncovered: #307 (expand.ts:675, a non-zero command that printed to stdout raises no failure). Its fix is deliberately not here.

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.

Bootstrap npm packages as a real xmd run: elicit the OTP

1 participant