@soederpop/luca 0.0.23 → 0.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/AGENTS.md +1 -1
  2. package/CLAUDE.md +6 -1
  3. package/assistants/codingAssistant/hooks.ts +0 -1
  4. package/assistants/lucaExpert/CORE.md +37 -0
  5. package/assistants/lucaExpert/hooks.ts +9 -0
  6. package/assistants/lucaExpert/tools.ts +177 -0
  7. package/commands/build-bootstrap.ts +41 -1
  8. package/docs/TABLE-OF-CONTENTS.md +0 -1
  9. package/docs/apis/clients/rest.md +5 -5
  10. package/docs/apis/features/agi/assistant.md +1 -1
  11. package/docs/apis/features/agi/conversation-history.md +6 -7
  12. package/docs/apis/features/agi/conversation.md +1 -1
  13. package/docs/apis/features/agi/semantic-search.md +1 -1
  14. package/docs/bootstrap/CLAUDE.md +1 -1
  15. package/docs/bootstrap/SKILL.md +7 -3
  16. package/docs/bootstrap/templates/luca-cli.ts +5 -0
  17. package/docs/mcp/readme.md +1 -1
  18. package/docs/tutorials/00-bootstrap.md +18 -0
  19. package/package.json +2 -2
  20. package/scripts/stamp-build.sh +12 -0
  21. package/scripts/test-docs-reader.ts +10 -0
  22. package/src/agi/container.server.ts +8 -5
  23. package/src/agi/features/assistant.ts +210 -55
  24. package/src/agi/features/assistants-manager.ts +138 -66
  25. package/src/agi/features/conversation.ts +46 -14
  26. package/src/agi/features/docs-reader.ts +166 -0
  27. package/src/agi/features/openapi.ts +1 -1
  28. package/src/agi/features/skills-library.ts +257 -313
  29. package/src/bootstrap/generated.ts +8163 -6
  30. package/src/cli/build-info.ts +4 -0
  31. package/src/cli/cli.ts +2 -1
  32. package/src/command.ts +75 -0
  33. package/src/commands/bootstrap.ts +16 -1
  34. package/src/commands/describe.ts +29 -1089
  35. package/src/commands/eval.ts +6 -1
  36. package/src/commands/sandbox-mcp.ts +17 -7
  37. package/src/container-describer.ts +1098 -0
  38. package/src/container.ts +11 -0
  39. package/src/helper.ts +56 -2
  40. package/src/introspection/generated.agi.ts +1684 -799
  41. package/src/introspection/generated.node.ts +964 -572
  42. package/src/introspection/generated.web.ts +9 -1
  43. package/src/node/container.ts +1 -1
  44. package/src/node/features/content-db.ts +268 -13
  45. package/src/node/features/fs.ts +18 -0
  46. package/src/node/features/git.ts +90 -0
  47. package/src/node/features/grep.ts +1 -1
  48. package/src/node/features/proc.ts +1 -0
  49. package/src/node/features/tts.ts +1 -1
  50. package/src/node/features/vm.ts +48 -0
  51. package/src/scaffolds/generated.ts +2 -2
  52. package/src/server.ts +40 -0
  53. package/src/servers/express.ts +2 -0
  54. package/src/servers/mcp.ts +1 -0
  55. package/src/servers/socket.ts +2 -0
  56. package/assistants/architect/CORE.md +0 -3
  57. package/assistants/architect/hooks.ts +0 -3
  58. package/assistants/architect/tools.ts +0 -10
  59. package/docs/apis/features/agi/skills-library.md +0 -234
  60. package/docs/reports/assistant-bugs.md +0 -38
  61. package/docs/reports/attach-pattern-usage.md +0 -18
  62. package/docs/reports/code-audit-results.md +0 -391
  63. package/docs/reports/console-hmr-design.md +0 -170
  64. package/docs/reports/helper-semantic-search.md +0 -72
  65. package/docs/reports/introspection-audit-tasks.md +0 -378
  66. package/docs/reports/luca-mcp-improvements.md +0 -128
  67. package/test-integration/skills-library.test.ts +0 -157
