@@ -149,6 +149,102 @@ export function searchTypes(query: string): (TypeSummary & { alias: string })[]
149149 return results ;
150150}
151151
152+ // ── Reusable Block Discovery (interfaces & addins by capability) ─────────
153+
154+ export interface ReusableBlockMatch {
155+ /** Alias-prefixed type name, e.g. `di:IVendorNameplateType`. */
156+ browseName : string ;
157+ alias : string ;
158+ kind : "interface" | "addin" ;
159+ isAbstract : boolean ;
160+ /** True if this addin type declares a DefaultInstanceBrowseName (composable by default name). */
161+ hasDefaultInstanceBrowseName : boolean ;
162+ /** Member browseNames that matched the query. */
163+ matchedMembers : string [ ] ;
164+ /** All member browseNames (components + properties + methods). */
165+ members : string [ ] ;
166+ }
167+
168+ function memberBrowseNames ( d : TypeDetails ) : string [ ] {
169+ return [ ...d . components . map ( ( m ) => m . browseName ) , ...d . properties . map ( ( m ) => m . browseName ) , ...d . methods ] ;
170+ }
171+
172+ /** Build the set of interface types (alias:browseName) via a subtypeOf → BaseInterfaceType fixpoint. */
173+ function buildInterfaceSet ( ) : Set < string > {
174+ const catalog = getCatalog ( ) ;
175+ const entries : { key : string ; subtypeOf ?: string } [ ] = [ ] ;
176+ for ( const [ alias , details ] of Object . entries ( catalog . typeDetails ) ) {
177+ for ( const [ bn , d ] of Object . entries ( details ) ) {
178+ entries . push ( { key : `${ alias } :${ bn } ` , subtypeOf : d . subtypeOf } ) ;
179+ }
180+ }
181+ const isInterface = new Set < string > ( ) ;
182+ let changed = true ;
183+ while ( changed ) {
184+ changed = false ;
185+ for ( const e of entries ) {
186+ if ( isInterface . has ( e . key ) ) continue ;
187+ if ( e . subtypeOf === "ua:BaseInterfaceType" || ( e . subtypeOf !== undefined && isInterface . has ( e . subtypeOf ) ) ) {
188+ isInterface . add ( e . key ) ;
189+ changed = true ;
190+ }
191+ }
192+ }
193+ return isInterface ;
194+ }
195+
196+ let _interfaceSet : Set < string > | null = null ;
197+
198+ /**
199+ * Find reusable Interfaces / AddIns by capability — e.g. `find_reusable_block("SerialNumber")`
200+ * returns the interfaces/addins that expose a matching member. Matches member browseNames,
201+ * plus the block's own name/description. Prefer an existing block over redefining members.
202+ */
203+ export function findReusableBlock ( query : string ) : ReusableBlockMatch [ ] {
204+ const catalog = getCatalog ( ) ;
205+ const q = query . toLowerCase ( ) ;
206+ if ( ! _interfaceSet ) _interfaceSet = buildInterfaceSet ( ) ;
207+
208+ const matches : ReusableBlockMatch [ ] = [ ] ;
209+ for ( const [ alias , details ] of Object . entries ( catalog . typeDetails ) ) {
210+ for ( const [ bn , d ] of Object . entries ( details ) ) {
211+ const key = `${ alias } :${ bn } ` ;
212+ const isInterface = _interfaceSet . has ( key ) ;
213+ const hasDefaultName = d . properties . some ( ( m ) => / d e f a u l t i n s t a n c e b r o w s e n a m e / i. test ( m . browseName ) ) ;
214+ // Only interfaces and addin-capable types are "reusable blocks".
215+ if ( ! isInterface && ! hasDefaultName ) continue ;
216+
217+ const members = memberBrowseNames ( d ) ;
218+ const matchedMembers = members . filter ( ( m ) => m . toLowerCase ( ) . includes ( q ) ) ;
219+ const nameMatch = bn . toLowerCase ( ) . includes ( q ) || ( d . description ?. toLowerCase ( ) . includes ( q ) ?? false ) ;
220+ if ( matchedMembers . length === 0 && ! nameMatch ) continue ;
221+
222+ matches . push ( {
223+ browseName : key ,
224+ alias,
225+ kind : isInterface ? "interface" : "addin" ,
226+ isAbstract : d . isAbstract ,
227+ hasDefaultInstanceBrowseName : hasDefaultName ,
228+ matchedMembers,
229+ members
230+ } ) ;
231+ }
232+ }
233+ // Rank: member match before name-only; interfaces (the canonical member
234+ // definers) before addins; more matched members first; abstract addin bases
235+ // after concrete composable ones.
236+ matches . sort ( ( a , b ) => {
237+ const am = a . matchedMembers . length > 0 ? 0 : 1 ;
238+ const bm = b . matchedMembers . length > 0 ? 0 : 1 ;
239+ if ( am !== bm ) return am - bm ;
240+ if ( a . kind !== b . kind ) return a . kind === "interface" ? - 1 : 1 ;
241+ if ( a . kind === "addin" && a . isAbstract !== b . isAbstract ) return a . isAbstract ? 1 : - 1 ;
242+ return b . matchedMembers . length - a . matchedMembers . length ;
243+ } ) ;
244+ // Cap: this is a "point me at the right block" tool, not an exhaustive dump.
245+ return matches . slice ( 0 , 15 ) ;
246+ }
247+
152248// ── Engineering Unit Lookup ──────────────────────────────────────────────
153249
154250export interface UnitLookupResult {
0 commit comments