范围关系与别名
策略的具体实现如下。
ts
export class ScopeRegistry<Definition extends ScopeDefinition = ScopeDefinition> {
/** Create and seal a complete scope catalog in one call. */
static readonly create: typeof createScopeRegistry = createScopeRegistry;
private readonly byId = new Map<string, RegisteredScope<Definition>>();
private readonly sources = new Map<string, Definition>();
private readonly aliases = new Map<string, string>();
private sealed = false;
private readonly policy: ScopeRegistryPolicy<Definition>;
/**
* @param policy - Optional id pattern, categories, required-binding switches, intent
* acceptance override, error wording and assertion hook; lists are copied on construction.
*/
constructor(policy: ScopeRegistryPolicy<Definition> = {}) {
const pattern = policy.idPattern;
const errorPorts = policy.errors,
{ sealed, invalid } = errorPorts ?? {};
const listFields = ['categories', 'tags'] as const;
const lists = Object.fromEntries(
listFields.map((key) => {
const value = policy[key];
return [key, value === undefined ? undefined : Object.freeze([...value])];
}),
) as Pick<ScopeRegistryPolicy<Definition>, (typeof listFields)[number]>;
this.policy = Object.freeze({
...registrationData(policy, [
'idPattern',
'assertDefinition',
'acceptsTool',
'errors',
...listFields,
]),
...lists,
idPattern: pattern ? new RegExp(pattern.source, pattern.flags) : undefined,
errors:
errorPorts &&
Object.freeze({
sealed: typeof sealed === 'function' ? sealed.bind(errorPorts) : sealed,
invalid: typeof invalid === 'function' ? invalid.bind(errorPorts) : invalid,
}),
assertDefinition: policy.assertDefinition?.bind(policy),
acceptsTool: policy.acceptsTool?.bind(policy),
});
}
/**
* Validate, freeze and add one scope together with its aliases.
*
* Every name is checked for conflicts before any of them is stored, so a rejected scope
* leaves no partial alias entry behind.
*
* @param input - Scope declaration; the raw object is retained for `acceptsTool` overrides
* while the compiled copy serves lookups.
* @returns This registry, for chaining.
* @throws A configured error for a sealed registry, an invalid name/description/category,
* a missing required binding, duplicate Tool ids or an already-claimed name.
*/
register(input: Definition): this {
const definition = {
...registrationData(input, [
'id',
'description',
'category',
'goalIds',
'queryScopeIds',
'knowledgeTopicIds',
'toolIds',
'aliases',
'evidence',
'contextTier',
]),
id: input.id,
description: input.description,
category: input.category,
goalIds: input.goalIds ?? [],
queryScopeIds: input.queryScopeIds ?? [],
knowledgeTopicIds: input.knowledgeTopicIds ?? [],
toolIds: input.toolIds ?? [],
aliases: input.aliases,
evidence: input.evidence,
contextTier: input.contextTier,
};
const names = [definition.id, ...(definition.aliases ?? [])];
const policy = this.policy,
tags = policy.tags ?? [];
const invalid = policy.errors?.invalid ?? 'Invalid scope definition';
const validName = (name: string) => {
if (typeof name !== 'string' || !name.trim()) return false;
if (!policy.idPattern) return true;
policy.idPattern.lastIndex = 0;
return policy.idPattern.test(name);
};
breakWhile(
[
{
occur: () => this.sealed,
error: policy.errors?.sealed ?? 'Scope registry is sealed',
tags: ['sealed'],
},
{
occur: [
() => !names.every(validName),
() => !definition.description.trim(),
() =>
policy.categories !== undefined && !policy.categories.includes(definition.category),
],
error: invalid,
tags: ['identity'],
},
{
occur: () => policy.requireGoals === true && !definition.goalIds.length,
error: invalid,
tags: ['goals', 'required'],
},
{
occur: [
() =>
policy.requireSources === true &&
!definition.queryScopeIds.length &&
!definition.knowledgeTopicIds.length,
() => policy.requireTools === true && !definition.toolIds.length,
],
error: invalid,
tags: ['capability-bindings'],
},
{
occur: [
() => definition.goalIds.some((intent) => typeof intent !== 'string'),
() => new Set(definition.goalIds).size !== definition.goalIds.length,
],
error: invalid,
tags: ['goals', 'values'],
},
],
tags,
);
assertion(
policy.assertDefinition?.(input),
'Registry definition assertions must return void synchronously',
);
breakWhile(
names.some((name) => this.byId.has(name) || this.aliases.has(name)),
invalid,
[...tags, 'duplicate'],
);
const frozen = Object.freeze({
...definition,
goalIds: Object.freeze([...definition.goalIds]),
toolIds: Object.freeze([...definition.toolIds]),
queryScopeIds: Object.freeze([...definition.queryScopeIds]),
knowledgeTopicIds: Object.freeze([...definition.knowledgeTopicIds]),
aliases: Object.freeze([...(definition.aliases ?? [])]),
evidence: Object.freeze(
[...(definition.evidence ?? [])].map((item) => Object.freeze(registrationData(item))),
),
}) as RegisteredScope<Definition>;
this.byId.set(definition.id, frozen);
this.sources.set(definition.id, input);
for (const alias of definition.aliases ?? []) this.aliases.set(alias, definition.id);
return this;
}
/** Register every scope in iteration order. */
registerAll(definitions: Iterable<Definition>): this {
for (const definition of definitions) this.register(definition);
return this;
}
/** Finalize the registry; no registration is accepted afterwards. */
seal(): this {
this.sealed = true;
return this;
}
/** Registered scopes in registration order. */
get definitions() {
return Object.freeze([...this.byId.values()]);
}
/** Registered scope ids in registration order; aliases are not included. */
ids(): readonly string[] {
return Object.freeze([...this.byId.keys()]);
}
/** Iterate the registered scopes. */
values() {
return this.byId.values();
}
/** Iterate `[id, scope]` pairs. */
entries() {
return this.byId.entries();
}
/**
* Registered scope for an id or alias.
*
* @throws TypeError When the name resolves to nothing.
*/
require(id: string) {
const value = this.get(id);
if (!value) throw new TypeError(`Unknown scope: ${id}`);
return value;
}
/** Resolve an alias to its scope id; unknown names are returned unchanged. */
normalize(scope: string): string {
return this.aliases.get(scope) ?? scope;
}
/** Registered scope for an id or alias, or `undefined` for a null/unknown name. */
get(scope?: string | null): RegisteredScope<Definition> | undefined {
return scope ? this.byId.get(this.normalize(scope)) : undefined;
}
/** Whether the name resolves to a registered scope. */
has(scope?: string | null): boolean {
return !!this.get(scope);
}
/**
* Whether a scope accepts an intent.
*
* The App may override the declared `goalIds` through `acceptsTool`; without an
* override the membership check applies.
*/
acceptsTool(scope: string, goal: string): boolean {
const definition = this.get(scope);
if (!definition) return false;
const ids: readonly string[] = definition.goalIds;
return this.policy.acceptsTool
? booleanResult(
this.policy.acceptsTool(this.sources.get(this.normalize(scope))!, goal),
'Scope intent predicates must return boolean synchronously',
)
: ids.includes(goal);
}
/** Whether the scope declares this query scope; a null scope accepts nothing. */
acceptsQuery(scope: string | null, queryScope: string): boolean {
const ids: readonly string[] = this.get(scope)?.queryScopeIds ?? [];
return !!scope && ids.includes(queryScope);
}
/** Whether the scope belongs to the given App category. */
is(scope: string | null | undefined, category: Definition['category']): boolean {
return this.get(scope)?.category === category;
}
/** Every query scope declared by any registered scope, deduplicated. */
queryScopeIds(): string[] {
return [...new Set([...this.byId.values()].flatMap((item) => item.queryScopeIds))];
}
/** Knowledge topic ids of one scope; copied so callers cannot mutate the registry. */
topics(scope?: string | null): string[] {
return [...(this.get(scope)?.knowledgeTopicIds ?? [])];
}
/** Evidence declarations of one scope; copied so callers cannot mutate the registry. */
evidence(scope?: string | null): RegisteredScopeEvidence<Definition>[] {
return [...(this.get(scope)?.evidence ?? [])];
}
/** Declared context tier of one scope, if any. */
contextTier(scope?: string | null): Definition['contextTier'] | undefined {
return this.get(scope)?.contextTier;
}
/**
* Copy the scopes that accept at least one of the given goals.
*
* @param goals - Goal ids to filter by; omit to include every registered scope.
* @returns Descriptions whose arrays are copied so callers cannot mutate registry state.
*/
describe(goals?: readonly string[]): ScopeDescription<Definition>[] {
const selected = goals ? new Set(goals) : undefined;
return [...this.byId.values()]
.filter((item) => !selected || item.goalIds.some((goal) => selected.has(goal)))
.map((item) => ({
...item,
goalIds: [...item.goalIds],
queryScopeIds: [...item.queryScopeIds],
knowledgeTopicIds: [...item.knowledgeTopicIds],
toolIds: [...item.toolIds],
aliases: [...item.aliases],
evidence: [...item.evidence],
})) as ScopeDescription<Definition>[];
}
}
export function createScopeRegistry<Definition extends ScopeDefinition>(
definitions: Iterable<Definition>,
policy: ScopeRegistryPolicy<Definition> = {},
): ScopeRegistry<Definition> {
return new ScopeRegistry(policy).registerAll(definitions).seal();
}