Replace per-plugin tapIndex regex edits with pure-data IndexInjection rows
collected fresh per render over one webserver/index-inject event. One table,
two renderers: the served form renders rows into index.html; a static worker
form ships the same rows over its boot payload. tapIndex survives as the
raw-HTML escape hatch, applied after row rendering; client-modules and
ui-theme move to the event, and the manifest global renders as
globalThis["__DSH_BOOT__"].
The client boot chain gains the seams a pre-injected transport needs: the
module loader takes loadBundle from the transport global by default, HTTP
prefetch stands down when a transport owns bundle bytes, the web-app bundle
can decline frontend serving, the gateway client installs a namespace's
whole method group inside its fiber apply so a parked dependent never
observes the service without its methods, and the dynamic-code precheck
gates through new Function so hosts without a real node:vm keep the
define-time parse gate.
The shell's Vite config aliases a few workspace packages to source; every other
workspace name resolved through node_modules to a lib/ entry the real build has
emitted but a clean checkout has not, so the generator only worked on a built
tree — and a static gate has to pass on a clean one. The dry run now supplies
source aliases for the names the shell leaves out.
Aliases rather than the recorder's resolveId hook, because Vite resolves a
stylesheet @import through aliases alone, and the theme package publishes its
stylesheets from lib/styles/. lib/ is compiled from src/, and the recorded set is
identical either way: 24 packages on a built tree and on a clean one.
Moving react, shiki, katex and the markdown pipeline to devDependencies took
them out of the notices runtime tier, which tiers by declaring section — yet
their code is inside lib/client.js and the shell dist. The generator now learns
what the browser artifacts carry from the real build configs: each client bundle
through its own tsdown config, the shell through apps/web's Vite config, with a
recorder that resolves each bare specifier, notes the package behind it, and
stops there. About three seconds, and only packages a resolved file backs, so a
bundler's virtual module is not mistaken for a shipped one.
Net effect on the file: the type-only packages @types/mdast and
micromark-util-types move to the development tier, because neither ships code.
A package a published browser artifact carries is a runtime disclosure whatever
section declares it, and the build answers which ones those are. Record that
clause, the dry run behind it, why a bundler's virtual module is not a shipped
package, why a types-only package is not either, and the generator run cost the
dry run adds.
react, react-dom, shiki, katex, clsx, the micromark and mdast families and nine
more reach only browser artifacts, which resolve nothing on a user's machine.
Moving 79 declarations out of dependencies and non-optional peerDependencies
drops 103 tarballs and 6.05 MB from an install of the published CLI.
verify-client-runtime-deps walks each package's host and browser faces from the
entries its manifest publishes, over the bound host and client Programs, and
reports an external package no host-face reference reaches. Wired into hygiene.
A workspace name is out of scope: it also states which package supplies an
injected service or a mounted Remote contribution, and the app installs it
either way. A package whose published Node entry has no source counterpart is
skipped and named, because a generated artifact carries imports no source
states.
Browser artifacts resolve nothing on a user's machine: tsdown inlines every
non-platform specifier, the shell dist answers the rest from its frozen module
table, and Vite inlines the shell's own imports into the published dist. Record
the resulting declaration rule, and the Agent Note behind it.
A dsh publication uploads 221 packages one at a time, spaced apart, and
the log gave no way to tell how far along a run was: every line named a
package, none said where that package sat in the set.
Each per-package line now carries [n/total]. Every entry in the order
settles as either published or already present, so the counter is both
"packages settled" and "position in the publish order", and the closing
summary names the member count alongside the published and skipped
totals.
Three corrections from review, none of which change behaviour.
attemptStreaming promised output "as the command produces it", which
spawnSync cannot do: it returns only after the child exits, and the two
streams are echoed one after the other, so their interleaving is lost.
For an npm publish that is visible — notices go to stderr while the
`+ name@version` confirmation goes to stdout, so the confirmation prints
first. The helper is now attemptEchoed and its contract says buffered,
echoed after exit, stdout before stderr; live progress would need an
asynchronous spawn with data listeners.
The traversal comment claimed a node on the stack is a cycle only peer
edges can form, and that skipping it drops just that edge. The cycle does
carry a peer edge, because the install edges were proved acyclic a moment
earlier, but the back edge that reaches the stacked node need not be the
peer one — which is what the post-condition exists to catch, so the
comment now points at it instead of asserting an invariant the traversal
does not have.
The `if (placed.has(member.name)) return` after leaving the stack was
unreachable: a re-entrant visit returns at the top guard while the member
is on the stack, so it can never be placed by the time the recursion
unwinds.
A dependency in optionalDependencies, or a peer carrying
peerDependenciesMeta.<name>.optional, may be absent from an installed
tree — that absence is the whole promise of "optional". A static import
is evaluated when the importing module loads, so one absent package
stops being "this capability is unavailable" and becomes a load failure
for everything that reaches the importing module.
Nothing checked it, and nothing here could: the failure needs an
installed tree missing that package, and a workspace install always has
every package, so the unit tests, the snapshots, and the packed-install
probe all pass while the published package is broken for the consumer
who declined the optional peer.
verify-optional-dependency-imports reads each package's own manifest for
what it allows to be absent, then scans the files that ship across both
compiler faces. Value-versus-type is decided against a bound Program
rather than the import syntax, because verbatimModuleSyntax is off: the
compiler already erases an import whose bindings resolve to types, so a
syntactic rule would report four forms that emit nothing. Only the type
phase erases an import — `import defer` still resolves and links its
module, deferring evaluation alone — which is what phaseModifier
expresses and the deprecated isTypeOnly cannot.
A violation names the package, the declaration that made it optional,
and the way out in order: import it as a type, or restructure so module
scope does not need it. A dynamic import() only moves the failure to
first use, so the gate does not offer it as the remedy.
The gate runs in ci-static and ci-primary through ciSharedStaticGates
and locally in hygiene; it needs no build. TypeScriptProject gained a
face parameter so a repository-wide gate can seed the client aggregate,
which was previously unreachable; the constraint it was built with is
unchanged, a face config and never the root solution.
The tree has no violation today, so this guards the rule rather than
fixing a defect. The spec pins all seven import forms against what tsc
emits, including the four a syntactic rule would misreport.
The verify step resolved the publish order and said only that it had:
the order a release actually follows, and the ordering it could not
honour, stayed invisible until a publication was already running.
publishOrder now returns that order together with the peer edges it
dropped, verify prints both, and pack reads the order off the plan. The
dropped edges are part of the result rather than a detail of forming it:
the dsh family drops one (dsh-api-remotes -> dsh-api-gateway) and the
vendored family drops two (cordis-plugin-include and
cordis-plugin-loader, which cordis declares as peers in return), and
only whoever reads the log can judge whether a newly dropped edge is
expected.
Because pack runs on every pull request and master push, a change to the
order is now reviewable there rather than observable only at publish
time.
The order is also checked against the edges it exists to honour. A cycle
mixing peer and dependency declarations can put a dependency on the
traversal stack, where it is skipped like a peer edge, emitting a
consumer before something it installs; no later step can detect that,
and it would surface as an unresolvable install for a consumer of the
published packages. No family has that shape today, and the new test
pins the three-package case that would.
Retry classification needs npm's failure text, so the publish call captured its
streams instead of inheriting them. That silenced npm on the success path: the
log lost the tarball contents, the notices, and the '+ name@version'
confirmation for every package.
Pipe the streams and echo them, so the log shows what npm reported and the
caller still gets the text it classifies. The registry probe behind it keeps its
streams captured, since its JSON and its E404 are internal queries rather than
progress.
Publish order exists to make a partial publication self-consistent: an
interrupted run should leave a prefix whose packages never point at a version
absent from the registry. It read only dependencies and optionalDependencies, so
peer declarations — how sibling harness packages reference each other, 1088 edges
in the dsh family — constrained nothing.
Peer edges now order the publication too. devDependencies still do not: a dev
dependency is absent from the published package.
Peers cannot constrain it absolutely. Sibling packages declare each other as
peers, which is what closes the two cycles here, and npm treats an unmet peer as
a warning rather than a resolution failure. Install edges therefore win: a peer
edge is dropped where the peer installs the member declaring it, or where
following it would revisit a member already being visited. One peer edge is
dropped in the dsh family and two in the vendored family; every install edge is
honoured.
A cycle among install edges stays a defect rather than something to order
around, and release:verify now reports it before the build instead of letting it
surface once pack is already writing tarballs. Install-edge acyclicity is checked
on its own graph, because a peer edge leading into an install edge otherwise
reads as a cycle where the install edges are perfectly orderable.
Every release member now declares publishConfig.access: public, so the scope no
longer mixes levels: the 221 packages/*/* and apps/* manifests join the vendored
framework and the native packages.
check-workspace-constraints drops the per-sequence expectation and holds every
release member to public, which is what stops a member from drifting back.
Access is a property of the package, not of a version: the dsh packages already
published as restricted become world-readable at their next publication.