* feat(cordis): add volatile config references without remounting
* refactor(cordis): use internal prefix for volatile updates
* fix(schemastery): preserve metadata-free serialized schemas
* refactor(config): move volatile updates into an opt-in Loader plugin
* refactor(config): reuse Loader context patch for volatile updates
* refactor(config): let the volatile watcher commit equal candidates
* refactor(config): separate comparison from volatile reconciliation
* refactor(loader): extract pure entry option diff
* refactor(loader): compare volatile config through schema metadata
* refactor(loader): make volatile comparison policy explicit
* refactor: name config reload and volatile HMR events
* refactor(loader): compare volatile config in one schema walk; declare hmr/volatile-update in volatile-hmr
Replace the mask/project pair in vendor/loader/src/config/diff.ts with one recursive schema-guided comparison. Absent objects now compare as their declared schema default at every object node, not only under volatile ancestors; both inputs resolve to the same validated config, so the one changed assertion (undefined vs {} under a non-volatile object schema) becomes unchanged. The isJsExpr special case is dropped: expression keys fall through to raw comparison.
Move the hmr/volatile-update Events declaration from vendored Cordis core to its producer, packages/boot/volatile-hmr, matching how dsh-hmr declares hmr/change and hmr/reload. Vendored cordis events.ts returns to upstream content; the inherited Cordis API catalog and the event matrix are regenerated, and the tutorial example imports the package types.
* fix(cosmokit): compare sparse array slots in deepEqual; extend volatile diff coverage
Array.prototype.every skipped holes, so deepEqual(Array(1), ['new']) was true in one direction. Compare every index with holes read as undefined.
config/diff.ts treats expression objects as opaque values even when a schema field shares the marker name, and looks up field schemas with Object.hasOwn so unknown keys named after Object prototype members compare raw. Tests cover absent-object defaults, sparse arrays in both directions, prototype-named keys, metadata-free serialized schema nodes, and a runtime remount check in volatile-hmr. READMEs and the architecture note describe the object-default rule.
* test(schemastery): build serialized schema fixtures without unknown casts
verify-no-unknown-casts rejects the two as-unknown assertions in scripts/volatile-config.spec.ts. Parse the serialized nodes from JSON text instead, matching scripts/loader-config-diff.spec.ts.
* docs(volatile-hmr): drop the scope-scan tag from hmr/volatile-update
verify-scoped-events rejects @dshScopeScan on an event that carries no scoped keys. Catalogs and the event matrix are regenerated.
* docs: regenerate module graph for the volatile-hmr dependency edge
verify-module-graph reports the merged docs stale: volatile-hmr now lists its app-boot dependency.
* test(volatile-hmr): watch the real YAML through an exact config watch
The Windows coverage lane lost the whole spec: its vitest worker exited without a report once this test started a chokidar directory watch (root: ['.']), the only real directory watch in the test suite. Register the include file through hmr.watchConfig instead, the pattern the HMR package's own real-file tests run on Windows, and emit loader/config-reload from the refresh callback as the root-watch path does. packages/boot/hmr/tests/coordination.spec.ts already covers that root-watch emission with a mocked watcher; volatile-hmr and hmr sources stay at 100% coverage.
* test(volatile-hmr): pass the Include path as a file URL
On Windows the plain absolute path made the include subtree fail to load, so the profile-patch test found no consumer entry. Other Include fixtures in this spec already pass file URLs.
* refactor(loader): commit volatile config in Entry.update and remove volatile-hmr
loader/config-reload was emitted from two places (app-boot profile reconciliation and the HMR root watch) and by hand in tests, because a separate plugin needed a "configuration applied" signal to run its own pass. Every update path already funnels through Entry.update, which is where the Loader decides to skip a remount for a volatile-only change. Entry._commitVolatile now parses the candidate through the fiber's internal/config hook and schema, commits the new values into the running references when the ordinary effective values still match, and emits loader/volatile-update to the owning fiber. A changed ordinary effective value falls back to the ordinary remount instead of leaving the instance stale; an invalid candidate is logged and keeps the running references, with the raw config retained for the next activation. Forced group updates take the same path.
Remove packages/boot/volatile-hmr, its event, the app-boot loader/config-reload declaration and emit, and the HMR emission. Loader runtime and real-file tests move to scripts/loader-volatile-update.spec.ts. Docs, catalogs, the event matrix, module graph and the architecture note follow.
* test: share the volatile schema fixture between the schema and Loader specs
The duplication gate flagged the identical Config fixture in scripts/volatile-config.spec.ts and scripts/loader-volatile-update.spec.ts.
* refactor(loader): reduce config/diff.ts to equalExceptVolatile
Entry.update now computes the strict raw option changes itself and only asks diff.ts whether two raw configs differ at most in volatile fields. The ignoreVolatile flag, the options object and the "config is the sole change" rule leave diff.ts; they were the Loader's decision and now sit next to the commit they gate. The raw-config deepEqual in the volatile branch is gone because the strict change list already answers it.
* refactor(loader): type the volatile comparison against Schemastery's Schema
config/diff.ts narrows the plugin's Standard Schema to Schemastery's own Schema type instead of a hand-written structural interface, which makes vendor/loader a type-only dependent of @deepseek-ai/schemastery. The rescope codemod treats the vendor identifier comparison like schemastery's own source and renames the import through an exact edit.
* fix(loader): address review findings on the volatile commit path
Entry.update only replaces the fiber's pending raw config for a volatile-only change, so a noSave direct update survives a no-op entry update. _commitVolatile logs at debug level when ordinary effective values changed and the ordinary update applies, dispatches loader/volatile-update with a Context receiver like Cordis' own filtered emits, and isolates a throwing listener. Schemastery reports unsupported volatile values as ValidationError with their path and validates a lazy inner once when it is built. The inherited-event catalog gains internal/config and corrected source lines; the Loader spec imports dsh-hmr by package name and covers the new behaviors. Docs record that class-instance ordinary fields always take the remount path and that .default() accepts the raw input type.
Vendored Packages
This directory contains source-vendored copies of the Cordis framework and its foundation libraries. They are copied into this monorepo instead of being depended on via npm, so that the harness fully owns its framework layer (auditable, patchable, pinned).
All vendored packages use the @deepseek-ai scope (cordis → @deepseek-ai/cordis, @cordisjs/plugin-<x> → @deepseek-ai/cordis-plugin-<x>). The manifest table records upstream versions and source commits; each package manifest carries its Harness release version and publication metadata. Repository-owned runtime dependencies use workspace:^, so local builds resolve the pinned workspace packages and publication substitutes release ranges. Upstream MIT LICENSE files are preserved in each package directory.
This file covers the manifest, the local-modification log, and the procedure for updating an existing vendored package. To add a new one, see the cookbook guide: docs/cookbook/adding-a-vendored-package.md.
Manifest
Upstream workspace: cordis-workspace (local checkout: ~/repos/cordis-workspace).
| Directory | npm name | Upstream name | Version | Upstream repo | Commit |
|---|---|---|---|---|---|
cosmokit/ |
@deepseek-ai/cosmokit |
cosmokit |
1.8.1 | https://github.com/deepseek-harness/cosmokit | 16f6fc058ade66e8ac5da0033d35a8d0f279f544 |
schemastery/ |
@deepseek-ai/schemastery |
schemastery |
3.18.0 | https://github.com/deepseek-harness/schemastery (packages/core) |
e67cee00ad725bd1534aee930a979ea3eec6f698 |
cordis/ |
@deepseek-ai/cordis |
cordis |
4.0.0-rc.7 | https://github.com/cordiverse/cordis (packages/core) |
56b3d4f725681cf4556c1a8695a709cc3b6eed74 |
loader/ |
@deepseek-ai/cordis-plugin-loader |
@cordisjs/plugin-loader |
1.0.0-rc.5 | https://github.com/cordiverse/cordis (packages/loader) |
56b3d4f725681cf4556c1a8695a709cc3b6eed74 |
include/ |
@deepseek-ai/cordis-plugin-include |
@cordisjs/plugin-include |
1.0.4 | https://github.com/deepseek-harness/cordis (packages/include) |
abb0a307cb1d3b0947f455d590cf5ba922d4caa4 |
group/ |
@deepseek-ai/cordis-plugin-group |
@cordisjs/plugin-group |
1.0.0 | https://github.com/deepseek-harness/cordis (packages/group) |
abb0a307cb1d3b0947f455d590cf5ba922d4caa4 |
timer/ |
@deepseek-ai/cordis-plugin-timer |
@cordisjs/plugin-timer |
1.1.2 | https://github.com/deepseek-harness/cordis (packages/timer) |
abb0a307cb1d3b0947f455d590cf5ba922d4caa4 |
hmr/ |
@deepseek-ai/cordis-plugin-hmr |
@cordisjs/plugin-hmr |
1.0.15 | https://github.com/deepseek-harness/cordis (packages/hmr) |
abb0a307cb1d3b0947f455d590cf5ba922d4caa4 |
logger-console/ |
@deepseek-ai/cordis-plugin-logger-console |
@cordisjs/plugin-logger-console |
1.0.0 | https://github.com/deepseek-harness/cordis (packages/logger-console) |
abb0a307cb1d3b0947f455d590cf5ba922d4caa4 |
Third-party dependencies of the vendored packages stay on npm: @standard-schema/spec, js-yaml, chokidar, picomatch, @babel/code-frame, supports-color, node-addon-require-builtin.
Intentionally not vendored (verified unused by this set): reggol, @cordisjs/utils, @cordisjs/element, @cordisjs/unyaml (dev-time YAML import hook only).
Local modifications
Keep this log exhaustive — every divergence from upstream must be listed.
-
hmr/src/index.ts: removed the./locales/en-US.yml/./locales/zh-CN.ymlimports, the.i18n({...})call on theConfigschema, and thesrc/locales/directory. Rationale: those imports require a runtime YAML loader hook (@cordisjs/unyaml) that we do not vendor; the i18n texts only localize config descriptions. -
All
package.jsonfiles: regenerated for Harness releases with scoped names, release versions, publication metadata, precise bundled-runtime andlib/types/**/*.d.ts/.d.ts.mapfile entries, source exports where applicable, and declaration metadata pointing atlib/types. Repository-owned dependencies use the workspace protocol. HMR declaresesbuildas a direct dev dependency for its importedBuildFailuretype, and Loader requiresnode-addon-require-builtin@^0.1.4to match published app runtimes. -
All
tsconfig.jsonfiles: regenerated to extend the repo-roottsconfig.base.json, emit TypeScript intermediates tolib/types, and declare project references. -
Vendored TypeScript source internal specifiers: changed local relative imports/exports from upstream's specifier shape to explicit
.tsspecifiers so TypeScript rewrites emitted JS to.jswhile declarations keep explicit, NodeNext-safe.tsspecifiers. This includesloader/src/config/isolate.tsusingdeclare module './entry.ts'. Type-only dependencies useimport typeor inlinetypemodifiers so ESM output does not retain erased interfaces. -
schemastery/tsdown.config.tsandlogger-console/tsdown.config.ts: ours, not upstream files — per-package build-shape overrides (dual ESM+CJS output; separate node/browser entries) for the repo-root tsdown build. They read the JS emitted underlib/typesand then write the publish runtime entries underlib/. Like the regenerated tsconfigs, they are not part of the upstream sync surface. -
cordis/src/fiber.tslifecycle hardening: locally closes three reentrant disposal gaps. An effect's owner-list wrapper is registered before its setup body runs, so an unload begun from inside setup awaits setup and every collected cleanup; synchronous setup failure removes the wrapper and rolls back collected cleanup. Async cleanup stays owner-visible until quiescence, and Cordis's internal effect composition joins an already-running cleanup while repeated public disposer calls retain their upstream single-shot result. Effect creation is rejected while the owner isUNLOADING(whilePENDINGandLOADINGremain legal), preventing cleanup-time registrations from escaping the unload snapshot. Child fibers register and receive their parent-owned disposer beforeinternal/pluginpublication, resolve dependency declarations added by that notification before activation, drain effects attached while pending, skip plugin execution when reentrant disposal invalidates the load epoch before its first checkpoint, and contain teardown-notification failures per observer so one callback cannot starve peers or interrupt ownership cleanup. -
Cordis and Loader JSDoc enrichment: added
@param/@returnstags and contract documentation (disposal semantics, waterfall veto, bail conditions, error cases) across the public plugin-author surface —Context(class, statics, and theContextinterface properties incl.root), module re-exports, shared utility helpers,EventsService,Fiber,RegistryService,ReflectService,Service,LoggerServiceand theirdeclare module './context.ts'overloads. Loader documentation coversloader/src/{index,internal,config/entry,config/group,config/isolate,config/tree,config/utils}.ts, including entry ownership, tree mutation methods, and the!!jsdiscriminator. Comment-only; no code changes. Motivation: the website API-reference generator renders these docs and hard-errors on undocumented members. Retire this entry when the enrichment is upstreamed to the fork. -
include/src/index.tsresilient file refresh and patch reapplication: #514 validates a top-level entry array before caching parsed content, logs refresh failures, reapplies patches after file or Include-config edits, updates the Include config when it vetoes a restart, and usesinitialonly afterENOENT. These parse protections retain the running tree after an invalid file; plugin activation failures can leave a partially applied tree. Loader entry/group/tree mutations follow the pinned eager, non-transactional implementation and do not restore previous plugins or options. Covered bypackages/boot/app-boot/tests/{config-reload,user-patches}.spec.ts. -
hmr/src/index.tsmodule-watcher readiness and native paths: realpath the existing watch base, classify framework modules and attach listeners before reporting readiness, and compare config paths using both canonical and configured spellings. This preserves Node module-cache identity across Windows short-name paths and filesystem aliases. The main watcher usesignoreInitial: trueso startup reads do not trigger another refresh; exact profile patch watching is owned bypackages/boot/hmr/src/watch-config.ts. Covered bypackages/boot/hmr/tests/watch-config.spec.ts. -
Vendored Node-compatible TypeScript: marked erased imports explicitly across
cordis,loader,include,hmr, andschemasteryso Node's native TypeScript transform does not request types as runtime exports. Schemastery's source uses an ESM default export and its package declarestype: module. Conditional exports select.mjsfor import and.cjsfor require; without them, concurrent imports under module-hook hosts reach the CJS entry and can race its require of ESM Cosmokit (ERR_REQUIRE_ESM_RACE_CONDITION). -
include/src/index.tspatch-semantics export: extracted the privateapplyPatchesbody into the exported pure functionapplyEntryPatches(data, patches, warn)(the method delegates to it) and exported the!!jsYAML dialect asentryListSchema, sodsh --dump-configcomposes and prints exactly what the include would mount without booting a tree. The entry list is deep-cloned when patches are present; without patches, the returned array retains the entry objects. Public patch and Include fields carry JSDoc for config tooling. Behavior-preserving for mounting; the extraction exists because config tooling must never reimplement (and drift from) the patch algorithm.applyEntryPatchesalso indexes eachinserted entry as it is added, so a later patch in the same list can configure or disable a row an earlier patch inserted; upstream built the id index once before the patch loop, leaving inserted rows silently unpatchable. That matters becausedshcomposes an empty profile root with each bundle's patch layer, the profile's and the home-levelcordis.patch.yml, and any--patchoverlays as sibling patch lists at one include level — patches never cross an include boundary, so surface-only rows would otherwise be unreachable from user config. Covered bypackages/boot/app-boot/tests/config-reload.spec.ts. -
loader/src/config/entry.tsactivation observer: the detachedEntry.init()completion observer handles both outcomes; the fiber retains its activation error for explicit consumer audits. Covered bypackages/boot/app-boot/tests/user-patches.spec.ts. -
include/src/index.tswriteTasktype: widened the optionalwriteTask?: NodeJS.Timeoutproperty toNodeJS.Timeout | undefined— the debounced writer assignsundefinedon flush, whichexactOptionalPropertyTypesrejects on a plain optional. Type-only; no behavior change. -
include/src/index.tsdurable debounced writes: serialized and tracked config-file writes, retried transientEACCES/EBUSY/EPERMrename failures with a bounded backoff, observed asynchronous timer rejections, and drained writes before and after child removal during Include teardown. The first drain preserves an existing terminal write failure even if child removal schedules a later write. Missing-file initialization awaits the write and forces a fresh parse before mounting the initial entries. Windows can briefly retain a destination handle after a Loader child disposes; the upstream fire-and-forget rename escaped as an unhandled rejection and could lose the persisteddisabledstate. A terminal failure is logged by the asynchronous writer and remains on the queue soInclude.stop()rethrows it instead of silently declaring persistence complete; Cordis's ordinary fiber teardown retains its separate error-containment contract. Covered bypackages/host/directory-picker-auto/tests/loader-composition.spec.tswith injected transient and terminal rename failures. -
Lazy Loader config resolution across
cordis/src/{events,fiber}.ts,loader/src/{index,config/entry}.ts,include/src/index.ts, andhmr/src/index.ts: ports cordiverse/cordis#41, retaining raw fiber config and resolving it throughinternal/configonly after declared injections are active. Provider replacement re-resolves the raw expression, pending updates retain it, and HMR transfers it. Resolution applies only to the entry root, so child plugins mounted by a row keep caller-owned config identity. Include declares theEntryGroup.keytree-carrier marker (as Group does): its config is entry and patch lists, so interpolation keeps it literal and a!!jsexpression inside a nested row's config resolves lazily in that row's own fiber (Include's ownpaththerefore stays literal too). Deferred failures retain the owning row diagnostic, and tree teardown does not persist failure-driven self-disposal. Covered bypackages/boot/app-boot/tests/{app-boot,user-patches}.spec.ts,packages/boot/cmdline/tests/cmdline.spec.ts,apps/cli/tests/web-agent-presets.e2e.ts, and the built custom-profile cases inapps/cli/tests/built-bin.e2e.ts. -
cordis/package.jsonpublishessrc: addedsrcto thefileslist, joining the other eight vendored packages. Cordis declares"./src/*": "./src/*"in its exports, so a tarball withoutsrcpublishes an export map pointing at absent files; the release change judgement also readsfilesto decide whether a diff reaches the payload, and a package whose only published paths are build output has no tracked path to match. -
@deepseek-airescope: every vendored manifestname, every internal dependency entry among the vendored set, and every module specifier that reaches them use the scoped names in the manifest table'snpm namecolumn. Directory names, version numbers, and dependency ranges are unchanged, and no upstream runtime identifier is renamed —Symbol.for('schemastery')and Schemastery'svendor:metadata field keep their upstream values. Re-apply withpnpm run rescope-vendor --applyafter a sync; the table's two name columns are the mapping, restated for consumers in docs/rescope.md. -
Entry
disabledinterpolation inloader/src/config/entry.ts: adisabled: !!jsexpression evaluates against the loader context at every mount decision; the raw node stays in the options, so write-back keeps the!!jsform.disabledis the only interpolated metadata field. Covered bypackages/boot/app-boot/tests/user-patches.spec.tsandapps/cli/tests/windows-shell.spec.ts. -
loader/src/internal.tsruntime shape detection:ModuleLoader.fromInternal()classifies the internal loader by which module-job API it owns —getOrCreateModuleJobfor v2,getModuleJobForImportfor v1 — instead of by Node major version. Upstream tags every major>= 24as v2, but the v2 interface arrived in Node 24.12.0, so 24.0–24.11.1 report major 24 while still carrying the v1 loader; consumers then calledresolveSyncwith reversed parameters and every call threw.dsh webserved an empty client graph (__DSH_BOOT__.entries: []) and HMR partial reload resolved no entry URL, both behind swallowed or warn-level errors. Arity cannot discriminate the two shapes, because each reportsresolveSync.length === 2. A loader owning neither API is left unclassified rather than guessed, so consumers take their documented no-internals path. Covered on thenode-compatNode version matrix, which pins 24.9 for the mistagged range. -
loader/src/config/entry.tsfiber identity: stores the original fiber from the registry result’s context instead of its PromiseLike wrapper. Configuration updates and service notifications therefore mutate the same lifecycle state; updating a provider and consumer together cannot strand the consumer inPENDING. Covered bypackages/boot/hmr/tests/modules.spec.tsand the built profile reload regression inapps/cli/tests/built-bin.e2e.ts. -
cordis/src/logger.tsexporter disposal: each disposer retains its registration id, so removing an earlier exporter cannot delete a later console or telemetry exporter. Covered by startup collector cleanup inpackages/boot/app-boot/tests/app-boot.spec.tsand disabled-feedback output inpackages/session/session-telemetry-otel/tests/loader-composition.e2e.ts. -
Volatile config across
cosmokit,schemastery,cordis, andloader: Cosmokit supplies immutable references and framework-owned commits;deepEqualtreats two references as equal, propagates strict comparison into arrays, compares every array slot with holes treated as undefined, handles cycles conservatively, compares URLs by normalized href, and compares other opaque objects by identity in strict mode. Schemastery adds.volatile(), optional/default inference, placement validation without eager lazy-schema expansion (a lazy inner is validated once when built), metadata-free serialized node compatibility, serialized metadata, reference-aware simplification, unsupported volatile values reported asValidationErrorwith their path, and.default()accepting the raw input typePartial<S>because a volatile field makes the output type carry references. Loader's save path callssimplifywith its schema receiver (unparse.call), an independent fix for the unbound-thiscall. Cordis exports consumer types without changing Fiber lifecycle. Loader compares raw options strictly inEntry.update; for a config-only change on an active fiber it calls the pureequalExceptVolatilefunction inconfig/diff.ts, which narrows the Standard Schema to Schemastery'sSchematype (a new type-only@deepseek-ai/schemasterydependency ofloader) and recursively skips fixed volatile paths using schema metadata without executing config hooks or validation. Missing or null object nodes compare using their declared defaults, including nodes without volatile descendants; schema backedges retain raw comparison. Entry passes its actual schema and preserves ordinary context and custom-update lifecycles. For a volatile-only change,Entry._commitVolatilere-parses the raw config through the fiber'sinternal/confighook andresolveConfig, compares ordinary effective values, commits references and emits the instance-localloader/volatile-updatedeclared inloader/src/index.ts; a changed ordinary effective value falls back to the ordinary remount, and an invalid candidate is logged while the raw config stays retained for the next activation. Cosmokit provides reference enumeration and commits, andsimplifyis called with its schema receiver. The package READMEs document these additions; schema tests live inscripts/volatile-config.spec.ts, Loader runtime and real-file tests inscripts/loader-volatile-update.spec.ts, and code replacement tests in the HMR module suite.
Sync procedure
To update a vendored package from upstream:
- In the upstream workspace, note
git rev-parse HEADof the relevant submodule. - Copy the package's
src/(andbin.js,README.md,LICENSEif changed) over the vendored directory. - Re-apply the local modifications listed above (or drop them if upstream made them unnecessary — update the log either way).
- Update the version and commit hash in the manifest table.
- Run
pnpm install && pnpm run test && pnpm run buildat the repo root.