Files
deepseek-harness/packages/core/scope/src/store.ts
imccyu ec601ca13d build(vendor): rescope the vendored Cordis packages into @deepseek-ai
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.
2026-08-10 22:04:13 +08:00

268 lines
7.8 KiB
TypeScript

/**
* Shared insertion-ordered storage and effect ownership for scope-aware registries.
*
* @module @deepseek-ai/dsh-scope
*/
import type { Context } from '@deepseek-ai/cordis'
import { scopeChainOf, scopeOf } from './index.ts'
import type { ScopeKey } from './index.ts'
/** One scope's aggregate contribution to a registry. */
export interface ScopeLayer {
/** Whether every table in this layer is empty. */
isEmpty(): boolean
}
/** Internal common read contract for the two entry-table implementations. */
interface EntryValues<V> {
values(): IterableIterator<V>
isEmpty(): boolean
}
/**
* Insertion-ordered named entries with caller-owned duplicate diagnostics.
*
* Values are borrowed. Iterators are live within one nonempty table
* generation; draining the table detaches them from later insertions. Each
* successful insertion returns an idempotent undo for that exact entry.
*/
export class NamedEntries<V> implements EntryValues<V> {
private data = new Map<string, V>()
constructor(
private readonly duplicateError: (name: string) => Error,
) {}
/**
* Insert one unique name.
* @param name - name unique within this table.
* @param value - borrowed value to retain.
* @returns an idempotent undo that removes only this insertion.
*/
insert(name: string, value: V): () => void {
const data = this.data
if (data.has(name)) throw this.duplicateError(name)
data.set(name, value)
let active = true
return () => {
if (!active) return
active = false
data.delete(name)
if (data.size === 0 && this.data === data) this.data = new Map()
}
}
/**
* Read one named value.
* @param name - name to resolve.
* @returns the retained value, or `undefined` when absent.
*/
get(name: string): V | undefined {
return this.data.get(name)
}
/**
* Test one name for membership.
* @param name - name to test.
* @returns whether the table contains that name.
*/
has(name: string): boolean {
return this.data.has(name)
}
/**
* Iterate live names in insertion order.
* @returns the native live key iterator.
*/
keys(): IterableIterator<string> {
return this.data.keys()
}
/**
* Iterate live entries in insertion order.
* @returns the native live entry iterator.
*/
entries(): IterableIterator<[string, V]> {
return this.data.entries()
}
/**
* Iterate live values in insertion order.
* @returns the native live value iterator.
*/
values(): IterableIterator<V> {
return this.data.values()
}
/**
* Test whether this table has no entries.
* @returns whether the table is empty.
*/
isEmpty(): boolean {
return this.data.size === 0
}
}
/**
* Insertion-ordered anonymous entries with independent registration identity.
*
* Equal values remain separate registrations. Values are borrowed, and
* iterators are live within one nonempty table generation; draining the table
* detaches them from later appends.
*/
export class AnonymousEntries<V> implements EntryValues<V> {
private data = new Map<symbol, V>()
/**
* Append one independently owned value.
* @param value - borrowed value to retain.
* @returns an idempotent undo for this exact append.
*/
append(value: V): () => void {
const data = this.data
const key = Symbol()
data.set(key, value)
let active = true
return () => {
if (!active) return
active = false
data.delete(key)
if (data.size === 0 && this.data === data) this.data = new Map()
}
}
/**
* Iterate live values in insertion order.
* @returns the native live value iterator.
*/
values(): IterableIterator<V> {
return this.data.values()
}
/**
* Test whether this table has no entries.
* @returns whether the table is empty.
*/
isEmpty(): boolean {
return this.data.size === 0
}
}
/**
* Own the global and exact-scope layers for one registry.
*
* Reads never create scoped layers. Registrations derive both visibility and
* effect ownership from the supplied Cordis context, collect undo before
* notification, and reclaim only a completely empty aggregate layer.
*/
export class ScopedLayers<L extends ScopeLayer> {
/** The eagerly constructed context-global layer. */
readonly global: L
private readonly scoped = new Map<ScopeKey, L>()
constructor(
private readonly createLayer: (scope: ScopeKey | undefined) => L,
private readonly onChange: () => void,
) {
this.global = createLayer(undefined)
}
/**
* Read an existing exact-scope overlay. Deliberately chain-blind: callers
* addressing one scope's OWN contributions (its restrictions, its guards)
* must not silently pick up an ancestor's — use {@link chainLayers} where
* inheritance is the point.
* @param scope - exact scope key; `undefined` denotes no overlay.
* @returns the existing scoped layer, or `undefined` without creating one.
*/
peek(scope: ScopeKey | undefined): L | undefined {
if (scope === undefined) return undefined
return this.scoped.get(scope)
}
/**
* Existing overlays along the scope's parent chain ({@link scopeChainOf}),
* farthest ancestor first and the exact scope last, so a caller layering
* them in order gives the nearest scope the final word.
* @param scope - viewing scope, or `undefined` for no overlays.
* @returns the existing layers, nearest last; absent overlays are skipped.
*/
chainLayers(scope: ScopeKey | undefined): L[] {
const layers: L[] = []
for (const key of scopeChainOf(scope).reverse()) {
const layer = this.scoped.get(key)
if (layer !== undefined) layers.push(layer)
}
return layers
}
/**
* Materialize global named entries followed by scope-chain shadows,
* farthest ancestor first, so the nearest scope's entry wins a name.
* @param scope - viewing scope, or `undefined` for the global view.
* @param pick - select the named table from a layer.
* @returns an insertion-ordered effective map.
*/
merge<V>(
scope: ScopeKey | undefined,
pick: (layer: L) => NamedEntries<V>,
): Map<string, V> {
const merged = new Map(pick(this.global).entries())
for (const layer of this.chainLayers(scope)) {
for (const [name, value] of pick(layer).entries()) merged.set(name, value)
}
return merged
}
/**
* Attach one synchronous layer mutation to its registration context.
* @param ctx - context that determines both scope visibility and effect ownership.
* @param action - atomic mutation returning its synchronous undo.
* @param options - Cordis effect label and optional change notification.
* @returns the exact disposer returned by `ctx.effect()`.
*/
effect(
ctx: Context,
action: (layer: L) => () => void,
options: { label: string; notify?: boolean },
): () => void {
const scope = scopeOf(ctx)
const notify = options.notify ?? true
const dispose = ctx.effect(function* (this: ScopedLayers<L>) {
let layer: L
let created = false
if (scope === undefined) {
layer = this.global
} else {
const existing = this.scoped.get(scope)
if (existing === undefined) {
layer = this.createLayer(scope)
this.scoped.set(scope, layer)
created = true
} else {
layer = existing
}
}
let undo: () => void
try {
undo = action(layer)
} catch (error) {
if (scope !== undefined && created && layer.isEmpty()) this.scoped.delete(scope)
throw error
}
yield () => {
undo()
if (scope !== undefined && layer.isEmpty()) this.scoped.delete(scope)
if (notify) this.onChange()
}
if (notify) this.onChange()
}.bind(this), options.label)
// oxlint-disable-next-line typescript/no-misused-promises -- exact synchronous disposer preserves Cordis effect identity
return dispose
}
}