Merge origin/master into feat/website-docs

Conflict resolution notes:
- package.json/run-gates: both sides' new doc-sync gates kept (master's
  scoped-events/readme gates + this branch's website-api/website-yaml);
  js-yaml devDeps deduped (master added them independently).
- pnpm-workspace/knip: website AND python/sdk-runtime entries kept.
- doc-typecheck/verify-type-equiv: master's condensed headers kept, website
  glob retained in both scan scopes.
- vendor/cordis/src/fiber.ts: master's lifecycle-hardening code taken; this
  branch's richer FiberState JSDoc reapplied on top. vendor/README.md logs
  both local modifications (hardening = 6, JSDoc enrichment = 7).
- pnpm-lock: regenerated from master's side (pnpm install).

Post-merge sync the gates forced (the system working as designed):
- verify-website-yaml caught 4 stale plugin names from master's package
  reorg (dsh-stdio-agent -> dsh-stdio-demo, dsh-acp-agent -> dsh-acp-demo);
  8 references fixed across guide/ and develop/.
- gen-website-api picked up master's 6 new services automatically
  (ctx.approval/permission/sandbox/sessionQuery/skills/tasks -> 6 new pages
  + sidebar); api/index.md hub updated to list them.
- AGENTS.md budget ceiling 1370 -> 1400: the website rows (layout line + two
  command lines) and master's own growth collided with the old ceiling; all
  three website rows are load-bearing (new top-level dir, new CI command).
This commit is contained in:
lintianle
2026-07-16 21:36:43 +08:00
1132 changed files with 71533 additions and 19566 deletions

9
vendor/README.md vendored
View File

