mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
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.
167 lines
5.4 KiB
TypeScript
167 lines
5.4 KiB
TypeScript
import { composeError, Context } from '@deepseek-ai/cordis'
|
|
import { isNonNullable, type Dict } from '@deepseek-ai/cosmokit'
|
|
import { Entry, type EntryOptions } from './entry.ts'
|
|
import { EntryGroup } from './group.ts'
|
|
|
|
/** Mutable tree of loader entries. Persistence is supplied by subclasses. */
|
|
export abstract class EntryTree {
|
|
static readonly sep = ':'
|
|
|
|
public ctx: Context
|
|
public enableLogs?: boolean
|
|
public root: EntryGroup
|
|
public store: Dict<Entry> = Object.create(null)
|
|
|
|
constructor(ctx: Context) {
|
|
this.ctx = ctx.extend({ baseUrl: ctx.baseUrl })
|
|
this.root = new EntryGroup(this.ctx, this)
|
|
const entry = this.ctx.fiber.entry
|
|
if (entry) entry.subtree = this
|
|
}
|
|
|
|
get context(): Context {
|
|
return this.ctx
|
|
}
|
|
|
|
/** Iterate entries in this tree and any nested subtrees. */
|
|
* entries(): Generator<Entry, void, void> {
|
|
for (const entry of Object.values(this.store)) {
|
|
yield entry
|
|
if (!entry.subtree) continue
|
|
yield* entry.subtree.entries()
|
|
}
|
|
}
|
|
|
|
/** Return pending import and lifecycle tasks owned by this tree. */
|
|
getTasks() {
|
|
return [...this.entries()]
|
|
.map(entry => entry._initTask || entry.fiber?.inertia)
|
|
.filter(isNonNullable)
|
|
}
|
|
|
|
/**
|
|
* Wait until this tree has no active import or lifecycle tasks.
|
|
* @throws a settled fiber failure, or an aggregate when several fibers failed.
|
|
*/
|
|
async await() {
|
|
while (true) {
|
|
const tasks = this.getTasks()
|
|
if (tasks.length) {
|
|
await Promise.allSettled(tasks)
|
|
continue
|
|
}
|
|
const outcomes = await Promise.allSettled(
|
|
[...this.entries()].map(entry => entry.fiber?.await()),
|
|
)
|
|
const failures = outcomes
|
|
.filter((outcome): outcome is PromiseRejectedResult => outcome.status === 'rejected')
|
|
.map(outcome => outcome.reason)
|
|
if (failures.length === 1) throw failures[0]
|
|
if (failures.length > 1) throw new AggregateError(failures, 'loader fibers failed')
|
|
this.ctx.reflect.notify(['loader'])
|
|
if (!this.getTasks().length) return
|
|
}
|
|
}
|
|
|
|
ensureId(options: Partial<EntryOptions>) {
|
|
if (!options.id) {
|
|
do {
|
|
options.id = Math.random().toString(16).slice(2, 10)
|
|
} while (this.store[options.id])
|
|
}
|
|
return options.id!
|
|
}
|
|
|
|
/** Resolve an entry by id, including nested ids separated by `EntryTree.sep`. */
|
|
resolve(id: string) {
|
|
const parts = id.split(EntryTree.sep)
|
|
let tree: EntryTree | undefined = this
|
|
const final = parts.pop()!
|
|
for (const part of parts) {
|
|
tree = tree.store[part]?.subtree
|
|
if (!tree) throw new Error(`cannot resolve entry ${id}`)
|
|
}
|
|
const entry = tree.store[final]
|
|
if (!entry) throw new Error(`cannot resolve entry ${id}`)
|
|
return entry
|
|
}
|
|
|
|
resolveGroup(id: string | null) {
|
|
if (!id) return this.root
|
|
const entry = this.resolve(id)
|
|
if (!entry.subgroup) throw new Error(`entry ${id} is not a group`)
|
|
return entry.subgroup
|
|
}
|
|
|
|
/** Create an entry in the root group or a nested group. */
|
|
async create(options: Omit<EntryOptions, 'id'>, parent: string | null = null, position = Infinity) {
|
|
const group = this.resolveGroup(parent)
|
|
const id = await group.create(options)
|
|
const entry = this.resolve(id)
|
|
group.data.splice(position, 0, entry.options)
|
|
group.tree.write()
|
|
return id
|
|
}
|
|
|
|
/** Stop and remove an entry from its parent group. */
|
|
async remove(id: string) {
|
|
const entry = this.resolve(id)
|
|
await entry.parent.remove(id)
|
|
entry.parent.tree.write()
|
|
}
|
|
|
|
/** Update an entry and optionally move it to another group. */
|
|
async update(id: string, options: Omit<EntryOptions, 'id' | 'name'>, parent?: string | null, position?: number) {
|
|
const entry = this.resolve(id)
|
|
const source = entry.parent
|
|
const sourceIndex = source.data.indexOf(entry.options)
|
|
let target = source
|
|
if (parent !== undefined) {
|
|
target = this.resolveGroup(parent)
|
|
source.unlink(entry.options)
|
|
target.data.splice(position ?? Infinity, 0, entry.options)
|
|
entry.parent = target
|
|
}
|
|
try {
|
|
await entry.update(options, false, true)
|
|
} catch (error) {
|
|
if (parent !== undefined) {
|
|
target.unlink(entry.options)
|
|
source.data.splice(sourceIndex < 0 ? source.data.length : sourceIndex, 0, entry.options)
|
|
entry.parent = source
|
|
try {
|
|
await entry.update({}, false, true)
|
|
} catch (rollbackError) {
|
|
throw new AggregateError([error, rollbackError], `failed to roll back loader entry move ${id}`)
|
|
}
|
|
}
|
|
throw error
|
|
}
|
|
source.tree.write()
|
|
if (target !== source) target.tree.write()
|
|
}
|
|
|
|
/** Import a plugin module from a specifier or `cordis:` builtin. */
|
|
import(name: string, getOuterStack?: () => string[]) {
|
|
if (name.startsWith('cordis:')) {
|
|
return this.ctx.loader.builtins[name.slice(7)]
|
|
}
|
|
return composeError(async (info) => {
|
|
// ModuleJob.run
|
|
// onImport.tracePromise.__proto__
|
|
// internal.import
|
|
info.offset += 3
|
|
if (this.ctx.loader.internal) {
|
|
return await this.ctx.loader.internal.import(name, this.ctx.baseUrl!, {})
|
|
} else if (name.startsWith('.')) {
|
|
return await import(/* @vite-ignore */new URL(name, this.ctx.baseUrl).href)
|
|
} else {
|
|
return await import(/* @vite-ignore */name)
|
|
}
|
|
}, getOuterStack)
|
|
}
|
|
|
|
/** Persist current tree state. In-memory trees may implement this as a no-op. */
|
|
abstract write(): void
|
|
}
|