@@ -0,0 +1,1098 @@
1
+ import type { IntrospectionSection, MethodIntrospection, GetterIntrospection, HelperIntrospection } from './introspection/index.js'
2
+ import { presentIntrospectionAsMarkdown } from './helper.js'
3
+
4
+ type Platform = 'browser' | 'server' | 'node' | 'all'
5
+
6
+ type ResolvedTarget =
7
+ | { kind: 'container' }
8
+ | { kind: 'registry'; name: string }
9
+ | { kind: 'helper'; registry: string; id: string }
10
+ | { kind: 'member'; registry: string; id: string; member: string; memberType: 'method' | 'getter' }
11
+ | { kind: 'browser-helper'; id: string }
12
+ | { kind: 'browser-member'; id: string; member: string; memberType: 'method' | 'getter' }
13
+
14
+ type DescribeOptions = {
15
+ sections?: (IntrospectionSection | 'description')[]
16
+ noTitle?: boolean
17
+ headingDepth?: number
18
+ platform?: Platform
19
+ }
20
+
21
+ type DescribeResult = { json: any; text: string }
22
+
23
+ type BrowserFeatureData = {
24
+ introspection: Map<string, HelperIntrospection>
25
+ constructors: Map<string, any>
26
+ available: string[]
27
+ collidingIds: Set<string>
28
+ }
29
+
30
+ class DescribeError extends Error {
31
+ constructor(message: string) {
32
+ super(message)
33
+ this.name = 'DescribeError'
34
+ }
35
+ }
36
+
37
+ /** Known top-level helper base class names — anything above these is "shared" */
38
+ const BASE_CLASS_NAMES = new Set(['Helper', 'Feature', 'Client', 'Server'])
39
+
40
+ /** Maps flag names to the section they represent. */
41
+ const SECTION_FLAGS: Record<string, IntrospectionSection | 'description'> = {
42
+ 'description': 'description',
43
+ 'usage': 'usage',
44
+ 'methods': 'methods',
45
+ 'getters': 'getters',
46
+ 'events': 'events',
47
+ 'state': 'state',
48
+ 'options': 'options',
49
+ 'env-vars': 'envVars',
50
+ 'envvars': 'envVars',
51
+ 'examples': 'examples',
52
+ 'only-methods': 'methods',
53
+ 'only-getters': 'getters',
54
+ 'only-events': 'events',
55
+ 'only-state': 'state',
56
+ 'only-options': 'options',
57
+ 'only-env-vars': 'envVars',
58
+ 'only-envvars': 'envVars',
59
+ 'only-examples': 'examples',
60
+ }
61
+
62
+ /**
63
+ * Encapsulates container introspection and description logic.
64
+ * Discovers registries dynamically from the container's own state —
65
+ * it knows nothing about which helpers exist until it asks the container.
66
+ * Browser feature data can be injected externally via setBrowserData().
67
+ */
68
+ export class ContainerDescriber {
69
+ container: any
70
+ private _browserData: BrowserFeatureData | null = null
71
+ private _initialized = false
72
+
73
+ constructor(container: any) {
74
+ this.container = container
75
+ }
76
+
77
+ /** The registry names this container actually has, discovered at runtime. */
78
+ private get registryNames(): string[] {
79
+ return this.container.registryNames || ['features']
80
+ }
81
+
82
+ /**
83
+ * Discover all helpers. Must be called before resolve/getData.
84
+ */
85
+ async initialize(): Promise<void> {
86
+ if (this._initialized) return
87
+ await this.container.helpers.discoverAll()
88
+ this._initialized = true
89
+ }
90
+
91
+ /**
92
+ * Inject browser feature data from an external source.
93
+ * The describer doesn't own browser loading — that's the caller's job.
94
+ */
95
+ setBrowserData(data: BrowserFeatureData): void {
96
+ this._browserData = data
97
+ }
98
+
99
+ /**
100
+ * High-level: describe one or more targets, returning combined json and text.
101
+ */
102
+ async describe(targets: string[], options: DescribeOptions = {}): Promise<DescribeResult> {
103
+ const platform = this.normalizePlatform(options.platform || 'all')
104
+ await this.initialize()
105
+
106
+ const sections = options.sections || []
107
+ const noTitle = options.noTitle || false
108
+
109
+ const resolved: ResolvedTarget[] = []
110
+ for (const target of targets) {
111
+ resolved.push(...this.resolve(target, platform))
112
+ }
113
+
114
+ const isMulti = resolved.length > 1 || resolved.some((r) => r.kind === 'registry')
115
+ const headingDepth = options.headingDepth ?? (isMulti ? 2 : 1)
116
+
117
+ const results = resolved.map((item) => this.getData(item, { sections, noTitle, headingDepth, platform }))
118
+
119
+ if (resolved.length === 1) {
120
+ return results[0]!
121
+ }
122
+
123
+ return {
124
+ json: results.map((r) => r.json),
125
+ text: `# Luca Helper Descriptions\n\nBelow you'll find documentation.\n\n${results.map((r) => r.text).join('\n\n---\n\n')}`,
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Describe the container itself.
131
+ */
132
+ async describeContainer(options: DescribeOptions = {}): Promise<DescribeResult> {
133
+ await this.initialize()
134
+ return this.getContainerData(options.sections || [], options.noTitle || false, options.headingDepth || 1)
135
+ }
136
+
137
+ /**
138
+ * Describe a registry by name.
139
+ */
140
+ async describeRegistry(registryName: string, options: DescribeOptions = {}): Promise<DescribeResult> {
141
+ const platform = this.normalizePlatform(options.platform || 'all')
142
+ await this.initialize()
143
+ const name = this.matchRegistryName(registryName)
144
+ if (!name) throw new DescribeError(`Unknown registry: ${registryName}. Available: ${this.registryNames.join(', ')}`)
145
+ return this.getRegistryData(name, options.sections || [], options.noTitle || false, options.headingDepth || 1, platform)
146
+ }
147
+
148
+ /**
149
+ * Describe a specific helper by name (qualified or unqualified).
150
+ */
151
+ async describeHelper(target: string, options: DescribeOptions = {}): Promise<DescribeResult> {
152
+ const platform = this.normalizePlatform(options.platform || 'all')
153
+ await this.initialize()
154
+
155
+ const resolved = this.resolve(target, platform)
156
+ if (resolved.length === 0) throw new DescribeError(`Could not resolve: ${target}`)
157
+
158
+ const headingDepth = options.headingDepth ?? (resolved.length > 1 ? 2 : 1)
159
+ const results = resolved.map((item) => this.getData(item, { ...options, headingDepth, platform }))
160
+
161
+ if (results.length === 1) return results[0]!
162
+ return {
163
+ json: results.map((r) => r.json),
164
+ text: results.map((r) => r.text).join('\n\n---\n\n'),
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Describe a specific member (method or getter) on a helper.
170
+ */
171
+ async describeMember(helperAndMember: string, options: DescribeOptions = {}): Promise<DescribeResult> {
172
+ return this.describeHelper(helperAndMember, options)
173
+ }
174
+
175
+ /**
176
+ * Collect sections from a flags object (as produced by the CLI args schema).
177
+ */
178
+ static getSectionsFromFlags(flags: Record<string, any>): (IntrospectionSection | 'description')[] {
179
+ const sections: (IntrospectionSection | 'description')[] = []
180
+ for (const [flag, section] of Object.entries(SECTION_FLAGS)) {
181
+ if (flags[flag] && !sections.includes(section)) {
182
+ sections.push(section)
183
+ }
184
+ }
185
+ return sections
186
+ }
187
+
188
+ /**
189
+ * Generate tool definitions suitable for use with AI assistant tool-calling interfaces.
190
+ * Registry names in the enum are populated dynamically from the container.
191
+ */
192
+ toTools(): Array<{ name: string; description: string; parameters: Record<string, any>; execute: (args: any) => Promise<string> }> {
193
+ const registryEnum = this.registryNames
194
+
195
+ return [
196
+ {
197
+ name: 'describe_container',
198
+ description: 'Describe the container itself — its class, registries, and configuration.',
199
+ parameters: {
200
+ type: 'object',
201
+ properties: {
202
+ sections: {
203
+ type: 'array',
204
+ items: { type: 'string', enum: ['description', 'usage', 'methods', 'getters', 'events', 'state', 'options', 'envVars', 'examples'] },
205
+ description: 'Which sections to include. Omit for all.',
206
+ },
207
+ },
208
+ required: [],
209
+ },
210
+ execute: async (args: any) => {
211
+ const result = await this.describeContainer({ sections: args.sections })
212
+ return result.text
213
+ },
214
+ },
215
+ {
216
+ name: 'describe_registry',
217
+ description: `List all helpers in a registry with concise summaries. Available registries: ${registryEnum.join(', ')}`,
218
+ parameters: {
219
+ type: 'object',
220
+ properties: {
221
+ registry: {
222
+ type: 'string',
223
+ enum: registryEnum,
224
+ description: 'Which registry to describe.',
225
+ },
226
+ platform: {
227
+ type: 'string',
228
+ enum: ['browser', 'server', 'all'],
229
+ description: 'Filter by platform. Defaults to all.',
230
+ },
231
+ sections: {
232
+ type: 'array',
233
+ items: { type: 'string', enum: ['description', 'usage', 'methods', 'getters', 'events', 'state', 'options', 'envVars', 'examples'] },
234
+ description: 'Which sections to include per helper. Omit for concise index.',
235
+ },
236
+ },
237
+ required: ['registry'],
238
+ },
239
+ execute: async (args: any) => {
240
+ const result = await this.describeRegistry(args.registry, {
241
+ sections: args.sections,
242
+ platform: args.platform,
243
+ })
244
+ return result.text
245
+ },
246
+ },
247
+ {
248
+ name: 'describe_helper',
249
+ description: 'Describe a specific helper by name. Supports qualified names like "features.fs" or unqualified like "fs". Also supports member access like "fs.readFile" or "ui.banner".',
250
+ parameters: {
251
+ type: 'object',
252
+ properties: {
253
+ target: {
254
+ type: 'string',
255
+ description: 'The helper to describe. Examples: "fs", "features.fs", "fs.readFile", "ui.banner"',
256
+ },
257
+ platform: {
258
+ type: 'string',
259
+ enum: ['browser', 'server', 'all'],
260
+ description: 'Filter by platform. Defaults to all.',
261
+ },
262
+ sections: {
263
+ type: 'array',
264
+ items: { type: 'string', enum: ['description', 'usage', 'methods', 'getters', 'events', 'state', 'options', 'envVars', 'examples'] },
265
+ description: 'Which sections to include. Omit for all.',
266
+ },
267
+ },
268
+ required: ['target'],
269
+ },
270
+ execute: async (args: any) => {
271
+ const result = await this.describeHelper(args.target, {
272
+ sections: args.sections,
273
+ platform: args.platform,
274
+ })
275
+ return result.text
276
+ },
277
+ },
278
+ ]
279
+ }
280
+
281
+ // --- Resolution ---
282
+
283
+ /**
284
+ * Parse a target string into one or more resolved targets.
285
+ */
286
+ resolve(target: string, platform: Platform = 'all'): ResolvedTarget[] {
287
+ const lower = target.toLowerCase()
288
+ const includeNode = this.shouldIncludeNode(platform)
289
+ const includeBrowser = this.shouldIncludeBrowser(platform)
290
+
291
+ if (lower === 'container' || lower === 'self') {
292
+ return [{ kind: 'container' }]
293
+ }
294
+
295
+ // Check if it matches a registry name
296
+ const registryMatch = this.matchRegistryName(target)
297
+ if (registryMatch && !target.includes('.')) {
298
+ return [{ kind: 'registry', name: registryMatch }]
299
+ }
300
+
301
+ if (target.includes('.')) {
302
+ const [prefix, ...rest] = target.split('.')
303
+ const id = rest.join('.')
304
+ const registry = this.matchRegistryName(prefix!)
305
+
306
+ if (registry) {
307
+ const results: ResolvedTarget[] = []
308
+
309
+ if (includeNode) {
310
+ const reg = this.container[registry]
311
+ const resolved = this.fuzzyFind(reg, id)
312
+ if (resolved) results.push({ kind: 'helper', registry, id: resolved })
313
+ }
314
+
315
+ if (includeBrowser && registry === 'features') {
316
+ const browserFound = this.fuzzyFindBrowser(id)
317
+ if (browserFound) results.push({ kind: 'browser-helper', id: browserFound })
318
+ }
319
+
320
+ if (results.length === 0) {
321
+ const reg = this.container[registry]
322
+ const availableMsg = includeNode ? reg.available.join(', ') : ''
323
+ const browserMsg = includeBrowser && this._browserData ? this._browserData.available.join(', ') : ''
324
+ const combined = [availableMsg, browserMsg].filter(Boolean).join(', ')
325
+ throw new DescribeError(`"${id}" is not registered in ${registry}. Available: ${combined}`)
326
+ }
327
+
328
+ return results
329
+ }
330
+
331
+ // Not a registry prefix — try "helper.member"
332
+ const helperName = prefix!
333
+ const memberName = rest.join('.')
334
+ const results: ResolvedTarget[] = []
335
+
336
+ if (includeNode) {
337
+ const memberResult = this.resolveHelperMember(helperName, memberName)
338
+ if (memberResult) results.push(memberResult)
339
+ }
340
+
341
+ if (includeBrowser) {
342
+ try {
343
+ const browserResult = this.resolveBrowserHelperMember(helperName, memberName)
344
+ if (browserResult) results.push(browserResult)
345
+ } catch (e) {
346
+ if (results.length === 0) throw e
347
+ }
348
+ }
349
+
350
+ if (results.length > 0) return results
351
+ }
352
+
353
+ // Unqualified name: search all registries
354
+ const matches: ResolvedTarget[] = []
355
+
356
+ if (includeNode) {
357
+ for (const registryName of this.registryNames) {
358
+ const reg = this.container[registryName]
359
+ if (!reg) continue
360
+ const found = this.fuzzyFind(reg, target)
361
+ if (found) {
362
+ matches.push({ kind: 'helper', registry: registryName, id: found })
363
+ }
364
+ }
365
+ }
366
+
367
+ if (includeBrowser) {
368
+ const browserFound = this.fuzzyFindBrowser(target)
369
+ if (browserFound) {
370
+ matches.push({ kind: 'browser-helper', id: browserFound })
371
+ }
372
+ }
373
+
374
+ if (matches.length === 0) {
375
+ const lines = [`"${target}" was not found in any registry.`, '', 'Available:']
376
+ if (includeNode) {
377
+ for (const registryName of this.registryNames) {
378
+ const reg = this.container[registryName]
379
+ if (reg && reg.available.length > 0) {
380
+ lines.push(` ${registryName}: ${reg.available.join(', ')}`)
381
+ }
382
+ }
383
+ }
384
+ if (includeBrowser && this._browserData && this._browserData.available.length > 0) {
385
+ lines.push(` browser features: ${this._browserData.available.join(', ')}`)
386
+ }
387
+ throw new DescribeError(lines.join('\n'))
388
+ }
389
+
390
+ const nodeMatches = matches.filter(m => m.kind === 'helper')
391
+ if (nodeMatches.length > 1) {
392
+ const lines = [`"${target}" is ambiguous — found in multiple registries:`]
393
+ for (const m of nodeMatches) {
394
+ if (m.kind === 'helper') lines.push(` ${m.registry}.${m.id}`)
395
+ }
396
+ lines.push('', `Please qualify it, e.g.: ${(nodeMatches[0] as any).registry}.${target}`)
397
+ throw new DescribeError(lines.join('\n'))
398
+ }
399
+
400
+ return matches
401
+ }
402
+
403
+ /**
404
+ * Get data for a resolved target.
405
+ */
406
+ getData(item: ResolvedTarget, options: DescribeOptions = {}): DescribeResult {
407
+ const sections = options.sections || []
408
+ const noTitle = options.noTitle || false
409
+ const headingDepth = options.headingDepth || 1
410
+ const platform = options.platform || 'all'
411
+
412
+ switch (item.kind) {
413
+ case 'container':
414
+ return this.getContainerData(sections, noTitle, headingDepth)
415
+ case 'registry':
416
+ return this.getRegistryData(item.name, sections, noTitle, headingDepth, platform)
417
+ case 'helper':
418
+ return this.getHelperData(item.registry, item.id, sections, noTitle, headingDepth)
419
+ case 'member':
420
+ return this.getMemberData(item.registry, item.id, item.member, item.memberType, headingDepth)
421
+ case 'browser-helper':
422
+ return this.getBrowserHelperData(item.id, sections, noTitle, headingDepth)
423
+ case 'browser-member':
424
+ return this.getBrowserMemberData(item.id, item.member, item.memberType, headingDepth)
425
+ }
426
+ }
427
+
428
+ // --- Private: Platform helpers ---
429
+
430
+ private normalizePlatform(p: string): Platform {
431
+ if (p === 'node') return 'server'
432
+ if (p === 'web') return 'browser'
433
+ return p as Platform
434
+ }
435
+
436
+ private shouldIncludeNode(platform: Platform): boolean {
437
+ return platform === 'server' || platform === 'node' || platform === 'all'
438
+ }
439
+
440
+ private shouldIncludeBrowser(platform: Platform): boolean {
441
+ return platform === 'browser' || platform === 'all'
442
+ }
443
+
444
+ // --- Private: Fuzzy matching ---
445
+
446
+ private normalize(name: string): string {
447
+ return name.replace(/\.[tj]sx?$/, '').replace(/[-_]/g, '').toLowerCase()
448
+ }
449
+
450
+ private fuzzyFind(registry: any, input: string): string | undefined {
451
+ if (registry.has(input)) return input
452
+ const norm = this.normalize(input)
453
+ return (registry.available as string[]).find((id: string) => this.normalize(id) === norm)
454
+ }
455
+
456
+ private fuzzyFindBrowser(input: string): string | undefined {
457
+ if (!this._browserData) return undefined
458
+ const norm = this.normalize(input)
459
+ return this._browserData.available.find(id => this.normalize(id) === norm)
460
+ }
461
+
462
+ /**
463
+ * Match a user-provided name to an actual registry name on the container.
464
+ * Handles pluralization and case variations dynamically.
465
+ */
466
+ private matchRegistryName(name: string): string | undefined {
467
+ const lower = name.toLowerCase()
468
+ return this.registryNames.find(
469
+ (r) => r === lower || r === lower + 's' || r.replace(/s$/, '') === lower
470
+ )
471
+ }
472
+
473
+ // --- Private: Member resolution ---
474
+
475
+ private resolveHelperMember(helperName: string, memberName: string): ResolvedTarget | null {
476
+ for (const registryName of this.registryNames) {
477
+ const reg = this.container[registryName]
478
+ if (!reg) continue
479
+ const found = this.fuzzyFind(reg, helperName)
480
+ if (!found) continue
481
+
482
+ const Ctor = reg.lookup(found)
483
+ const introspection = Ctor.introspect?.()
484
+ if (!introspection) continue
485
+
486
+ if (introspection.methods?.[memberName]) {
487
+ return { kind: 'member', registry: registryName, id: found, member: memberName, memberType: 'method' }
488
+ }
489
+ if (introspection.getters?.[memberName]) {
490
+ return { kind: 'member', registry: registryName, id: found, member: memberName, memberType: 'getter' }
491
+ }
492
+
493
+ const allMembers = [
494
+ ...Object.keys(introspection.methods || {}).map((m: string) => m + '()'),
495
+ ...Object.keys(introspection.getters || {}),
496
+ ].sort()
497
+ throw new DescribeError(
498
+ `"${memberName}" is not a known method or getter on ${found}.\n\nAvailable members:\n ${allMembers.join(', ')}`
499
+ )
500
+ }
501
+ return null
502
+ }
503
+
504
+ private resolveBrowserHelperMember(helperName: string, memberName: string): ResolvedTarget | null {
505
+ if (!this._browserData) return null
506
+ const found = this.fuzzyFindBrowser(helperName)
507
+ if (!found) return null
508
+
509
+ const data = this._browserData.introspection.get(`features.${found}`)
510
+ if (!data) return null
511
+
512
+ if (data.methods?.[memberName]) {
513
+ return { kind: 'browser-member', id: found, member: memberName, memberType: 'method' }
514
+ }
515
+ if (data.getters?.[memberName]) {
516
+ return { kind: 'browser-member', id: found, member: memberName, memberType: 'getter' }
517
+ }
518
+
519
+ const allMembers = [
520
+ ...Object.keys(data.methods || {}).map((m: string) => m + '()'),
521
+ ...Object.keys(data.getters || {}),
522
+ ].sort()
523
+ throw new DescribeError(
524
+ `"${memberName}" is not a known method or getter on ${found} (browser).\n\nAvailable members:\n ${allMembers.join(', ')}`
525
+ )
526
+ }
527
+
528
+ // --- Private: Data getters ---
529
+
530
+ private getContainerData(sections: (IntrospectionSection | 'description')[], noTitle: boolean, headingDepth: number): DescribeResult {
531
+ const container = this.container
532
+
533
+ if (sections.length === 0) {
534
+ const data = container.inspect()
535
+ return { json: data, text: container.inspectAsText(undefined, headingDepth) }
536
+ }
537
+
538
+ const data = container.inspect()
539
+ const introspectionSections = sections.filter((s): s is IntrospectionSection => s !== 'description')
540
+ const textParts: string[] = []
541
+ const jsonResult: Record<string, any> = {}
542
+ const h = '#'.repeat(headingDepth)
543
+
544
+ if (!noTitle) {
545
+ const className = data.className || 'Container'
546
+ textParts.push(`${h} ${className} (Container)`)
547
+ jsonResult.className = className
548
+ if (data.description) {
549
+ textParts.push(data.description)
550
+ jsonResult.description = data.description
551
+ }
552
+ }
553
+
554
+ for (const section of introspectionSections) {
555
+ textParts.push(container.inspectAsText(section, headingDepth))
556
+ jsonResult[section] = data[section]
557
+ }
558
+
559
+ return { json: jsonResult, text: textParts.join('\n\n') }
560
+ }
561
+
562
+ private getHelperData(registryName: string, id: string, sections: (IntrospectionSection | 'description')[], noTitle: boolean, headingDepth: number): DescribeResult {
563
+ const registry = this.container[registryName]
564
+ const Ctor = registry.lookup(id)
565
+ const text = this.renderHelperText(Ctor, sections, noTitle, headingDepth)
566
+
567
+ let finalText = text
568
+ if (sections.length === 0 && !noTitle) {
569
+ const summary = this.buildHelperSummary(Ctor)
570
+ if (summary) {
571
+ const sectionHeading = '#'.repeat(headingDepth + 1) + ' '
572
+ const idx = text.indexOf('\n' + sectionHeading)
573
+ if (idx >= 0) {
574
+ finalText = text.slice(0, idx) + '\n\n' + summary + '\n' + text.slice(idx)
575
+ }
576
+ }
577
+ }
578
+
579
+ return {
580
+ json: this.renderHelperJson(Ctor, sections, noTitle),
581
+ text: finalText,
582
+ }
583
+ }
584
+
585
+ private getMemberData(registryName: string, id: string, member: string, memberType: 'method' | 'getter', headingDepth: number): DescribeResult {
586
+ const registry = this.container[registryName]
587
+ const Ctor = registry.lookup(id)
588
+ const introspection = Ctor.introspect?.()
589
+ const h = '#'.repeat(headingDepth)
590
+ const hSub = '#'.repeat(headingDepth + 1)
591
+
592
+ if (memberType === 'method') {
593
+ const method = introspection?.methods?.[member] as MethodIntrospection | undefined
594
+ if (!method) return { json: {}, text: `No introspection data for ${id}.${member}()` }
595
+
596
+ const parts: string[] = []
597
+ parts.push(`${h} ${id}.${member}()`)
598
+ parts.push(`> method on **${introspection.className || id}**`)
599
+ if (method.description) parts.push(method.description)
600
+
601
+ const paramEntries = Object.entries(method.parameters || {})
602
+ if (paramEntries.length > 0) {
603
+ const paramLines = [`${hSub} Parameters`, '']
604
+ for (const [name, info] of paramEntries) {
605
+ const req = (method.required || []).includes(name) ? ' *(required)*' : ''
606
+ paramLines.push(`- **${name}** \`${info.type}\`${req}${info.description ? ' — ' + info.description : ''}`)
607
+ if (info.properties) {
608
+ for (const [propName, propInfo] of Object.entries(info.properties)) {
609
+ paramLines.push(` - **${propName}** \`${propInfo.type}\`${propInfo.description ? ' — ' + propInfo.description : ''}`)
610
+ }
611
+ }
612
+ }
613
+ parts.push(paramLines.join('\n'))
614
+ }
615
+
616
+ if (method.returns && method.returns !== 'void') {
617
+ parts.push(`${hSub} Returns\n\n\`${method.returns}\``)
618
+ }
619
+
620
+ if (method.examples?.length) {
621
+ parts.push(`${hSub} Examples`)
622
+ for (const ex of method.examples) {
623
+ parts.push(`\`\`\`${ex.language || 'typescript'}\n${ex.code}\n\`\`\``)
624
+ }
625
+ }
626
+
627
+ return { json: { [member]: method, _helper: id, _type: 'method' }, text: parts.join('\n\n') }
628
+ }
629
+
630
+ const getter = introspection?.getters?.[member] as GetterIntrospection | undefined
631
+ if (!getter) return { json: {}, text: `No introspection data for ${id}.${member}` }
632
+
633
+ const parts: string[] = []
634
+ parts.push(`${h} ${id}.${member}`)
635
+ parts.push(`> getter on **${introspection.className || id}** — returns \`${getter.returns || 'unknown'}\``)
636
+ if (getter.description) parts.push(getter.description)
637
+
638
+ if (getter.examples?.length) {
639
+ parts.push(`${hSub} Examples`)
640
+ for (const ex of getter.examples) {
641
+ parts.push(`\`\`\`${ex.language || 'typescript'}\n${ex.code}\n\`\`\``)
642
+ }
643
+ }
644
+
645
+ return { json: { [member]: getter, _helper: id, _type: 'getter' }, text: parts.join('\n\n') }
646
+ }
647
+
648
+ private getBrowserHelperData(id: string, sections: (IntrospectionSection | 'description')[], noTitle: boolean, headingDepth: number): DescribeResult {
649
+ const data = this._browserData!.introspection.get(`features.${id}`)
650
+ if (!data) return { json: {}, text: `No browser introspection data for ${id}` }
651
+
652
+ const text = this.renderBrowserHelperText(data, sections, noTitle, headingDepth)
653
+
654
+ let finalText = text
655
+ if (sections.length === 0 && !noTitle) {
656
+ const summary = this.buildBrowserHelperSummary(data)
657
+ if (summary) {
658
+ const sectionHeading = '#'.repeat(headingDepth + 1) + ' '
659
+ const idx = text.indexOf('\n' + sectionHeading)
660
+ if (idx >= 0) {
661
+ finalText = text.slice(0, idx) + '\n\n' + summary + '\n' + text.slice(idx)
662
+ }
663
+ }
664
+ }
665
+
666
+ return {
667
+ json: this.renderBrowserHelperJson(data, sections, noTitle),
668
+ text: finalText,
669
+ }
670
+ }
671
+
672
+ private getBrowserMemberData(id: string, member: string, memberType: 'method' | 'getter', headingDepth: number): DescribeResult {
673
+ const data = this._browserData!.introspection.get(`features.${id}`)
674
+ if (!data) return { json: {}, text: `No browser introspection data for ${id}` }
675
+
676
+ const h = '#'.repeat(headingDepth)
677
+ const hSub = '#'.repeat(headingDepth + 1)
678
+
679
+ if (memberType === 'method') {
680
+ const method = data.methods?.[member] as MethodIntrospection | undefined
681
+ if (!method) return { json: {}, text: `No introspection data for ${id}.${member}()` }
682
+
683
+ const parts: string[] = [`${h} ${id}.${member}() (browser)`]
684
+ parts.push(`> method on **${data.className || id}**`)
685
+ if (method.description) parts.push(method.description)
686
+
687
+ const paramEntries = Object.entries(method.parameters || {})
688
+ if (paramEntries.length > 0) {
689
+ const paramLines = [`${hSub} Parameters`, '']
690
+ for (const [name, info] of paramEntries) {
691
+ const req = (method.required || []).includes(name) ? ' *(required)*' : ''
692
+ paramLines.push(`- **${name}** \`${info.type}\`${req}${info.description ? ' — ' + info.description : ''}`)
693
+ }
694
+ parts.push(paramLines.join('\n'))
695
+ }
696
+
697
+ if (method.returns && method.returns !== 'void') {
698
+ parts.push(`${hSub} Returns\n\n\`${method.returns}\``)
699
+ }
700
+
701
+ return { json: { [member]: method, _helper: id, _type: 'method', _platform: 'browser' }, text: parts.join('\n\n') }
702
+ }
703
+
704
+ const getter = data.getters?.[member] as GetterIntrospection | undefined
705
+ if (!getter) return { json: {}, text: `No introspection data for ${id}.${member}` }
706
+
707
+ const parts: string[] = [`${h} ${id}.${member} (browser)`]
708
+ parts.push(`> getter on **${data.className || id}** — returns \`${getter.returns || 'unknown'}\``)
709
+ if (getter.description) parts.push(getter.description)
710
+
711
+ return { json: { [member]: getter, _helper: id, _type: 'getter', _platform: 'browser' }, text: parts.join('\n\n') }
712
+ }
713
+
714
+ private getRegistryData(registryName: string, sections: (IntrospectionSection | 'description')[], noTitle: boolean, headingDepth: number, platform: Platform): DescribeResult {
715
+ const includeNode = this.shouldIncludeNode(platform)
716
+ const includeBrowser = this.shouldIncludeBrowser(platform) && registryName === 'features' && this._browserData
717
+
718
+ const registry = this.container[registryName]
719
+ const nodeAvailable: string[] = includeNode ? registry.available : []
720
+ const browserAvailable: string[] = includeBrowser ? this._browserData!.available : []
721
+
722
+ const collidingIds = includeBrowser ? this._browserData!.collidingIds : new Set<string>()
723
+ const totalCount = nodeAvailable.length + browserAvailable.filter(id => !includeNode || !collidingIds.has(id)).length
724
+
725
+ if (totalCount === 0) {
726
+ return { json: {}, text: `No ${registryName} are registered.` }
727
+ }
728
+
729
+ if (sections.length === 0) {
730
+ const h = '#'.repeat(headingDepth)
731
+ const hSub = '#'.repeat(headingDepth + 1)
732
+ const jsonResult: Record<string, any> = {}
733
+ const textParts: string[] = [`${h} Available ${registryName} (${totalCount})\n`]
734
+
735
+ if (includeNode) {
736
+ const baseClass = registry.baseClass
737
+ if (baseClass) {
738
+ const shared = this.collectSharedMembers(baseClass)
739
+ const label = registryName[0]!.toUpperCase() + registryName.slice(1).replace(/s$/, '')
740
+
741
+ if (shared.getters.length) textParts.push(`**Shared ${label} Getters:** ${shared.getters.join(', ')}\n`)
742
+ if (shared.methods.length) textParts.push(`**Shared ${label} Methods:** ${shared.methods.map(m => m + '()').join(', ')}\n`)
743
+ jsonResult._shared = { methods: shared.methods, getters: shared.getters }
744
+ }
745
+
746
+ const baseClassRef = registry.baseClass
747
+ const sorted = [...nodeAvailable].sort((a, b) => {
748
+ const aCtor = registry.lookup(a)
749
+ const bCtor = registry.lookup(b)
750
+ const aIsDirect = !this.findIntermediateParent(aCtor, baseClassRef)
751
+ const bIsDirect = !this.findIntermediateParent(bCtor, baseClassRef)
752
+ if (aIsDirect && !bIsDirect) return -1
753
+ if (!aIsDirect && bIsDirect) return 1
754
+ return 0
755
+ })
756
+
757
+ for (const id of sorted) {
758
+ const Ctor = registry.lookup(id)
759
+ const introspection = Ctor.introspect?.()
760
+ const description = introspection?.description || Ctor.description || 'No description provided'
761
+ const summary = this.extractSummary(description)
762
+ const featureGetters = Object.keys(introspection?.getters || {}).sort()
763
+ const featureMethods = Object.keys(introspection?.methods || {}).sort()
764
+ const intermediate = this.findIntermediateParent(Ctor, baseClassRef)
765
+
766
+ const platformTag = includeBrowser && collidingIds.has(id) ? ' (node)' : ''
767
+
768
+ const entryJson: Record<string, any> = { description: summary, methods: featureMethods, getters: featureGetters }
769
+ if (intermediate) {
770
+ entryJson.extends = intermediate.name
771
+ entryJson.inheritedMethods = intermediate.methods
772
+ entryJson.inheritedGetters = intermediate.getters
773
+ }
774
+ if (platformTag) entryJson.platform = 'node'
775
+ jsonResult[id + (platformTag ? ':node' : '')] = entryJson
776
+
777
+ const extendsLine = intermediate ? `\n> extends ${intermediate.name}\n` : ''
778
+ const memberLines: string[] = []
779
+ if (featureGetters.length) memberLines.push(` getters: ${featureGetters.join(', ')}`)
780
+ if (featureMethods.length) memberLines.push(` methods: ${featureMethods.map(m => m + '()').join(', ')}`)
781
+ if (intermediate) {
782
+ if (intermediate.getters.length) memberLines.push(` inherited getters: ${intermediate.getters.join(', ')}`)
783
+ if (intermediate.methods.length) memberLines.push(` inherited methods: ${intermediate.methods.map(m => m + '()').join(', ')}`)
784
+ }
785
+ const memberBlock = memberLines.length ? '\n' + memberLines.join('\n') + '\n' : ''
786
+ textParts.push(`${hSub} ${id}${platformTag}${extendsLine}\n${summary}\n${memberBlock}`)
787
+ }
788
+ }
789
+
790
+ if (includeBrowser) {
791
+ for (const id of browserAvailable.sort()) {
792
+ if (includeNode && collidingIds.has(id)) {
793
+ const data = this._browserData!.introspection.get(`features.${id}`)
794
+ if (!data) continue
795
+ const summary = this.extractSummary(data.description || 'No description provided')
796
+ const featureGetters = Object.keys(data.getters || {}).sort()
797
+ const featureMethods = Object.keys(data.methods || {}).sort()
798
+
799
+ jsonResult[id + ':browser'] = { description: summary, methods: featureMethods, getters: featureGetters, platform: 'browser' }
800
+
801
+ const memberLines: string[] = []
802
+ if (featureGetters.length) memberLines.push(` getters: ${featureGetters.join(', ')}`)
803
+ if (featureMethods.length) memberLines.push(` methods: ${featureMethods.map(m => m + '()').join(', ')}`)
804
+ const memberBlock = memberLines.length ? '\n' + memberLines.join('\n') + '\n' : ''
805
+ textParts.push(`${hSub} ${id} (browser)\n${summary}\n${memberBlock}`)
806
+ continue
807
+ }
808
+
809
+ const data = this._browserData!.introspection.get(`features.${id}`)
810
+ if (!data) continue
811
+ const summary = this.extractSummary(data.description || 'No description provided')
812
+ const featureGetters = Object.keys(data.getters || {}).sort()
813
+ const featureMethods = Object.keys(data.methods || {}).sort()
814
+
815
+ const platformTag = !includeNode ? '' : ' (browser)'
816
+ jsonResult[id] = { description: summary, methods: featureMethods, getters: featureGetters, platform: 'browser' }
817
+
818
+ const memberLines: string[] = []
819
+ if (featureGetters.length) memberLines.push(` getters: ${featureGetters.join(', ')}`)
820
+ if (featureMethods.length) memberLines.push(` methods: ${featureMethods.map(m => m + '()').join(', ')}`)
821
+ const memberBlock = memberLines.length ? '\n' + memberLines.join('\n') + '\n' : ''
822
+ textParts.push(`${hSub} ${id}${platformTag}\n${summary}\n${memberBlock}`)
823
+ }
824
+ }
825
+
826
+ return { json: jsonResult, text: textParts.join('\n') }
827
+ }
828
+
829
+ // Sections specified: render each helper in detail
830
+ const jsonResult: Record<string, any> = {}
831
+ const textParts: string[] = []
832
+
833
+ if (includeNode) {
834
+ for (const id of nodeAvailable) {
835
+ const Ctor = registry.lookup(id)
836
+ jsonResult[id] = this.renderHelperJson(Ctor, sections, noTitle)
837
+ textParts.push(this.renderHelperText(Ctor, sections, noTitle, headingDepth))
838
+ }
839
+ }
840
+
841
+ if (includeBrowser) {
842
+ for (const id of browserAvailable) {
843
+ if (includeNode && collidingIds.has(id)) continue
844
+ const data = this._browserData!.introspection.get(`features.${id}`)
845
+ if (!data) continue
846
+ jsonResult[id] = this.renderBrowserHelperJson(data, sections, noTitle)
847
+ textParts.push(this.renderBrowserHelperText(data, sections, noTitle, headingDepth))
848
+ }
849
+ }
850
+
851
+ return { json: jsonResult, text: textParts.join('\n\n---\n\n') }
852
+ }
853
+
854
+ // --- Private: Rendering helpers ---
855
+
856
+ private renderTitle(Ctor: any, headingDepth = 1): string {
857
+ const data = Ctor.introspect?.()
858
+ const id = data?.id || Ctor.shortcut || Ctor.name
859
+ const className = data?.className || Ctor.name
860
+ const h = '#'.repeat(headingDepth)
861
+ return className ? `${h} ${className} (${id})` : `${h} ${id}`
862
+ }
863
+
864
+ private renderHelperText(Ctor: any, sections: (IntrospectionSection | 'description')[], noTitle: boolean, headingDepth: number): string {
865
+ if (sections.length === 0) {
866
+ if (noTitle) {
867
+ const data = Ctor.introspect?.()
868
+ if (!data) return 'No introspection data available.'
869
+ const parts: string[] = [data.description]
870
+ const text = Ctor.introspectAsText?.(headingDepth)
871
+ if (text) {
872
+ const lines = text.split('\n')
873
+ const headingPrefix = '#'.repeat(headingDepth + 1) + ' '
874
+ let startIdx = 0
875
+ for (let i = 0; i < lines.length; i++) {
876
+ if (i > 0 && lines[i]!.startsWith(headingPrefix)) {
877
+ startIdx = i
878
+ break
879
+ }
880
+ }
881
+ if (startIdx > 0) {
882
+ parts.length = 0
883
+ parts.push(data.description)
884
+ parts.push(lines.slice(startIdx).join('\n'))
885
+ }
886
+ }
887
+ return parts.join('\n\n')
888
+ }
889
+ return Ctor.introspectAsText?.(headingDepth) ?? `${this.renderTitle(Ctor, headingDepth)}\n\nNo introspection data available.`
890
+ }
891
+
892
+ const introspectionSections = sections.filter((s): s is IntrospectionSection => s !== 'description')
893
+ const parts: string[] = []
894
+
895
+ if (!noTitle) {
896
+ const data = Ctor.introspect?.()
897
+ parts.push(this.renderTitle(Ctor, headingDepth))
898
+ if (data?.description) parts.push(data.description)
899
+ }
900
+
901
+ for (const section of introspectionSections) {
902
+ const text = Ctor.introspectAsText?.(section, headingDepth)
903
+ if (text) parts.push(text)
904
+ }
905
+
906
+ return parts.join('\n\n') || `${noTitle ? '' : this.renderTitle(Ctor, headingDepth) + '\n\n'}No introspection data available.`
907
+ }
908
+
909
+ private renderHelperJson(Ctor: any, sections: (IntrospectionSection | 'description')[], noTitle: boolean): any {
910
+ if (sections.length === 0) return Ctor.introspect?.() ?? {}
911
+
912
+ const data = Ctor.introspect?.() ?? {}
913
+ const result: Record<string, any> = {}
914
+
915
+ if (!noTitle) {
916
+ result.id = data.id
917
+ if (data.className) result.className = data.className
918
+ }
919
+
920
+ for (const section of sections) {
921
+ if (section === 'description') {
922
+ result.id = data.id
923
+ if (data.className) result.className = data.className
924
+ result.description = data.description
925
+ } else if (section === 'usage') {
926
+ result.usage = { shortcut: data.shortcut, options: data.options }
927
+ } else {
928
+ const sectionData = Ctor.introspect?.(section)
929
+ if (sectionData) result[section] = sectionData[section]
930
+ }
931
+ }
932
+
933
+ return result
934
+ }
935
+
936
+ private renderBrowserHelperText(data: HelperIntrospection, sections: (IntrospectionSection | 'description')[], noTitle: boolean, headingDepth: number): string {
937
+ const h = '#'.repeat(headingDepth)
938
+ const className = data.className || data.id
939
+
940
+ if (sections.length === 0) {
941
+ const body = presentIntrospectionAsMarkdown(data, headingDepth)
942
+ if (noTitle) {
943
+ const lines = body.split('\n')
944
+ const sectionHeading = '#'.repeat(headingDepth + 1) + ' '
945
+ const firstSectionIdx = lines.findIndex((l, i) => i > 0 && l.startsWith(sectionHeading))
946
+ const desc = data.description ? data.description + '\n\n' : ''
947
+ if (firstSectionIdx > 0) return desc + lines.slice(firstSectionIdx).join('\n')
948
+ return desc.trim() || 'No introspection data available.'
949
+ }
950
+ return body
951
+ }
952
+
953
+ const parts: string[] = []
954
+ if (!noTitle) {
955
+ parts.push(`${h} ${className} (${data.id})`)
956
+ if (data.description) parts.push(data.description)
957
+ }
958
+
959
+ const introspectionSections = sections.filter((s): s is IntrospectionSection => s !== 'description')
960
+ for (const section of introspectionSections) {
961
+ const text = presentIntrospectionAsMarkdown(data, headingDepth, section)
962
+ if (text) parts.push(text)
963
+ }
964
+
965
+ return parts.join('\n\n') || `${noTitle ? '' : `${h} ${className}\n\n`}No introspection data available.`
966
+ }
967
+
968
+ private renderBrowserHelperJson(data: HelperIntrospection, sections: (IntrospectionSection | 'description')[], noTitle: boolean): any {
969
+ if (sections.length === 0) return data
970
+
971
+ const result: Record<string, any> = {}
972
+ if (!noTitle) {
973
+ result.id = data.id
974
+ if (data.className) result.className = data.className
975
+ }
976
+ for (const section of sections) {
977
+ if (section === 'description') {
978
+ result.id = data.id
979
+ if (data.className) result.className = data.className
980
+ result.description = data.description
981
+ } else if (section === 'usage') {
982
+ result.usage = { shortcut: data.shortcut, options: data.options }
983
+ } else {
984
+ result[section] = (data as any)[section]
985
+ }
986
+ }
987
+ return result
988
+ }
989
+
990
+ // --- Private: Summary builders ---
991
+
992
+ private extractSummary(description: string): string {
993
+ const cut = description.search(/\s\*\*[A-Z][\w\s]+:\*\*|```|^\s*[-*]\s/m)
994
+ const text = cut > 0 ? description.slice(0, cut).trim() : description
995
+ if (text.length <= 300) return text
996
+ const sentenceEnd = text.lastIndexOf('. ', 300)
997
+ if (sentenceEnd > 100) return text.slice(0, sentenceEnd + 1)
998
+ return text.slice(0, 300).trim() + '...'
999
+ }
1000
+
1001
+ private buildHelperSummary(Ctor: any): string {
1002
+ const introspection = Ctor.introspect?.()
1003
+ const ownMethods = Object.keys(introspection?.methods || {}).sort()
1004
+ const ownGetters = Object.keys(introspection?.getters || {}).sort()
1005
+
1006
+ const chain: any[] = []
1007
+ let current = Object.getPrototypeOf(Ctor)
1008
+ while (current && current.name && !BASE_CLASS_NAMES.has(current.name) && current !== Function.prototype) {
1009
+ chain.push(current)
1010
+ current = Object.getPrototypeOf(current)
1011
+ }
1012
+
1013
+ const lines: string[] = []
1014
+ if (chain.length > 0) lines.push(`> extends ${chain[0].name}`)
1015
+ if (ownGetters.length) lines.push(`getters: ${ownGetters.join(', ')}`)
1016
+ if (ownMethods.length) lines.push(`methods: ${ownMethods.map(m => m + '()').join(', ')}`)
1017
+
1018
+ for (const parent of chain) {
1019
+ const parentIntrospection = parent.introspect?.()
1020
+ const inheritedMethods = Object.keys(parentIntrospection?.methods || {}).sort()
1021
+ const inheritedGetters = Object.keys(parentIntrospection?.getters || {}).sort()
1022
+ if (inheritedGetters.length) lines.push(`inherited getters (${parent.name}): ${inheritedGetters.join(', ')}`)
1023
+ if (inheritedMethods.length) lines.push(`inherited methods (${parent.name}): ${inheritedMethods.map(m => m + '()').join(', ')}`)
1024
+ }
1025
+
1026
+ return lines.join('\n')
1027
+ }
1028
+
1029
+ private buildBrowserHelperSummary(data: HelperIntrospection): string {
1030
+ const ownMethods = Object.keys(data.methods || {}).sort()
1031
+ const ownGetters = Object.keys(data.getters || {}).sort()
1032
+ const lines: string[] = []
1033
+ if (ownGetters.length) lines.push(`getters: ${ownGetters.join(', ')}`)
1034
+ if (ownMethods.length) lines.push(`methods: ${ownMethods.map(m => m + '()').join(', ')}`)
1035
+ return lines.join('\n')
1036
+ }
1037
+
1038
+ // --- Private: Prototype chain helpers ---
1039
+
1040
+ private collectSharedMembers(baseClass: any): { methods: string[]; getters: string[] } {
1041
+ const methods: string[] = []
1042
+ const getters: string[] = []
1043
+
1044
+ let proto = baseClass?.prototype
1045
+ while (proto && proto.constructor.name !== 'Object') {
1046
+ for (const k of Object.getOwnPropertyNames(proto)) {
1047
+ if (k === 'constructor' || k.startsWith('_')) continue
1048
+ const desc = Object.getOwnPropertyDescriptor(proto, k)
1049
+ if (!desc) continue
1050
+ if (desc.get && !getters.includes(k)) getters.push(k)
1051
+ else if (typeof desc.value === 'function' && !methods.includes(k)) methods.push(k)
1052
+ }
1053
+ proto = Object.getPrototypeOf(proto)
1054
+ }
1055
+
1056
+ return { methods: methods.sort(), getters: getters.sort() }
1057
+ }
1058
+
1059
+ private findIntermediateParent(Ctor: any, baseClass: any): { name: string; methods: string[]; getters: string[] } | null {
1060
+ if (!baseClass) return null
1061
+
1062
+ let parent = Object.getPrototypeOf(Ctor)
1063
+ if (!parent || parent === baseClass) return null
1064
+
1065
+ const chain: any[] = []
1066
+ let current = parent
1067
+ while (current && current !== baseClass && current !== Function.prototype) {
1068
+ chain.push(current)
1069
+ current = Object.getPrototypeOf(current)
1070
+ }
1071
+
1072
+ if (chain.length === 0 || current !== baseClass) return null
1073
+
1074
+ const intermediate = chain[0]
1075
+ const methods: string[] = []
1076
+ const getters: string[] = []
1077
+
1078
+ const proto = intermediate?.prototype
1079
+ if (proto) {
1080
+ for (const k of Object.getOwnPropertyNames(proto)) {
1081
+ if (k === 'constructor' || k.startsWith('_')) continue
1082
+ const desc = Object.getOwnPropertyDescriptor(proto, k)
1083
+ if (!desc) continue
1084
+ if (desc.get && !getters.includes(k)) getters.push(k)
1085
+ else if (typeof desc.value === 'function' && !methods.includes(k)) methods.push(k)
1086
+ }
1087
+ }
1088
+
1089
+ return {
1090
+ name: intermediate.name,
1091
+ methods: methods.sort(),
1092
+ getters: getters.sort(),
1093
+ }
1094
+ }
1095
+ }
1096
+
1097
+ export { DescribeError, SECTION_FLAGS }
1098
+ export type { ResolvedTarget, Platform, DescribeOptions, DescribeResult, BrowserFeatureData }