@@ -14,15 +14,15 @@ Upstream workspace: `cordis-workspace` (local checkout: `~/repos/cordis-workspac
|---|---|---|---|---|
| `cosmokit/` | `cosmokit` | 1.8.1 | https://github.com/deepseek-harness/cosmokit | `16f6fc058ade66e8ac5da0033d35a8d0f279f544` |
| `schemastery/` | `schemastery` | 3.18.0 | https://github.com/deepseek-harness/schemastery (`packages/core`) | `e67cee00ad725bd1534aee930a979ea3eec6f698` |
| `cordis/` | `cordis` | 4.0.0-rc.6 | https://github.com/deepseek-harness/cordis (`packages/core`) | `abb0a307cb1d3b0947f455d590cf5ba922d4caa4` |
| `loader/` | `@cordisjs/plugin-loader` | 1.0.0-rc.4 | https://github.com/deepseek-harness/cordis (`packages/loader`) | `abb0a307cb1d3b0947f455d590cf5ba922d4caa4` |
| `cordis/` | `cordis` | 4.0.0-rc.7 | https://github.com/cordiverse/cordis (`packages/core`) | `56b3d4f725681cf4556c1a8695a709cc3b6eed74` |
| `loader/` | `@cordisjs/plugin-loader` | 1.0.0-rc.5 | https://github.com/cordiverse/cordis (`packages/loader`) | `56b3d4f725681cf4556c1a8695a709cc3b6eed74` |
| `include/` | `@cordisjs/plugin-include` | 1.0.4 | https://github.com/deepseek-harness/cordis (`packages/include`) | `abb0a307cb1d3b0947f455d590cf5ba922d4caa4` |
| `group/` | `@cordisjs/plugin-group` | 1.0.0 | https://github.com/deepseek-harness/cordis (`packages/group`) | `abb0a307cb1d3b0947f455d590cf5ba922d4caa4` |
| `timer/` | `@cordisjs/plugin-timer` | 1.1.2 | https://github.com/deepseek-harness/cordis (`packages/timer`) | `abb0a307cb1d3b0947f455d590cf5ba922d4caa4` |
| `hmr/` | `@cordisjs/plugin-hmr` | 1.0.15 | https://github.com/deepseek-harness/cordis (`packages/hmr`) | `abb0a307cb1d3b0947f455d590cf5ba922d4caa4` |
| `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`.
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).
@@ -35,7 +35,8 @@ Keep this log exhaustive — every divergence from upstream must be listed.
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'`.
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/*.ts` 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`), `EventsService`, `Fiber`, `RegistryService`, `ReflectService`, `Service`, `LoggerService` and their `declare module './context.ts'` overloads. 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.
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/src/*.ts` 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`), `EventsService`, `Fiber`, `RegistryService`, `ReflectService`, `Service`, `LoggerService` and their `declare module './context.ts'` overloads. 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.
## Sync procedure

View File

@@ -1,7 +1,7 @@
{
"name": "cordis",
"description": "Meta-Framework for Modern JavaScript Applications",
"version": "4.0.0-rc.6",
"version": "4.0.0-rc.7",
"private": true,
"sideEffects": false,
"type": "module",
@@ -26,7 +26,7 @@
"license": "MIT",
"peerDependencies": {
"@cordisjs/plugin-include": "^1.0.4",
"@cordisjs/plugin-loader": "^1.0.0-rc.4"
"@cordisjs/plugin-loader": "^1.0.0-rc.5"
},
"peerDependenciesMeta": {
"@cordisjs/plugin-include": {

View File

@@ -180,7 +180,9 @@ export class EventsService {
* @returns a promise resolving once every listener has settled.
*/
async parallel(...args: any[]) {
await Promise.all(this.dispatch('emit', args).map(cb => cb(...args)))
const results = await Promise.allSettled(this.dispatch('emit', args).map(async cb => cb(...args)))
const errors = results.filter((result): result is PromiseRejectedResult => result.status === 'rejected')
if (errors.length) throw new AggregateError(errors.map(error => error.reason))
}
/**

View File

@@ -106,6 +106,35 @@ interface EffectRunner<T> {
getOuterStack: () => string[]
}
// Public effect disposers remain single-shot, but structural owners and outer
// effects must still be able to join a cleanup that another caller started.
const effectInertia = new WeakMap<Disposable, () => void | Promise<void>>()
function runDisposable(dispose: Disposable) {
const result = dispose()
return effectInertia.get(dispose)?.() ?? result
}
/** Notify plugin teardown without allowing one observer to break ownership cleanup. */
function emitPluginDisposed(context: Context, fiber: Fiber) {
const args: any[] = ['internal/plugin', fiber]
let callbacks: Function[]
try {
callbacks = context.events.dispatch('emit', args)
} catch (error) {
context.logger.error(error)
return
}
for (const callback of callbacks) {
try {
const returned = callback(...args)
void Promise.resolve(returned).catch(error => context.logger.error(error))
} catch (error) {
context.logger.error(error)
}
}
}
/**
* Lifecycle state for one plugin fiber.
*
@@ -214,7 +243,7 @@ export class Fiber {
this._runner = {
epoch: INACTIVE,
getOuterStack,
execute: () => {
execute: function () {
if (isConstructor(runtime.callback)) {
// eslint-disable-next-line new-cap
const instance = new runtime.callback(this.ctx, this.config)
@@ -229,24 +258,19 @@ export class Fiber {
collect,
}
this.context.emit('internal/plugin', this)
for (const name of Object.keys(this.inject)) {
this._checkImpl(name)
}
let shouldRefresh = false
this.dispose = parent.fiber.effect(() => {
const remove = runtime.fibers.push(this)
try {
this.config = resolveConfig(runtime, config)
this._refresh()
shouldRefresh = true
} catch (error) {
this.ctx.logger.error(error)
this._error = error
}
return async () => {
this.uid = null
this.context.emit('internal/plugin', this)
emitPluginDisposed(this.context, this)
if (this.ctx.registry.has(runtime.callback)) {
remove()
if (!runtime.fibers.length) {
@@ -254,6 +278,16 @@ export class Fiber {
}
}
this._setEpoch(INACTIVE)
// A PENDING fiber can already own effects registered by an
// internal/plugin observer. Its epoch is still INACTIVE, so
// _setEpoch() has no transition to drive; explicitly unload that
// pre-activation work before reporting disposal complete.
if (!this.inertia) {
this._updateState(() => {
this.inertia = this._unload()
return FiberState.UNLOADING
})
}
// `this.inertia` itself should never reject — both `_reload` and
// `_unload` swallow their own work errors via `ctx.logger.error`.
// If it *does* reject, the only remaining cause is the logger
@@ -265,6 +299,28 @@ export class Fiber {
}
}
}, 'ctx.plugin()')
try {
// Publish only after the parent owns a fully assigned disposer. A
// synchronous observer may dispose either this fiber or its parent.
this.context.emit('internal/plugin', this)
} catch (error) {
// Publication failed synchronously. The disposer removes the child
// from both the parent and runtime before control escapes.
void Promise.resolve(this.dispose()).catch(reason => this.ctx.logger.error(reason))
throw error
}
// Keep the initial notification's historical PENDING view. The loader
// may also extend `inject` in that notification, so resolve dependencies
// only after publication. A reentrant parent unload makes the child
// disposer responsible for draining any PENDING effects instead.
if (this.uid !== null && parent.fiber.state !== FiberState.UNLOADING) {
for (const name of Object.keys(this.inject)) {
this._checkImpl(name)
}
if (shouldRefresh) this._refresh()
}
} else {
this.uid = 0
this.ctx = this.context = parent
@@ -311,7 +367,7 @@ export class Fiber {
throw new TypeError('Invalid effect')
}
}
const effect: Effect = runner.execute()
const effect: Effect = runner.execute.call(this)
if (typeof effect === 'function') {
return runner.collect(effect)
} else if (isNullable(effect)) {
@@ -365,21 +421,28 @@ export class Fiber {
effect(execute: () => Effect, label?: string): AsyncDisposable<Promise<void>>
effect(execute: () => Effect, label = 'anonymous'): any {
this.assertActive()
if (this.state === FiberState.UNLOADING) {
throw new CordisError('INACTIVE_EFFECT')
}
const disposables: Disposable[] = []
let disposing = false
let disposalTask: void | Promise<void>
const dispose = () => {
if (disposing) return disposalTask
disposing = true
let task!: void | Promise<void>
for (const dispose of disposables.splice(0).reverse()) {
for (const disposable of disposables.splice(0).reverse()) {
if (task) {
task = task.then(dispose)
task = task.then(() => runDisposable(disposable))
} else {
const result = dispose()
const result = runDisposable(disposable)
if (isObject(result) && 'then' in result) {
task = result as any
}
}
}
return task
return disposalTask = task
}
const meta: EffectMeta = { label, children: [] }
@@ -397,34 +460,107 @@ export class Fiber {
}
let task: void | Promise<void>
let executing = true
let resolveSetup: (() => void) | undefined
let rejectSetup: ((reason: unknown) => void) | undefined
let setupBarrier: Promise<void> | undefined
let setupFailed = false
let inFlight: void | Promise<void>
let removeWrapper = () => false
const waitForSetup = () => {
setupBarrier ??= new Promise<void>((resolve, reject) => {
resolveSetup = resolve
rejectSetup = reject
})
return setupBarrier
}
const disposeAfter = (setup: PromiseLike<void>) => {
return Promise.resolve(setup).then(
() => dispose(),
async (reason) => {
await dispose()
throw reason
},
)
}
const finalizeDisposal = (callback: () => void | Promise<void>) => {
let result: void | Promise<void>
try {
result = callback()
} catch (error) {
removeWrapper()
throw error
}
if (isObject(result) && 'then' in result) {
const pending = Promise.resolve(result).finally(() => {
removeWrapper()
if (inFlight === pending) inFlight = undefined
})
return inFlight = pending
}
removeWrapper()
return result
}
const wrapper = defineProperty(() => {
// A synchronous setup failure can race an owner unload that already
// captured this wrapper but has not invoked it yet. The failed effect is
// never returned publicly, so let that internal caller await rollback.
if (!runner.epoch) return setupFailed ? inFlight : undefined
runner.epoch = false
return finalizeDisposal(() => {
if (executing) return disposeAfter(waitForSetup())
return task ? disposeAfter(task) : dispose()
})
}, symbols.effect, meta) as AsyncDisposable
effectInertia.set(wrapper, () => inFlight)
// Make the effect visible to a reentrant owner unload before execute()
// runs any plugin code. Async teardown stays owner-visible until it
// settles, allowing an outer effect to join cleanup another caller began.
removeWrapper = this._disposables.push(wrapper)
try {
task = this._execute(runner)
} catch (reason) {
dispose()
executing = false
setupFailed = true
runner.epoch = false
let cleanup: void | Promise<void>
try {
cleanup = finalizeDisposal(dispose)
} finally {
rejectSetup?.(reason)
}
if (isObject(cleanup) && 'then' in cleanup) {
cleanup.catch(error => this.ctx.logger.error(error))
}
throw reason
}
executing = false
if (setupBarrier) {
Promise.resolve(task).then(resolveSetup, rejectSetup)
}
// prevent unhandled rejection — both from `task` itself and from the
// disposer chain if it fails to settle cleanly.
task?.catch(dispose).catch((error) => this.ctx.logger.error(error))
const wrapper = defineProperty(() => {
if (!runner.epoch) return
runner.epoch = false
return task ? task.then(dispose) : dispose()
}, symbols.effect, meta) as AsyncDisposable
task?.catch(() => {
if (!runner.epoch) return dispose()
return finalizeDisposal(dispose)
}).catch((error) => this.ctx.logger.error(error))
const disposeAsync = () => {
if (!runner.epoch) return
runner.epoch = false
return dispose()
return finalizeDisposal(dispose)
}
wrapper.then = async (onFulfilled, onRejected) => {
return Promise.resolve(task)
.then(() => disposeAsync)
.then(onFulfilled, onRejected)
}
disposables.push(this._disposables.push(wrapper))
return wrapper
}
@@ -511,7 +647,12 @@ export class Fiber {
const oldEpoch = this._runner.epoch
try {
await Promise.resolve()
await this._execute(this._runner)
// A disposer queued before this checkpoint may already have invalidated
// the load. Do not run plugin code for a stale epoch; the state update
// below will drain any effects collected while the fiber was PENDING.
if (this._runner.epoch === oldEpoch) {
await this._execute(this._runner)
}
} catch (reason) {
// impl guarantees that the error is non-null (?)
this.ctx.logger.error(reason)
@@ -534,7 +675,7 @@ export class Fiber {
await composeError(async (info) => {
await Promise.resolve()
info.error = new Error()
await dispose()
await runDisposable(dispose)
}, this._runner.getOuterStack)
} catch (reason) {
this.ctx.logger.error(reason)

View File

@@ -326,6 +326,11 @@ export class ReflectService {
fibers.push(fiber)
}
}
for (const name of names) {
const self: Context = Object.create(this.ctx)
self[symbols.filter] = (target: Context) => filter(target, name)
this.ctx.events.emit(self, 'internal/service', name, this._getImpl(name, false)?.value)
}
return fibers
}

View File

@@ -23,7 +23,7 @@
"author": "Shigma <shigma10826@gmail.com>",
"license": "MIT",
"peerDependencies": {
"@cordisjs/plugin-loader": "^1.0.0-rc.4",
"cordis": "^4.0.0-rc.6"
"@cordisjs/plugin-loader": "^1.0.0-rc.5",
"cordis": "^4.0.0-rc.7"
}
}

View File

@@ -35,7 +35,7 @@
},
"peerDependencies": {
"@cordisjs/plugin-timer": "^1.1.2",
"cordis": "^4.0.0-rc.6"
"cordis": "^4.0.0-rc.7"
},
"dependencies": {
"@babel/code-frame": "^7.29.0",

View File

@@ -23,8 +23,8 @@
"author": "Shigma <shigma10826@gmail.com>",
"license": "MIT",
"peerDependencies": {
"@cordisjs/plugin-loader": "^1.0.0-rc.4",
"cordis": "^4.0.0-rc.6"
"@cordisjs/plugin-loader": "^1.0.0-rc.5",
"cordis": "^4.0.0-rc.7"
},
"dependencies": {
"cosmokit": "^1.8.1",

View File

@@ -1,7 +1,7 @@
{
"name": "@cordisjs/plugin-loader",
"description": "Plugin loader for cordis",
"version": "1.0.0-rc.4",
"version": "1.0.0-rc.5",
"private": true,
"type": "module",
"main": "lib/index.js",
@@ -23,7 +23,13 @@
"author": "Shigma <shigma10826@gmail.com>",
"license": "MIT",
"peerDependencies": {
"cordis": "^4.0.0-rc.6"
"cordis": "^4.0.0-rc.7",
"node-addon-require-builtin": "^0.1.0"
},
"peerDependenciesMeta": {
"node-addon-require-builtin": {
"optional": true
}
},
"dependencies": {
"cosmokit": "^1.8.1"

View File

@@ -105,18 +105,28 @@ export type ModuleLoader = ModuleLoaderV1 | ModuleLoaderV2
export namespace ModuleLoader {
let _cachedLoader: ModuleLoader | undefined
export function fromInternal(): ModuleLoader | undefined {
if (!process.execArgv.includes('--expose-internals')) return
if (_cachedLoader) return _cachedLoader
function requireInternal(id: string): any {
const require = createRequire(import.meta.url)
if (process.execArgv.includes('--expose-internals')) {
try {
return require(id)
} catch {}
}
try {
return require('node-addon-require-builtin').requireBuiltin(id)
} catch {}
}
export function fromInternal(): ModuleLoader | undefined {
if (_cachedLoader) return _cachedLoader
const [major] = process.versions.node.split('.').map(Number)
if (major >= 24) {
const raw = require('internal/modules/esm/loader').getOrInitializeCascadedLoader()
return _cachedLoader = Object.assign(raw, { version: 'v2' })
const raw = requireInternal('internal/modules/esm/loader')?.getOrInitializeCascadedLoader()
if (raw) return _cachedLoader = Object.assign(raw, { version: 'v2' })
} else if (major >= 22) {
const raw = require('internal/modules/esm/loader').getOrInitializeCascadedLoader()
return _cachedLoader = Object.assign(raw, { version: 'v1' })
const raw = requireInternal('internal/modules/esm/loader')?.getOrInitializeCascadedLoader()
if (raw) return _cachedLoader = Object.assign(raw, { version: 'v1' })
}
}
}

View File

@@ -25,7 +25,7 @@
"author": "Shigma <shigma10826@gmail.com>",
"license": "MIT",
"peerDependencies": {
"cordis": "^4.0.0-rc.6"
"cordis": "^4.0.0-rc.7"
},
"dependencies": {
"cosmokit": "^1.8.1",

View File

@@ -23,7 +23,7 @@
"author": "Shigma <shigma10826@gmail.com>",
"license": "MIT",
"peerDependencies": {
"cordis": "^4.0.0-rc.6"
"cordis": "^4.0.0-rc.7"
},
"dependencies": {
"cosmokit": "^1.8.1"