| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102 |
- /**
- * AST helpers shared by the Cordis generators: locate the Cordis module merge
- * in a source file and enumerate the `interface Context` keys it declares.
- * The vendored core API projector consumes the merge body; the per-subsystem
- * region generator's exhaustiveness backstop consumes the key scan.
- */
- import { globSync, readFileSync } from 'node:fs'
- import { resolve, sep } from 'node:path'
- import ts from 'typescript'
- /** Cheap textual prefilter for a cordis module merge, quote-style agnostic
- * (the AST match below reads `stmt.name.text` and never sees the quotes). */
- const MERGE_HEAD = /declare module ['"](?:@deepseek-ai\/cordis|\.\/context\.ts)['"]/
- /**
- * Parse every file matching `patterns` (repo-relative, sorted, `/`-normalized)
- * that textually contains a cordis module merge, yielding one entry per merge
- * BLOCK — a file may legally hold several `declare module '@deepseek-ai/cordis'` blocks
- * (the Typert analyzer reads them all), so the exhaustiveness scan must too.
- * Files without a merge are skipped.
- * @param scanRoot - Repository root the patterns are resolved against.
- * @param patterns - Glob(s) selecting the TypeScript files to scan.
- * @returns One entry per cordis module block, in path then source order.
- */
- export function contextMergeFiles(
- scanRoot: string,
- patterns: string | readonly string[],
- ): { rel: string; sf: ts.SourceFile; text: string; body: ts.ModuleBlock }[] {
- const out: { rel: string; sf: ts.SourceFile; text: string; body: ts.ModuleBlock }[] = []
- const rels = [...new Set(globSync(patterns as string | string[], { cwd: scanRoot }).map(s => s.split(sep).join('/')))].sort()
- for (const rel of rels) {
- const abs = resolve(scanRoot, rel)
- const text = readFileSync(abs, 'utf8')
- if (!MERGE_HEAD.test(text)) continue
- const sf = ts.createSourceFile(abs, text, ts.ScriptTarget.Latest, true)
- for (const body of cordisModuleBodies(sf)) out.push({ rel, sf, text, body })
- }
- return out
- }
- /** Every cordis module-merge body in `sf`: `declare module '@deepseek-ai/cordis'` (harness
- * packages) or `declare module './context.ts'` (vendor core), in source order.
- * Module-local: consumers walk blocks through {@link contextMergeFiles}. */
- function cordisModuleBodies(sf: ts.SourceFile): ts.ModuleBlock[] {
- const bodies: ts.ModuleBlock[] = []
- for (const stmt of sf.statements) {
- if (!ts.isModuleDeclaration(stmt) || !ts.isStringLiteral(stmt.name)) continue
- if (stmt.name.text !== '@deepseek-ai/cordis' && stmt.name.text !== './context.ts') continue
- if (stmt.body && ts.isModuleBlock(stmt.body)) bodies.push(stmt.body)
- }
- return bodies
- }
- /** The FIRST cordis module-merge body in `sf`, or null without one — for the
- * vendor core-API renderer whose input files carry exactly one merge; the
- * exhaustiveness scan uses {@link cordisModuleBodies} to read them all. */
- export function cordisModuleBody(sf: ts.SourceFile): ts.ModuleBlock | null {
- return cordisModuleBodies(sf)[0] ?? null
- }
- /**
- * Every `key: Type` property a `declare module '@deepseek-ai/cordis'` Context merge
- * declares in one module body.
- * @param body - The cordis module augmentation block.
- * @param sf - Owning source file (for text extraction).
- * @returns key → declared type-name text, in declaration order.
- */
- export function contextKeyMap(body: ts.ModuleBlock, sf: ts.SourceFile): Map<string, string> {
- const keyToType = new Map<string, string>()
- for (const stmt of body.statements) {
- if (!ts.isInterfaceDeclaration(stmt) || stmt.name.text !== 'Context') continue
- for (const member of stmt.members) {
- if (!ts.isPropertySignature(member) || !member.type) continue
- keyToType.set(member.name.getText(sf), member.type.getText(sf))
- }
- }
- return keyToType
- }
- /**
- * Every event name a `declare module '@deepseek-ai/cordis'` Events merge declares in one
- * module body. Names are the literal member keys (`'agent/created'`), read
- * from method and property members alike so a declaration form the projector
- * would reject still enters the exhaustiveness scan.
- * @param body - The cordis module augmentation block.
- * @param sf - Owning source file (for computed-name text extraction).
- * @returns Declared event names, in declaration order.
- */
- export function eventNameList(body: ts.ModuleBlock, sf: ts.SourceFile): string[] {
- const names: string[] = []
- for (const stmt of body.statements) {
- if (!ts.isInterfaceDeclaration(stmt) || stmt.name.text !== 'Events') continue
- for (const member of stmt.members) {
- if (!member.name) continue
- names.push(ts.isStringLiteral(member.name) || ts.isIdentifier(member.name)
- ? member.name.text
- : member.name.getText(sf))
- }
- }
- return names
- }
|