@vibe-forge/workspace-assets 2.0.0 → 2.0.2

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.
@@ -1,5 +1,9 @@
1
1
  import type {
2
2
  Definition,
3
+ Entity,
4
+ EntityInheritance,
5
+ EntityInheritanceMode,
6
+ Filter,
3
7
  PluginConfig,
4
8
  PluginOverlayConfig,
5
9
  Rule,
@@ -22,6 +26,7 @@ import {
22
26
  isRemoteRuleReference,
23
27
  parseScopedReference
24
28
  } from '@vibe-forge/definition-core'
29
+ import { expandSkillAssetDependencies, expandSkillAssetDependenciesWithRegistry } from './skill-dependencies'
25
30
 
26
31
  type DocumentAssetKind = Extract<WorkspaceAssetKind, 'rule' | 'spec' | 'entity' | 'skill'>
27
32
  type DocumentAsset<TDefinition> = Extract<WorkspaceAsset, { kind: DocumentAssetKind }> & {
@@ -29,8 +34,28 @@ type DocumentAsset<TDefinition> = Extract<WorkspaceAsset, { kind: DocumentAssetK
29
34
  definition: TDefinition & { path: string }
30
35
  }
31
36
  }
37
+ type EntityAsset = Extract<WorkspaceAsset, { kind: 'entity' }>
38
+ type EntityInheritanceField = Exclude<keyof EntityInheritance, 'default'>
32
39
 
33
40
  const ASSET_REFERENCE_PATH_SUFFIXES = ['.md', '.json', '.yaml', '.yml']
41
+ const ENTITY_INHERITANCE_FIELDS = ['prompt', 'tags', 'rules', 'skills', 'tools', 'mcpServers'] as const
42
+ const ENTITY_INHERITANCE_MODES = new Set<EntityInheritanceMode>(['append', 'prepend', 'merge', 'replace', 'none'])
43
+ const DEFAULT_CHILD_ENTITY_INHERITANCE: Record<EntityInheritanceField, EntityInheritanceMode> = {
44
+ prompt: 'append',
45
+ tags: 'merge',
46
+ rules: 'merge',
47
+ skills: 'merge',
48
+ tools: 'replace',
49
+ mcpServers: 'replace'
50
+ }
51
+ const PARENT_ENTITY_INHERITANCE: Record<EntityInheritanceField, EntityInheritanceMode> = {
52
+ prompt: 'append',
53
+ tags: 'merge',
54
+ rules: 'merge',
55
+ skills: 'merge',
56
+ tools: 'replace',
57
+ mcpServers: 'replace'
58
+ }
34
59
 
35
60
  export const definitionWithResolvedName = <TDefinition>(
36
61
  definition: Definition<TDefinition>,
@@ -122,6 +147,293 @@ export const resolveNamedAssets = <TAsset extends Extract<WorkspaceAsset, { kind
122
147
  return selected
123
148
  }
124
149
 
150
+ const normalizeEntityExtends = (value: Entity['extends']) => {
151
+ if (typeof value === 'string') return value.trim() !== '' ? [value.trim()] : []
152
+ if (!Array.isArray(value)) return []
153
+
154
+ return value
155
+ .map(ref => ref.trim())
156
+ .filter(Boolean)
157
+ }
158
+
159
+ const parseEntityInheritanceMode = (
160
+ value: unknown,
161
+ label: string
162
+ ): EntityInheritanceMode | undefined => {
163
+ if (value == null) return undefined
164
+ if (typeof value !== 'string' || !ENTITY_INHERITANCE_MODES.has(value as EntityInheritanceMode)) {
165
+ throw new Error(`Invalid entity inherit mode for ${label}: ${String(value)}`)
166
+ }
167
+ return value as EntityInheritanceMode
168
+ }
169
+
170
+ const resolveEntityInheritanceModes = (
171
+ value: Entity['inherit'],
172
+ defaults: Record<EntityInheritanceField, EntityInheritanceMode>
173
+ ) => {
174
+ const modes = { ...defaults }
175
+ if (value == null) return modes
176
+
177
+ if (typeof value === 'string') {
178
+ const defaultMode = parseEntityInheritanceMode(value, 'inherit')
179
+ for (const field of ENTITY_INHERITANCE_FIELDS) {
180
+ modes[field] = defaultMode ?? modes[field]
181
+ }
182
+ return modes
183
+ }
184
+
185
+ const defaultMode = parseEntityInheritanceMode(value.default, 'inherit.default')
186
+ for (const field of ENTITY_INHERITANCE_FIELDS) {
187
+ modes[field] = parseEntityInheritanceMode(value[field], `inherit.${field}`) ?? defaultMode ?? modes[field]
188
+ }
189
+ return modes
190
+ }
191
+
192
+ const toUniqueStrings = (values: string[]) => Array.from(new Set(values))
193
+
194
+ const toUniqueValues = <TValue>(values: TValue[], keyOf: (value: TValue) => string) => {
195
+ const seen = new Set<string>()
196
+ const result: TValue[] = []
197
+
198
+ for (const value of values) {
199
+ const key = keyOf(value)
200
+ if (seen.has(key)) continue
201
+ seen.add(key)
202
+ result.push(value)
203
+ }
204
+
205
+ return result
206
+ }
207
+
208
+ const keyRuleReference = (rule: RuleReference) => (
209
+ typeof rule === 'string' ? `string:${rule}` : `object:${JSON.stringify(rule)}`
210
+ )
211
+
212
+ const qualifyEntityReference = (
213
+ asset: EntityAsset,
214
+ ref: string
215
+ ) => {
216
+ const value = ref.trim()
217
+ if (value === '' || asset.scope == null) return value
218
+ if (parseScopedReference(value, { pathSuffixes: ASSET_REFERENCE_PATH_SUFFIXES }) != null) return value
219
+ if (
220
+ isPathLikeReference(value, {
221
+ pathSuffixes: ASSET_REFERENCE_PATH_SUFFIXES,
222
+ allowGlob: true
223
+ })
224
+ ) {
225
+ return value
226
+ }
227
+
228
+ return `${asset.scope}/${value}`
229
+ }
230
+
231
+ const qualifyEntityRuleReferences = (
232
+ asset: EntityAsset,
233
+ rules: Entity['rules']
234
+ ) => rules?.map(rule => typeof rule === 'string' ? qualifyEntityReference(asset, rule) : rule)
235
+
236
+ const qualifyEntitySkillSelection = (
237
+ asset: EntityAsset,
238
+ selection: Entity['skills']
239
+ ): Entity['skills'] => {
240
+ if (selection == null) return undefined
241
+ if (Array.isArray(selection)) return selection.map(ref => qualifyEntityReference(asset, ref))
242
+
243
+ return {
244
+ ...selection,
245
+ list: selection.list.map(ref => qualifyEntityReference(asset, ref))
246
+ }
247
+ }
248
+
249
+ const qualifyEntityInternalReferences = (
250
+ asset: EntityAsset,
251
+ definition: Definition<Entity>
252
+ ): Definition<Entity> => ({
253
+ ...definition,
254
+ attributes: {
255
+ ...definition.attributes,
256
+ rules: qualifyEntityRuleReferences(asset, definition.attributes.rules),
257
+ skills: qualifyEntitySkillSelection(asset, definition.attributes.skills)
258
+ }
259
+ })
260
+
261
+ const selectInheritedValue = <TValue>(
262
+ parent: TValue | undefined,
263
+ child: TValue | undefined,
264
+ mode: EntityInheritanceMode,
265
+ merge: (left: TValue, right: TValue) => TValue
266
+ ) => {
267
+ if (mode === 'none') return child
268
+ if (mode === 'replace') return child ?? parent
269
+ if (parent == null) return child
270
+ if (child == null) return parent
271
+ return mode === 'prepend' ? merge(child, parent) : merge(parent, child)
272
+ }
273
+
274
+ const mergeEntityBody = (
275
+ parent: string,
276
+ child: string,
277
+ mode: EntityInheritanceMode
278
+ ) => {
279
+ if (mode === 'none' || mode === 'replace') return child
280
+
281
+ const values = mode === 'prepend' ? [child, parent] : [parent, child]
282
+ return values
283
+ .map(value => value.trim())
284
+ .filter(Boolean)
285
+ .join('\n\n')
286
+ }
287
+
288
+ const getSkillIncludeRefs = (selection: Entity['skills']) => {
289
+ if (Array.isArray(selection)) return selection
290
+ return selection?.type === 'include' ? selection.list : undefined
291
+ }
292
+
293
+ const mergeSkillSelections = (
294
+ parent: Entity['skills'],
295
+ child: Entity['skills']
296
+ ): Entity['skills'] => {
297
+ const parentRefs = getSkillIncludeRefs(parent)
298
+ const childRefs = getSkillIncludeRefs(child)
299
+ if (parentRefs != null && childRefs != null) return toUniqueStrings([...parentRefs, ...childRefs])
300
+
301
+ return child ?? parent
302
+ }
303
+
304
+ const mergeFilters = (
305
+ parent: Filter,
306
+ child: Filter
307
+ ): Filter => {
308
+ const include = toUniqueStrings([
309
+ ...(parent.include ?? []),
310
+ ...(child.include ?? [])
311
+ ])
312
+ const exclude = toUniqueStrings([
313
+ ...(parent.exclude ?? []),
314
+ ...(child.exclude ?? [])
315
+ ])
316
+
317
+ return {
318
+ ...(include.length > 0 ? { include } : {}),
319
+ ...(exclude.length > 0 ? { exclude } : {})
320
+ }
321
+ }
322
+
323
+ const mergeEntityDefinitions = (
324
+ parent: Definition<Entity>,
325
+ child: Definition<Entity>,
326
+ modes: Record<EntityInheritanceField, EntityInheritanceMode>
327
+ ): Definition<Entity> => ({
328
+ ...child,
329
+ body: mergeEntityBody(parent.body, child.body, modes.prompt),
330
+ attributes: {
331
+ ...parent.attributes,
332
+ ...child.attributes,
333
+ name: child.attributes.name,
334
+ description: child.attributes.description ?? parent.attributes.description,
335
+ always: child.attributes.always ?? parent.attributes.always,
336
+ tags: selectInheritedValue(
337
+ parent.attributes.tags,
338
+ child.attributes.tags,
339
+ modes.tags,
340
+ (left, right) => toUniqueStrings([...left, ...right])
341
+ ),
342
+ rules: selectInheritedValue(
343
+ parent.attributes.rules,
344
+ child.attributes.rules,
345
+ modes.rules,
346
+ (left, right) => toUniqueValues([...left, ...right], keyRuleReference)
347
+ ),
348
+ skills: selectInheritedValue(parent.attributes.skills, child.attributes.skills, modes.skills, mergeSkillSelections),
349
+ tools: selectInheritedValue(parent.attributes.tools, child.attributes.tools, modes.tools, mergeFilters),
350
+ mcpServers: selectInheritedValue(
351
+ parent.attributes.mcpServers,
352
+ child.attributes.mcpServers,
353
+ modes.mcpServers,
354
+ mergeFilters
355
+ ),
356
+ plugins: child.attributes.plugins
357
+ }
358
+ })
359
+
360
+ const uniqueAssetIds = (values: string[]) => toUniqueValues(values, value => value)
361
+
362
+ const formatEntityCycle = (stack: EntityAsset[], asset: EntityAsset) => (
363
+ [...stack.slice(stack.findIndex(item => item.id === asset.id)), asset]
364
+ .map(item => item.displayName)
365
+ .join(' -> ')
366
+ )
367
+
368
+ const createAvailableEntitiesMessage = (entities: EntityAsset[]) => (
369
+ entities
370
+ .map(asset => asset.displayName)
371
+ .sort((left, right) => left.localeCompare(right))
372
+ .join(', ')
373
+ )
374
+
375
+ export const resolveEntityInheritance = (
376
+ bundle: WorkspaceAssetBundle,
377
+ asset: EntityAsset
378
+ ): {
379
+ assetIds: string[]
380
+ definition: Definition<Entity>
381
+ } => {
382
+ const resolveAsset = (
383
+ current: EntityAsset,
384
+ stack: EntityAsset[]
385
+ ): {
386
+ assetIds: string[]
387
+ definition: Definition<Entity>
388
+ } => {
389
+ if (stack.some(item => item.id === current.id)) {
390
+ throw new Error(`Circular entity inheritance detected: ${formatEntityCycle(stack, current)}`)
391
+ }
392
+
393
+ const currentDefinition = definitionWithResolvedName(
394
+ current.payload.definition,
395
+ current.displayName,
396
+ current.instancePath
397
+ )
398
+ const qualifiedCurrentDefinition = qualifyEntityInternalReferences(current, currentDefinition)
399
+ const parentRefs = normalizeEntityExtends(qualifiedCurrentDefinition.attributes.extends)
400
+ if (parentRefs.length === 0) {
401
+ return {
402
+ assetIds: [current.id],
403
+ definition: qualifiedCurrentDefinition
404
+ }
405
+ }
406
+
407
+ let inheritedBase: Definition<Entity> | undefined
408
+ const inheritedAssetIds: string[] = []
409
+ for (const ref of parentRefs) {
410
+ const parentAsset = findNamedAsset(bundle.entities, ref, current.instancePath)
411
+ if (parentAsset == null) {
412
+ throw new Error(
413
+ `Failed to resolve entity ${ref}. Available entities: ${createAvailableEntitiesMessage(bundle.entities)}`
414
+ )
415
+ }
416
+
417
+ const parent = resolveAsset(parentAsset, [...stack, current])
418
+ inheritedAssetIds.push(...parent.assetIds)
419
+ inheritedBase = inheritedBase == null
420
+ ? parent.definition
421
+ : mergeEntityDefinitions(inheritedBase, parent.definition, PARENT_ENTITY_INHERITANCE)
422
+ }
423
+
424
+ return {
425
+ assetIds: uniqueAssetIds([...inheritedAssetIds, current.id]),
426
+ definition: mergeEntityDefinitions(
427
+ inheritedBase ?? qualifiedCurrentDefinition,
428
+ qualifiedCurrentDefinition,
429
+ resolveEntityInheritanceModes(qualifiedCurrentDefinition.attributes.inherit, DEFAULT_CHILD_ENTITY_INHERITANCE)
430
+ )
431
+ }
432
+ }
433
+
434
+ return resolveAsset(asset, [])
435
+ }
436
+
125
437
  const resolvePathMatchedRules = async (
126
438
  bundle: WorkspaceAssetBundle,
127
439
  ref: string
@@ -211,7 +523,29 @@ export const resolveSelectedSkillAssets = (
211
523
  const excluded = new Set(
212
524
  resolveNamedAssets(assets, selection.exclude).map(asset => asset.id)
213
525
  )
214
- return included.filter(asset => !excluded.has(asset.id))
526
+ return expandSkillAssetDependencies(assets, included, {
527
+ excludedIds: excluded
528
+ })
529
+ }
530
+
531
+ export const resolveSelectedSkillAssetsWithDependencies = async (
532
+ bundle: WorkspaceAssetBundle,
533
+ selection?: WorkspaceSkillSelection
534
+ ): Promise<Array<Extract<WorkspaceAsset, { kind: 'skill' }>>> => {
535
+ const included = selection?.include != null && selection.include.length > 0
536
+ ? resolveNamedAssets(bundle.skills, selection.include)
537
+ : bundle.skills
538
+ const excluded = new Set(
539
+ resolveNamedAssets(bundle.skills, selection?.exclude).map(asset => asset.id)
540
+ )
541
+ return await expandSkillAssetDependenciesWithRegistry({
542
+ allAssets: bundle.assets,
543
+ configs: bundle.configs ?? [undefined, undefined],
544
+ cwd: bundle.cwd,
545
+ excludedIds: excluded,
546
+ selectedAssets: included,
547
+ skillAssets: bundle.skills
548
+ })
215
549
  }
216
550
 
217
551
  export const resolveSelectedMcpNames = (