Files
deepseek-harness/vendor
Turtle 3f7016a422 feat(config): add volatile schemas and watcher-driven updates (#4579)
* 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.
2026-09-21 00:04:06 +08:00
..

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.

  1. hmr/src/index.ts: removed the ./locales/en-US.yml / ./locales/zh-CN.yml imports, the .i18n({...}) call on the Config schema, and the src/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.

  2. All package.json files: regenerated for Harness releases with scoped names, release versions, publication metadata, precise bundled-runtime and lib/types/**/*.d.ts / .d.ts.map file entries, source exports where applicable, and declaration metadata pointing at lib/types. Repository-owned dependencies use the workspace protocol. HMR declares esbuild as a direct dev dependency for its imported BuildFailure type, and Loader requires node-addon-require-builtin@^0.1.4 to match published app runtimes.

  3. All tsconfig.json files: regenerated to extend the repo-root tsconfig.base.json, emit TypeScript intermediates to lib/types, and declare project references.

  4. Vendored TypeScript source internal specifiers: changed local relative imports/exports from upstream's specifier shape to explicit .ts specifiers so TypeScript rewrites emitted JS to .js while declarations keep explicit, NodeNext-safe .ts specifiers. This includes loader/src/config/isolate.ts using declare module './entry.ts'. Type-only dependencies use import type or inline type modifiers so ESM output does not retain erased interfaces.

  5. schemastery/tsdown.config.ts and logger-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 under lib/types and then write the publish runtime entries under lib/. Like the regenerated tsconfigs, they are not part of the upstream sync surface.

  6. cordis/src/fiber.ts lifecycle 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 is UNLOADING (while PENDING and LOADING remain legal), preventing cleanup-time registrations from escaping the unload snapshot. Child fibers register and receive their parent-owned disposer before internal/plugin publication, 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.

  7. Cordis and Loader JSDoc enrichment: added @param/@returns tags and contract documentation (disposal semantics, waterfall veto, bail conditions, error cases) across the public plugin-author surface — Context (class, statics, and the Context interface properties incl. root), module re-exports, shared utility helpers, EventsService, Fiber, RegistryService, ReflectService, Service, LoggerService and their declare module './context.ts' overloads. Loader documentation covers loader/src/{index,internal,config/entry,config/group,config/isolate,config/tree,config/utils}.ts, including entry ownership, tree mutation methods, and the !!js discriminator. 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.

  8. include/src/index.ts resilient 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 uses initial only after ENOENT. 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 by packages/boot/app-boot/tests/{config-reload,user-patches}.spec.ts.

  9. hmr/src/index.ts module-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 uses ignoreInitial: true so startup reads do not trigger another refresh; exact profile patch watching is owned by packages/boot/hmr/src/watch-config.ts. Covered by packages/boot/hmr/tests/watch-config.spec.ts.

  10. Vendored Node-compatible TypeScript: marked erased imports explicitly across cordis, loader, include, hmr, and schemastery so Node's native TypeScript transform does not request types as runtime exports. Schemastery's source uses an ESM default export and its package declares type: module. Conditional exports select .mjs for import and .cjs for 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).

  11. include/src/index.ts patch-semantics export: extracted the private applyPatches body into the exported pure function applyEntryPatches(data, patches, warn) (the method delegates to it) and exported the !!js YAML dialect as entryListSchema, so dsh --dump-config composes 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. applyEntryPatches also indexes each inserted 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 because dsh composes an empty profile root with each bundle's patch layer, the profile's and the home-level cordis.patch.yml, and any --patch overlays 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 by packages/boot/app-boot/tests/config-reload.spec.ts.

  12. loader/src/config/entry.ts activation observer: the detached Entry.init() completion observer handles both outcomes; the fiber retains its activation error for explicit consumer audits. Covered by packages/boot/app-boot/tests/user-patches.spec.ts.

  13. include/src/index.ts writeTask type: widened the optional writeTask?: NodeJS.Timeout property to NodeJS.Timeout | undefined — the debounced writer assigns undefined on flush, which exactOptionalPropertyTypes rejects on a plain optional. Type-only; no behavior change.

  14. include/src/index.ts durable debounced writes: serialized and tracked config-file writes, retried transient EACCES/EBUSY/EPERM rename 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 persisted disabled state. A terminal failure is logged by the asynchronous writer and remains on the queue so Include.stop() rethrows it instead of silently declaring persistence complete; Cordis's ordinary fiber teardown retains its separate error-containment contract. Covered by packages/host/directory-picker-auto/tests/loader-composition.spec.ts with injected transient and terminal rename failures.

  15. Lazy Loader config resolution across cordis/src/{events,fiber}.ts, loader/src/{index,config/entry}.ts, include/src/index.ts, and hmr/src/index.ts: ports cordiverse/cordis#41, retaining raw fiber config and resolving it through internal/config only 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 the EntryGroup.key tree-carrier marker (as Group does): its config is entry and patch lists, so interpolation keeps it literal and a !!js expression inside a nested row's config resolves lazily in that row's own fiber (Include's own path therefore stays literal too). Deferred failures retain the owning row diagnostic, and tree teardown does not persist failure-driven self-disposal. Covered by packages/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 in apps/cli/tests/built-bin.e2e.ts.

  16. cordis/package.json publishes src: added src to the files list, joining the other eight vendored packages. Cordis declares "./src/*": "./src/*" in its exports, so a tarball without src publishes an export map pointing at absent files; the release change judgement also reads files to decide whether a diff reaches the payload, and a package whose only published paths are build output has no tracked path to match.

  17. @deepseek-ai rescope: every vendored manifest name, every internal dependency entry among the vendored set, and every module specifier that reaches them use the scoped names in the manifest table's npm name column. Directory names, version numbers, and dependency ranges are unchanged, and no upstream runtime identifier is renamed — Symbol.for('schemastery') and Schemastery's vendor: metadata field keep their upstream values. Re-apply with pnpm run rescope-vendor --apply after a sync; the table's two name columns are the mapping, restated for consumers in docs/rescope.md.

  18. Entry disabled interpolation in loader/src/config/entry.ts: a disabled: !!js expression evaluates against the loader context at every mount decision; the raw node stays in the options, so write-back keeps the !!js form. disabled is the only interpolated metadata field. Covered by packages/boot/app-boot/tests/user-patches.spec.ts and apps/cli/tests/windows-shell.spec.ts.

  19. loader/src/internal.ts runtime shape detection: ModuleLoader.fromInternal() classifies the internal loader by which module-job API it owns — getOrCreateModuleJob for v2, getModuleJobForImport for v1 — instead of by Node major version. Upstream tags every major >= 24 as 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 called resolveSync with reversed parameters and every call threw. dsh web served 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 reports resolveSync.length === 2. A loader owning neither API is left unclassified rather than guessed, so consumers take their documented no-internals path. Covered on the node-compat Node version matrix, which pins 24.9 for the mistagged range.

  20. loader/src/config/entry.ts fiber 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 in PENDING. Covered by packages/boot/hmr/tests/modules.spec.ts and the built profile reload regression in apps/cli/tests/built-bin.e2e.ts.

  21. cordis/src/logger.ts exporter 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 in packages/boot/app-boot/tests/app-boot.spec.ts and disabled-feedback output in packages/session/session-telemetry-otel/tests/loader-composition.e2e.ts.

  22. Volatile config across cosmokit, schemastery, cordis, and loader: Cosmokit supplies immutable references and framework-owned commits; deepEqual treats 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 as ValidationError with their path, and .default() accepting the raw input type Partial<S> because a volatile field makes the output type carry references. Loader's save path calls simplify with its schema receiver (unparse.call), an independent fix for the unbound-this call. Cordis exports consumer types without changing Fiber lifecycle. Loader compares raw options strictly in Entry.update; for a config-only change on an active fiber it calls the pure equalExceptVolatile function in config/diff.ts, which narrows the Standard Schema to Schemastery's Schema type (a new type-only @deepseek-ai/schemastery dependency of loader) 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._commitVolatile re-parses the raw config through the fiber's internal/config hook and resolveConfig, compares ordinary effective values, commits references and emits the instance-local loader/volatile-update declared in loader/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, and simplify is called with its schema receiver. The package READMEs document these additions; schema tests live in scripts/volatile-config.spec.ts, Loader runtime and real-file tests in scripts/loader-volatile-update.spec.ts, and code replacement tests in the HMR module suite.

Sync procedure

To update a vendored package from upstream:

  1. In the upstream workspace, note git rev-parse HEAD of the relevant submodule.
  2. Copy the package's src/ (and bin.js, README.md, LICENSE if changed) over the vendored directory.
  3. Re-apply the local modifications listed above (or drop them if upstream made them unnecessary — update the log either way).
  4. Update the version and commit hash in the manifest table.
  5. Run pnpm install && pnpm run test && pnpm run build at the repo root.