✨ Bootstrap npm packages as a real xmd run: elicit the OTP - #308
Conversation
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.
PR #308: ✨ Bootstrap npm packages as a real
|
|
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 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:
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 What was split out is the engine defect this work uncovered: #307 ( |
Closes #276. Supersedes draft #154, which is closed alongside this.
Why
scripts/bootstrap-npm-package.mdonly ever existed on draft #154, and it isexecutable Markdown that cannot execute. npm's interactive auth outlives the
fixed 30-second subprocess timeout (#153), so the document told the operator to
awkits ownbash execblock out of itself, run it under bash, pass thetarget package through
PACKAGE_DIR, and race a 30-second OTP window across twoinvocations.
<Elicit>(#273) inverts that: ask for the code at the point of use, and the npmcommands become non-interactive and fast enough to fit.
What changes
Before: a document that documents a shell script, plus a manual extraction dance.
After:
Preview renders, a browser form asks for a fresh code, the artifact publishes
under the
bootstrapdist-tag, trust is configured, and the run reports.How it works
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:result.ok<Output>regionA failed block becomes
<!-- ERROR -->under the collecting policy and the loopcontinues (
expand.ts:672-707). A non-zero command that printed anything raisesnothing at all, because the guard is
exitCode !== 0 && output === ""(
expand.ts:675) — filed separately as #307. And the only policy that doesfail 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 arefatal 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 pipefailbefore it can report leaves onebehind. 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; thegates only make sense in order.
Then review:
patternand the## Runwarning placement.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
preview verdict gate, checked by four refusal cases.
## Runwarning is before anyblock, since a warning read after the code is handled protects nothing.
pattern, checked by asserting the Process middleware's call count is zero.<TempDir>, checkedby comparing the
PWDnpm recorded forpackandpublish.AssertMatchis anchored —/^\s*ok$/, sonot okfails. A substringtest would not be fail-closed; there is a test that mutates the document's
seeded verdict to
not okand proves the gate still refuses.How to verify it
20 cases drive the real document through
execute(), substituting only atcontextual Api boundaries —
Elicitationfor the question,API.Processdelegating via
next()to real bash with a fakenpmonBASH_ENV. Every caseasserts the completion
Resultand both sides of the ordering.Each was demonstrated red against a mutation of the document:
npm_config_otpfrom the trust callmktemp -dinstead of<TempDir>Two cases exist specifically because of #307:
refuses when the preview prints before it failsandreports a publish that prints before it fails— a commandthat 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 · test323 passed / 0 failed ·
check:jsrSuccess ·pnpm install·tscclean ·pnpm test:node2189/0 ·bun run test:bun2189/0 ·deno task build./dist/xmd test packages/core/src --rawexit 0 ·git diff --summaryempty ·git diff --checkclean.Scope
Included
makes necessary.
Intentionally unchanged
expand.ts:675). Filed during this work; the fix is deliberatelyoutside this PR because the gates route around exit codes entirely.
interpolation today, so the document writes
{package};{props.package}follows when that breaking change ships.
{otp.code}needs no workaround.@executablemd/webbootstrap, which completes with Make npm bootstrap resumable #154's script.Risks and limitations
AssertMatch, notAssertEquals.<Capture>trims only trailingwhitespace, and a rendered code block carries a leading newline the command
cannot control, so exact equality against
okcannot hold. The pattern isanchored at both ends, which is equally fail-closed — proven by the
not okmutation test.
a never-published name. Two facts stay open until then: whether
npm trustrequires 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.§6states that pre-existence trust is observed to be accepted, not yetobserved to govern a first publish. That becomes an amendment, not a rewrite.
Generated or mechanical changes
node -esnippets 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