1504 hand-written ranges pointing at workspace members become workspace:^, so
pnpm pack substitutes each member's real version at publication: sibling
peerDependencies follow the family version instead of being pinned at ^0.0.1,
and a reference to a vendored package follows that package's own line. Without
this, publishing 0.0.2 ships peer ranges naming a version that does not exist,
and 0.0.1-rc.1 does not satisfy ^0.0.1 either.
It also retires ranges that had gone stale against the workspace: ^4.0.0-rc.6
for a 4.0.0-rc.7 checkout, ^3.17.0 for schemastery 3.18.0.
workspace:* stays where an exact published version is the point, which is how
the Landlock entry pins its platform packages.
A workspace constraint now requires the protocol, so a new package cannot
reintroduce a hand-written range. The same constraint caught packages/boot/cmdline
arriving on master without the publishable trio, which this change completes.
Every package under packages/, apps/, and vendor/ drops "private": true and
declares publishConfig.access "restricted": the repository now states which
packages it publishes instead of deciding it at publish time. Each one also
declares its repository and directory, which is how a consumer of a private
package reaches its source.
The Landlock packages move to restricted with them. They have never been
published, so nothing anonymous depends on them today, and the whole
@deepseek-ai scope stays private.
The workspace constraint that required every package to be private now applies
to non-members only, and asserts the publishable trio on each release member.
Machine-produced by `pnpm run rescope-vendor --apply` plus the regeneration it
prints: `pnpm install` for the lockfile, `pnpm run gen-third-party-notices`,
`verify-translation-pairing --write` for the touched bilingual pairs,
`gen-doc-graphs`, and one typert snapshot whose ids embed character offsets.
`pnpm run rescope-vendor --check` verifies the result.
Renames nine vendored packages (cordis, cosmokit, schemastery and the six
@cordisjs plugins) and every reference that resolves them: manifest names and
dependency keys, module specifiers including declare-module merges, cordis.yml
plugin names, tsconfig paths, every Markdown fence, and `docs/` prose.
Directory names, upstream versions, and dependency ranges are unchanged, so
vendor/README.md still reads as an upstream snapshot; its manifest table gains
an upstream-name column so THIRD_PARTY_NOTICES keeps MIT attribution pointed
at each fork's origin.
The tutorial tier follows the rename end to end: its yaml fences named plugins
the Loader can no longer resolve, its `ts ignore-check` fences disagreed with
the compiled fences beside them, and its prose quoted both. The contracts that
told readers to keep upstream names — the root convention and the vendoring
cookbook's tree comment and manifest invariant — now say to rescope instead.
Two rules read `@deepseek-ai/` as "another workspace plugin": the client bundle
purity gate now names the vendored libraries a browser bundle inlines, and the
files where a bare `cordis` is an agent-preset id keep that product data.
The layered-env reader gained an unreadable-layer path, a default
reporter, and two absent-layer arms with no cases; the credential store
gained two error paths that must not be mistaken for an absent file.
The platform arms and the `linePos` guard cannot be reached from a POSIX
test run — the first is covered by the native Windows job, the second
only satisfies an optional type that `prettyErrors` always fills — so
both carry a v8 ignore naming why.
Review found five real defects in the configuration-source work, all confirmed
against the code rather than argued:
1. The note claimed --config outranks settings.yaml. It does not: the settings
seam registers a plugin's cordis entry config as the `base` layer and the
user section layers over it, and the seam cannot tell a shipped value from a
--config one. The note now states shipped reality and names --config-replace
as the lever for a deployment that must win. Separately, a literal `apiKey`
in settings outranked both the environment and .credentials.yaml — the field
is removed, so configuration carries a reference and nothing else.
2. DEEPSEEK_SEARCH_BASE_URL was functionally deleted: the shipped inline went
away without the provider learning to read it. It now resolves from the
environment snapshot, as the README always claimed.
3. The bootstrap deny list missed the interpreter start-up hooks. BASH_ENV is
the sharpest: `bash -c` sources it on every bash tool call, so a project
.env could run a file of its choosing before every command. The list now
covers BASH_ENV and its per-language siblings, the Git hook commands, and
the remaining preload and CA variables, organised by what a variable does
rather than which runtime owns it.
4. YAML parse errors quoted the offending source line — which in a credentials
document is the secret — into boot stderr and the watcher's logger. Only the
error code and position are reported now, in credentials-local and
settings-local alike, pinned by a test that asserts the secret is absent.
5. 0600 governed only files the harness wrote. A hand-created 0644 document was
read normally. POSIX now checks the mode before reading contents, at boot
and on every reload; Windows has no mode to inspect and is skipped rather
than faked.
The project a session is launched in is trusted by default, with no prompt and
no stored trust record: it may supply its own endpoint, ordinary variables, and
a key ranked below the managed store. Trust stops at the harness itself — a
discovered file still cannot set DSH_PERMISSION_MODE, PATH, BASH_ENV, or the
rest, because those take effect with no user action, before any turn, outside
the permission policy and the sandbox.
Review round three, credentials half. dsh-atomic-write grows the
cross-process writer-lock primitive (withFileLock: wx sentinel, bounded
backoff, stale takeover via onStaleBreak, deadline failure) plus a dirMode
option, and settings-local migrates its private copy to it; both providers
now create harness-home directories 0700.
credentials-local reuses the reviewed settings-local shape: watcher
reloads and line edits share one settled operation chain; every write
re-reads the document under the lock and publishes unobserved external
entries before editing, so an edit inside the debounce window (or another
process's write) can never be overwritten; the watcher's ready signal
queues one reconcile closing the startup gap.
The line editor is now physical-line aware: continuation lines of a
quoted multi-line value are never mistaken for assignments, untouched
lines keep their exact bytes (CRLF included), an edited line keeps its
own terminator, and appends use the document's dominant ending. A
multi-line entry reports writable: false, matching what set() would do.
The Credentials base class owns a contained notifyUpdated fan-out:
providers publish only after the commit, every listener runs, sync throws
and async rejections are logged without failing the committed write, and
INVARIANT-coded failures rethrow after the fan-out.
The seam README states the JSON-shaped write boundary, watch-disposer
quiescence, async listener containment, and the drained teardown; the
provider README rewrites Behavior around the operation chain,
read-modify-write, writer lock, ready reconcile, and leaf-level YAML
diffs, and updates Known Limitations to the residual guarantees.
A new Agent Note records the round's decisions and supersedes the
original note's deferred-lockfile alternative (cross-linked in place).
Chinese counterparts updated pair-by-pair (three briefed minimal
updates, one whole-document translation); type-equiv, config, cordis,
and module-graph catalogs re-recorded.
Review round three found the provider's write path could destroy state it
never observed:
- Watcher reloads and document writes ran on two independent promise
chains, and a write rendered the whole next document from the cached
text. An external edit still inside the debounce window (or missed
outright) was overwritten, and the follow-up reload no-oped because the
post-rename content matched the cache — the edit vanished without a
trace. Reloads and writes now share one operation chain, and every write
starts by reconciling the on-disk text into the seam before rendering,
so unobserved sibling sections survive and publish first. An unparsable
on-disk document fails the write loud instead of being overwritten.
- The initial load raced the watcher's own setup: a change written between
that read and the watcher becoming active never fired an event. The
watcher's ready signal now queues one reconcile, closing the gap.
- Two processes sharing a harness home rendered from independent caches,
last writer winning. Writes now hold a wx-created <file>.lock sibling
around the read-render-rename cycle with bounded backoff, a crashed-
holder stale takeover, and a deadline failure; readers stay lock-free
because the rename commit is atomic.
- renderYaml replaced the whole namespace node, dropping every comment
inside the section. The next section now lands as a leaf-level diff
(set changed values, delete removed keys), so comments, anchors, and
formatting survive on every untouched node and on the key of every
changed pair; arrays still replace wholesale when unequal.
writeFileAtomic: exclusive-create random-suffix temp + rename carrying the
caller-stated mode; settings-local persistSection now consumes it. The
credentials-local store shares it next.
Confirmed and fixed, each with a regression test that failed first:
- Concurrent writes to different namespaces lost whole sections on disk
(each persist rendered the full document from a stale text): the local
provider serializes render->write->rename->text-commit on one internal
persist chain shared by every namespace queue.
- One throwing settings/updated listener starved the rest (cordis emit
stops at the first throw): commit fans out per listener via
events.dispatch, contains individual failures, and rethrows the first
INVARIANT-coded error only after every listener ran.
- Write queues ignored fiber/service lifecycle: the base init now
registers a teardown that refuses new writes and drains queued chains;
queued tasks re-verify service liveness and namespace ownership before
running and again before committing, so a registrant disposed
mid-flight is never notified and a disposed service never commits.
- Async watcher invocations could interleave (a slow stale call applied
last): each watcher carries a serialized invocation chain — one call
at a time, in commit order; JSDoc/doc pages state the async timing.
- update/replace borrowed the caller's object until the queued task ran:
inputs are structured-clone snapshotted at call time; non-cloneable
plain objects reject with a typed error.
- Composition guard now proves the documented fallback: the consumer
uses the optional scoped-inject shape and boots both with the settings
entry (hot publish) and without it (entry-config resolution, no scope).
- core-data-structures index: settings.md row added to the sub-page
table in core.md/core.zh.md.
Both packages hold per-file 100% coverage across repeated runs.
Confirmed and fixed, each with a regression test that failed first:
- Concurrent update() lost patches (merge over one stale snapshot):
per-namespace serialized write queues; a failed write cannot poison
the queue for later writers.
- Fixed-name .tmp write followed planted symlinks and kept stale modes:
random-suffix sibling, exclusive-create (wx), 0600, cleanup on
failure, then rename.
- A throwing settings/updated listener escaped commit and permanently
wedged the provider reload chain (rejected refreshTask): commit now
contains listener failures (INVARIANT-coded errors still propagate),
async watcher rejections are adopted and contained
(watch callbacks are officially void | Promise<void>), and the
provider chains refreshes on a settled tail with an error log.
- No way to remove a user override: scope/service replace(section)
sets the user section wholesale; replace({}) re-inherits base and
schema defaults.
- The three-primitive provider contract did not hold (base never
called load()): the base Service.init loads and publishes once;
settings-local delegates via yield* super[Service.init]().
- Dispose did not quiesce: teardown flags closed, closes the watcher,
then awaits queued/in-flight reloads; closed is re-checked across
await points.
- Invariant now checks the authoritative relation with the seam's own
deepEqualJson: emitted next must equal settings.get(ns), and
next/prev must differ structurally (cosmokit dependency dropped).
- New docs/core-data-structures/settings.{md,zh.md} with type-equiv
blocks + manifest entries; catalog types moved from exemptions to
LINK_MAP; website page registered.
Both packages stay at per-file 100% coverage.
Two-package capability family mirroring session-persistence/:
- dsh-settings: abstract Settings service — namespace registry with
caller-fiber effect registrations, layered resolution (schema defaults
< composition base < user document), schemastery validation,
per-namespace deep-equal commit detection, and the settings/updated
event. Boot/registration validation fails loud; provider publishes
keep last-good per namespace.
- dsh-settings-local: settings.yaml/.json provider — resolveSpec
defaulting to $DSH_HOME/settings.yaml, chokidar hot reload,
content-equality self-write suppression, atomic 0600 tmp+rename
writes, comment-preserving YAML namespace patching.
Consumers register inside ctx.inject(['settings'], …), so every
composition works unchanged without a mounted provider. Real Loader +
Include composition test proves cordis.yml boot and external-edit hot
propagation; HMR disposal test proves registry cleanup. Both packages
hold per-file 100% coverage.
Doc budgets rise 1705→1710 (AGENTS.md) and 835→845 (packages/README.md):
one structural line per file for the new package group.
Agent Note: .agents/notes/implemented/architecture/2026-07-28-user-settings-seam.md