• feat(self-modification): add dynamic Cordis plugin runtime and UI

This commit is contained in:
imccyu
2026-08-12 23:51:31 +08:00
parent 0367506471
commit 4064198560
147 changed files with 20904 additions and 2412 deletions

View File

@@ -170,7 +170,7 @@ export class CordisCatalogProjector {
return renderRuntimeApi(
services,
model.events,
this.runtimeTypes(services),
this.runtimeTypes(services, model.events),
this.policy.inheritedServices,
)
}
@@ -325,7 +325,10 @@ export class CordisCatalogProjector {
return entries.sort((left, right) => left.key.localeCompare(right.key))
}
private runtimeTypes(services: readonly ServiceEntry[]): { name: string; declaration: string }[] {
private runtimeTypes(
services: readonly ServiceEntry[],
events: readonly EventEntry[],
): { name: string; declaration: string }[] {
const declarations = new Map<string, string>()
const ambiguous = new Set<string>()
for (const declaration of this.sourceDeclarations) {
@@ -343,7 +346,10 @@ export class CordisCatalogProjector {
)
}
for (const name of ambiguous) declarations.delete(name)
return referencedTypes(services.flatMap(service => service.methods.map(method => method.signature)), declarations)
return referencedTypes([
...services.flatMap(service => service.methods.map(method => method.signature)),
...events.map(event => event.signature),
], declarations)
}
}
@@ -409,6 +415,7 @@ interface ParsedJsDoc {
readonly doc: string
readonly params: ReadonlyMap<string, string>
readonly returns: string | null
readonly throws: readonly string[]
readonly deprecated: boolean
}
@@ -466,6 +473,7 @@ function parseJsDoc(raw: string): ParsedJsDoc {
const params = new Map<string, string>()
let returns: string | null = null
const throws: string[] = []
let deprecated = false
let sink: ((text: string) => void) | undefined
for (const line of lines) {
@@ -495,6 +503,17 @@ function parseJsDoc(raw: string): ParsedJsDoc {
}
continue
}
const throwsTag = /^@throws?(?:\s+[-—–]?\s*(.*))?$/.exec(line)
if (throwsTag !== null) {
let value = throwsTag[1] ?? ''
throws.push(value)
const index = throws.length - 1
sink = (text) => {
value = value === '' ? text : `${value} ${text}`
throws[index] = value
}
continue
}
if (line.startsWith('@') || line.trim() === '') sink = undefined
else sink?.(line.trim())
}
@@ -502,6 +521,7 @@ function parseJsDoc(raw: string): ParsedJsDoc {
doc: blocks.join('\n\n').replace(/\{@link\s+([^}]+)\}/g, '$1').trim(),
params,
returns,
throws,
deprecated,
}
}
@@ -641,33 +661,49 @@ function renderRuntimeApi(
' * `pnpm run verify-cordis-api` in doc-sync).',
' *',
' * The machine-readable cordis API catalog `cordis_inspect` serves to the',
' * model: harness services (summary + public method signatures/JSDoc),',
' * harness events (mode + signature/JSDoc), and the inherited `ctx` API. Produced by',
' * model: harness services (summary + structured public method contracts),',
' * harness events (mode + structured listener contracts), and the inherited `ctx` API. Produced by',
' * the same AST walk as docs/cordis-catalog, so this data and the rendered',
' * docs cannot diverge.',
' *',
' * @module @deepseek-ai/dsh-tool-cordis/api-catalog',
' */',
'',
'/** One public service method and its source-owned contract. */',
'/** One named parameter in a Service method or Event listener. */',
'export interface ApiParameter {',
' /** Parameter name from the exact signature. */',
' name: string',
' /** Source-owned parameter contract. */',
' description: string',
'}',
'',
'/** One public service member and its source-owned contract. */',
'export interface ServiceApiMethod {',
' /** Public method signature with its body stripped. */',
' signature: string',
' /** Original method JSDoc, with only container indentation removed. */',
' jsDoc: string',
' /** Method purpose and behavior. */',
' description: string',
' /** Named parameters in signature order. */',
' parameters: readonly ApiParameter[]',
' /** Non-void result contract when documented. */',
' returns?: string',
' /** Documented failure conditions. */',
' throws?: readonly string[]',
'}',
'',
'/** One harness `ctx.<key>` service: its one-line summary and public methods. */',
'/** One harness `ctx.<key>` service and its public methods. */',
'export interface ServiceApiEntry {',
' /** The `ctx.<key>` name, e.g. `tools`. */',
' key: string',
' /** First sentence of the service class JSDoc. */',
' summary: string',
' /** Complete service description. */',
' description: string',
' /** Public methods, bodies stripped, in source order. */',
' methods: readonly ServiceApiMethod[]',
'}',
'',
'/** One harness event: its dispatch mode, exact signature, and one-line summary. */',
'/** One harness event: its dispatch mode, exact signature, and listener contract. */',
'export interface EventApiEntry {',
' /** The scoped event name, e.g. `agent/status`. */',
' name: string',
@@ -675,10 +711,12 @@ function renderRuntimeApi(
' mode: string',
' /** The exact listener signature, whitespace-normalized. */',
' signature: string',
' /** Original event JSDoc, with only container indentation removed. */',
' jsDoc: string',
' /** First sentence of the event JSDoc. */',
' summary: string',
' /** Complete event description. */',
' description: string',
' /** Named listener parameters in signature order. */',
' parameters: readonly ApiParameter[]',
'}',
'',
'/** One inherited (cordis core + loader/hmr/timer) `ctx` member group with its summary. */',
@@ -689,7 +727,7 @@ function renderRuntimeApi(
' summary: string',
'}',
'',
'/** One named type shape the service signatures reference. */',
'/** One named type declaration referenced by a Service or Event signature. */',
'export interface TypeApiEntry {',
' /** The exported type/interface name, e.g. `ShellRunResult`. */',
' name: string',
@@ -704,14 +742,19 @@ function renderRuntimeApi(
lines.push(' {')
lines.push(` key: ${quote(service.key)},`)
lines.push(` summary: ${quote(firstSentence(service.doc))},`)
lines.push(` description: ${quote(service.doc)},`)
if (service.methods.length === 0) {
lines.push(' methods: [],')
} else {
lines.push(' methods: [')
for (const method of service.methods) {
const contract = parseJsDoc(method.jsDoc)
lines.push(' {')
lines.push(` signature: ${quote(method.signature)},`)
lines.push(` jsDoc: ${quote(method.jsDoc)},`)
lines.push(` description: ${quote(contract.doc)},`)
lines.push(` parameters: ${JSON.stringify([...contract.params].map(([name, description]) => ({ name, description })))},`)
if (contract.returns !== null) lines.push(` returns: ${quote(contract.returns)},`)
if (contract.throws.length > 0) lines.push(` throws: ${JSON.stringify(contract.throws)},`)
lines.push(' },')
}
lines.push(' ],')
@@ -725,18 +768,20 @@ function renderRuntimeApi(
'export const EVENT_API: readonly EventApiEntry[] = [',
)
for (const event of [...events].sort((left, right) => left.name.localeCompare(right.name))) {
const contract = parseJsDoc(event.jsDoc)
lines.push(' {')
lines.push(` name: ${quote(event.name)},`)
lines.push(` mode: ${quote(event.mode)},`)
lines.push(` signature: ${quote(event.signature)},`)
lines.push(` jsDoc: ${quote(event.jsDoc)},`)
lines.push(` summary: ${quote(firstSentence(event.doc))},`)
lines.push(` description: ${quote(event.doc)},`)
lines.push(` parameters: ${JSON.stringify([...contract.params].map(([name, description]) => ({ name, description })))},`)
lines.push(' },')
}
lines.push(
']',
'',
'/** Shapes of every exported type the SERVICE_API signatures reference (transitively), sorted by name. */',
'/** Shapes of every exported type the Service and Event signatures reference (transitively), sorted by name. */',
'export const TYPE_API: readonly TypeApiEntry[] = [',
)
for (const type of types) {
@@ -754,7 +799,98 @@ function renderRuntimeApi(
for (const inherited of inheritedServices) {
lines.push(` { name: ${quote(inherited.name)}, summary: ${quote(inherited.summary)} },`)
}
lines.push(']', '')
lines.push(
']',
'',
'function referencedTypeClosure(seeds: readonly string[]): TypeApiEntry[] {',
' const included = new Set<string>()',
' let frontier = [...seeds]',
' while (frontier.length > 0) {',
' const next: string[] = []',
' for (const entry of TYPE_API) {',
' if (included.has(entry.name)) continue',
' const pattern = new RegExp(`\\b${entry.name}\\b`)',
' if (!frontier.some(text => pattern.test(text))) continue',
' included.add(entry.name)',
' next.push(entry.declaration)',
' }',
' frontier = next',
' }',
' return TYPE_API.filter(entry => included.has(entry.name))',
'}',
'',
'function contextProperty(key: string): string {',
' return /^[A-Za-z_$][\\w$]*$/.test(key) ? `ctx.${key}` : `ctx[${JSON.stringify(key)}]`',
'}',
'',
'/**',
' * Project the Service Catalog as a compact directory or one exact coding contract.',
' * @param key - exact Service key; omit it to list all Services and method signatures.',
' * @param services - platform-specific visible Service entries.',
' * @returns compact navigation data or one detailed Service with its referenced type closure.',
' */',
'export function queryServiceApi(key?: string, services: readonly ServiceApiEntry[] = SERVICE_API): object {',
' if (key === undefined) {',
' return {',
" mode: 'catalog',",
' services: services.map(service => ({',
' key: service.key,',
' description: service.summary,',
' methods: service.methods.map(method => ({ signature: method.signature })),',
' })),',
' }',
' }',
' const service = services.find(candidate => candidate.key === key)',
' if (service === undefined) throw new Error(`no catalogued Service named "${key}"`)',
' return {',
" mode: 'service',",
' service: {',
' key: service.key,',
' description: service.description,',
' access: {',
' optional: { expression: `ctx.get(${JSON.stringify(service.key)})`, requiresUndefinedCheck: true },',
' hardDependency: { inject: [service.key], expression: contextProperty(service.key) },',
' },',
' methods: service.methods,',
' },',
' referencedTypes: referencedTypeClosure(service.methods.map(method => method.signature)),',
' }',
'}',
'',
'/**',
' * Project the Event Catalog as a compact directory or one exact listener contract.',
' * @param name - exact Event name; omit it to list all Events and listener signatures.',
' * @param events - platform-specific visible Event entries.',
' * @returns compact navigation data or one detailed Event with its referenced type closure.',
' */',
'export function queryEventApi(name?: string, events: readonly EventApiEntry[] = EVENT_API): object {',
' if (name === undefined) {',
' return {',
" mode: 'catalog',",
' events: events.map(event => ({',
' name: event.name,',
' description: event.summary,',
' mode: event.mode,',
' signature: event.signature,',
' })),',
' }',
' }',
' const event = events.find(candidate => candidate.name === name)',
' if (event === undefined) throw new Error(`no catalogued Event named "${name}"`)',
' return {',
" mode: 'event',",
' event: {',
' name: event.name,',
' description: event.description,',
' mode: event.mode,',
' signature: event.signature,',
' parameters: event.parameters,',
' },',
' referencedTypes: referencedTypeClosure([event.signature]),',
' }',
'}',
'',
)
return lines.join('\n')
}
/** Opening region delimiter; injected content lives between the pair and the page owns everything outside. */