@vibe-forge/workspace-assets 0.9.1-alpha.0 → 0.9.2-alpha.0

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,139 +1,179 @@
1
- import { basename, dirname } from 'node:path'
2
-
3
- import { DefinitionLoader } from '@vibe-forge/definition-loader'
4
1
  import type {
2
+ Definition,
5
3
  Filter,
6
- PromptAssetResolution,
7
- ResolvedPromptAssetOptions,
4
+ PluginOverlayConfig,
5
+ Rule,
6
+ RuleReference,
7
+ SkillSelection,
8
8
  WorkspaceAsset,
9
9
  WorkspaceAssetBundle,
10
+ WorkspaceMcpSelection,
10
11
  WorkspaceSkillSelection
11
12
  } from '@vibe-forge/types'
12
13
 
14
+ import { resolveWorkspaceAssetBundle } from './bundle'
13
15
  import {
14
- dedupeSkillAssets,
15
- filterSkillAssets,
16
- pickEntityAsset,
17
- pickSpecAsset,
18
- resolveExcludedSkillNames,
19
- resolveIncludedSkillNames,
20
- resolveRulePatterns,
21
- resolveSelectedRuleAssets,
22
- toDocumentDefinitions,
23
- toPromptAssetIds
24
- } from './document-assets'
25
-
26
- export async function resolvePromptAssetSelection(
27
- params: {
28
- bundle: WorkspaceAssetBundle
29
- type: 'spec' | 'entity' | undefined
30
- name?: string
31
- input?: {
32
- skills?: WorkspaceSkillSelection
33
- }
16
+ generateEntitiesRoutePrompt,
17
+ generateRulesPrompt,
18
+ generateSkillsPrompt,
19
+ generateSkillsRoutePrompt,
20
+ generateSpecRoutePrompt
21
+ } from './prompt-builders'
22
+ import {
23
+ definitionWithResolvedName,
24
+ pickDocumentAsset,
25
+ resolveExcludedSkillRefs,
26
+ resolveIncludedSkillRefs,
27
+ resolveNamedAssets,
28
+ resolvePluginOverlay,
29
+ resolveRuleSelection,
30
+ resolveSelectedSkillAssets,
31
+ toDocumentDefinitions
32
+ } from './selection-internal'
33
+
34
+ export async function resolvePromptAssetSelection(params: {
35
+ bundle: WorkspaceAssetBundle
36
+ type: 'spec' | 'entity' | undefined
37
+ name?: string
38
+ input?: {
39
+ skills?: WorkspaceSkillSelection
40
+ }
41
+ }) {
42
+ const options: {
43
+ systemPrompt?: string
44
+ tools?: Filter
45
+ mcpServers?: WorkspaceMcpSelection
46
+ promptAssetIds?: string[]
47
+ assetBundle?: WorkspaceAssetBundle
48
+ } = {
49
+ assetBundle: params.bundle
34
50
  }
35
- ): Promise<[PromptAssetResolution, ResolvedPromptAssetOptions]> {
36
- const loader = new DefinitionLoader(params.bundle.cwd)
37
- const options: ResolvedPromptAssetOptions = {}
38
- const systemPromptParts: string[] = []
39
-
40
- const entities = params.type !== 'entity'
41
- ? toDocumentDefinitions(params.bundle.entities)
42
- : []
43
- const skills = toDocumentDefinitions(
44
- filterSkillAssets(params.bundle.skills, params.input?.skills)
45
- )
46
- const rules = toDocumentDefinitions(params.bundle.rules)
47
- const specs = toDocumentDefinitions(params.bundle.specs)
48
-
49
- const promptAssetIds = new Set<string>([
50
- ...toPromptAssetIds(params.bundle.rules),
51
- ...(params.type !== 'entity' ? toPromptAssetIds(params.bundle.entities) : []),
52
- ...toPromptAssetIds(params.bundle.specs),
53
- ...toPromptAssetIds(filterSkillAssets(params.bundle.skills, params.input?.skills))
54
- ])
55
51
 
56
- const targetSkillsAssets: Array<Extract<WorkspaceAsset, { kind: 'skill' }>> = []
52
+ let effectiveBundle = params.bundle
53
+ let pinnedTargetAsset: Extract<WorkspaceAsset, { kind: 'spec' | 'entity' }> | undefined
57
54
  let targetBody = ''
58
55
  let targetToolsFilter: Filter | undefined
59
56
  let targetMcpServersFilter: Filter | undefined
60
- let selectedSkillAssets: Array<Extract<WorkspaceAsset, { kind: 'skill' }>> = []
61
-
62
- if (params.input?.skills?.include != null && params.input.skills.include.length > 0) {
63
- selectedSkillAssets = dedupeSkillAssets(
64
- filterSkillAssets(params.bundle.skills, { include: params.input.skills.include })
65
- )
66
- }
57
+ let targetInstancePath: string | undefined
67
58
 
68
59
  if (params.type && params.name) {
69
- const targetAsset = params.type === 'spec'
70
- ? pickSpecAsset(params.bundle, params.name)
71
- : pickEntityAsset(params.bundle, params.name)
72
-
73
- if (targetAsset == null) {
60
+ const baseTarget = params.type === 'spec'
61
+ ? pickDocumentAsset(params.bundle.specs, params.name)
62
+ : pickDocumentAsset(params.bundle.entities, params.name)
63
+ if (baseTarget == null) {
74
64
  throw new Error(`Failed to load ${params.type} ${params.name}`)
75
65
  }
76
66
 
77
- const { definition } = targetAsset.payload
78
- const { attributes, body } = definition
79
- promptAssetIds.add(targetAsset.id)
67
+ const pluginOverlay = baseTarget.payload.definition.attributes.plugins as PluginOverlayConfig | undefined
68
+ if (pluginOverlay != null) {
69
+ effectiveBundle = await resolveWorkspaceAssetBundle({
70
+ cwd: params.bundle.cwd,
71
+ plugins: resolvePluginOverlay(params.bundle.pluginConfigs, pluginOverlay),
72
+ overlaySource: `${params.type}:${baseTarget.displayName}`
73
+ })
74
+ }
75
+
76
+ pinnedTargetAsset = baseTarget
77
+ targetBody = baseTarget.payload.definition.body
78
+ targetToolsFilter = baseTarget.payload.definition.attributes.tools
79
+ targetMcpServersFilter = baseTarget.payload.definition.attributes.mcpServers
80
+ targetInstancePath = baseTarget.instancePath
81
+ options.assetBundle = effectiveBundle
82
+ }
80
83
 
81
- if (attributes.rules) {
82
- const matchedRuleAssets = await resolveSelectedRuleAssets(params.bundle, resolveRulePatterns(attributes.rules))
83
- rules.push(
84
- ...matchedRuleAssets.map((asset) => ({
85
- ...asset.payload.definition,
86
- attributes: {
87
- ...asset.payload.definition.attributes,
88
- always: true
89
- }
90
- }))
84
+ const selectedSkillAssets = resolveSelectedSkillAssets(effectiveBundle.skills, params.input?.skills)
85
+ const promptAssetIds = new Set<string>([
86
+ ...effectiveBundle.rules.map(asset => asset.id),
87
+ ...effectiveBundle.specs.map(asset => asset.id),
88
+ ...selectedSkillAssets.map(asset => asset.id),
89
+ ...(params.type !== 'entity' ? effectiveBundle.entities.map(asset => asset.id) : [])
90
+ ])
91
+ const ruleDefinitions = new Map<string, Definition<Rule>>(
92
+ effectiveBundle.rules.map(asset => [
93
+ asset.id,
94
+ definitionWithResolvedName(asset.payload.definition, asset.displayName, asset.instancePath)
95
+ ])
96
+ )
97
+ const targetSkillsAssets: Array<Extract<WorkspaceAsset, { kind: 'skill' }>> = []
98
+
99
+ if (pinnedTargetAsset != null) {
100
+ promptAssetIds.add(pinnedTargetAsset.id)
101
+ const attributes = pinnedTargetAsset.payload.definition.attributes
102
+
103
+ if (attributes.rules != null) {
104
+ const selection = await resolveRuleSelection(
105
+ effectiveBundle,
106
+ attributes.rules as RuleReference[] | string[],
107
+ targetInstancePath
91
108
  )
92
- for (const asset of matchedRuleAssets) {
109
+ for (const asset of selection.assets) {
93
110
  promptAssetIds.add(asset.id)
111
+ ruleDefinitions.set(
112
+ asset.id,
113
+ definitionWithResolvedName(
114
+ {
115
+ ...asset.payload.definition,
116
+ attributes: {
117
+ ...asset.payload.definition.attributes,
118
+ always: true
119
+ }
120
+ },
121
+ asset.displayName,
122
+ asset.instancePath
123
+ )
124
+ )
94
125
  }
126
+ selection.remoteDefinitions.forEach((definition) => {
127
+ ruleDefinitions.set(definition.path, definition)
128
+ })
95
129
  }
96
130
 
97
- if (attributes.skills) {
98
- const includedSkillNames = new Set(resolveIncludedSkillNames(attributes.skills))
99
- const excludedSkillNames = new Set(resolveExcludedSkillNames(attributes.skills))
100
- for (const skillAsset of params.bundle.skills) {
101
- const skillName = basename(dirname(skillAsset.payload.definition.path))
102
- if (includedSkillNames.size > 0 && !includedSkillNames.has(skillName)) continue
103
- if (excludedSkillNames.has(skillName)) continue
104
- targetSkillsAssets.push(skillAsset)
105
- promptAssetIds.add(skillAsset.id)
106
- }
107
- }
131
+ const skillSelection = attributes.skills as string[] | SkillSelection | undefined
132
+ const includedRefs = resolveIncludedSkillRefs(skillSelection)
133
+ const excludedRefs = resolveExcludedSkillRefs(skillSelection)
134
+ const includedAssets = skillSelection == null
135
+ ? []
136
+ : includedRefs != null
137
+ ? (includedRefs.length > 0 ? resolveNamedAssets(effectiveBundle.skills, includedRefs, targetInstancePath) : [])
138
+ : effectiveBundle.skills
139
+ const excludedIds = new Set(
140
+ resolveNamedAssets(effectiveBundle.skills, excludedRefs, targetInstancePath).map(asset => asset.id)
141
+ )
108
142
 
109
- targetBody = body
110
- targetToolsFilter = attributes.tools
111
- targetMcpServersFilter = attributes.mcpServers
143
+ includedAssets
144
+ .filter(asset => !excludedIds.has(asset.id))
145
+ .forEach((asset) => {
146
+ targetSkillsAssets.push(asset)
147
+ promptAssetIds.add(asset.id)
148
+ })
112
149
  }
113
150
 
151
+ const rules = Array.from(ruleDefinitions.values())
114
152
  const targetSkills = toDocumentDefinitions(targetSkillsAssets)
115
- const selectedSkillsPrompt = toDocumentDefinitions(
116
- selectedSkillAssets.filter(
117
- skill => !targetSkillsAssets.some(target => target.payload.definition.path === skill.payload.definition.path)
118
- )
153
+ const routedSkills = toDocumentDefinitions(
154
+ selectedSkillAssets.filter(skill => !targetSkillsAssets.some(target => target.id === skill.id))
119
155
  )
120
-
121
- systemPromptParts.push(loader.generateRulesPrompt(rules))
122
- systemPromptParts.push(loader.generateSkillsPrompt(targetSkills))
123
- systemPromptParts.push(loader.generateSkillsPrompt(selectedSkillsPrompt))
124
- systemPromptParts.push(loader.generateEntitiesRoutePrompt(entities))
125
- systemPromptParts.push(loader.generateSkillsRoutePrompt(skills))
126
- systemPromptParts.push(loader.generateSpecRoutePrompt(specs))
127
- systemPromptParts.push(targetBody)
128
-
129
- if (targetToolsFilter) {
156
+ const entities = params.type !== 'entity'
157
+ ? toDocumentDefinitions(effectiveBundle.entities)
158
+ : []
159
+ const skills = toDocumentDefinitions(selectedSkillAssets)
160
+ const specs = toDocumentDefinitions(effectiveBundle.specs)
161
+
162
+ options.systemPrompt = [
163
+ generateRulesPrompt(effectiveBundle.cwd, rules),
164
+ generateSkillsPrompt(effectiveBundle.cwd, targetSkills),
165
+ generateEntitiesRoutePrompt(entities),
166
+ generateSkillsRoutePrompt(effectiveBundle.cwd, routedSkills),
167
+ generateSpecRoutePrompt(specs, { active: params.type === 'spec' }),
168
+ targetBody
169
+ ].join('\n\n')
170
+
171
+ if (targetToolsFilter != null) {
130
172
  options.tools = targetToolsFilter
131
173
  }
132
- if (targetMcpServersFilter) {
174
+ if (targetMcpServersFilter != null) {
133
175
  options.mcpServers = targetMcpServersFilter
134
176
  }
135
-
136
- options.systemPrompt = systemPromptParts.join('\n\n')
137
177
  options.promptAssetIds = Array.from(promptAssetIds)
138
178
 
139
179
  return [
@@ -147,5 +187,5 @@ export async function resolvePromptAssetSelection(
147
187
  promptAssetIds: Array.from(promptAssetIds)
148
188
  },
149
189
  options
150
- ]
190
+ ] as const
151
191
  }
@@ -0,0 +1,275 @@
1
+ import type {
2
+ Definition,
3
+ PluginConfig,
4
+ PluginOverlayConfig,
5
+ Rule,
6
+ RuleReference,
7
+ SkillSelection,
8
+ WorkspaceAsset,
9
+ WorkspaceAssetBundle,
10
+ WorkspaceAssetKind,
11
+ WorkspaceMcpSelection,
12
+ WorkspaceSkillSelection
13
+ } from '@vibe-forge/types'
14
+ import { normalizePath } from '@vibe-forge/utils'
15
+ import { mergePluginConfigs, normalizePluginConfig } from '@vibe-forge/utils/plugin-resolver'
16
+ import { glob } from 'fast-glob'
17
+
18
+ import {
19
+ createRemoteRuleDefinition,
20
+ isLocalRuleReference,
21
+ isPathLikeReference,
22
+ isRemoteRuleReference,
23
+ parseScopedReference
24
+ } from '@vibe-forge/definition-core'
25
+
26
+ type DocumentAssetKind = Extract<WorkspaceAssetKind, 'rule' | 'spec' | 'entity' | 'skill'>
27
+ type DocumentAsset<TDefinition> = Extract<WorkspaceAsset, { kind: DocumentAssetKind }> & {
28
+ payload: {
29
+ definition: TDefinition & { path: string }
30
+ }
31
+ }
32
+
33
+ const ASSET_REFERENCE_PATH_SUFFIXES = ['.md', '.json', '.yaml', '.yml']
34
+
35
+ export const definitionWithResolvedName = <TDefinition>(
36
+ definition: Definition<TDefinition>,
37
+ resolvedName: string,
38
+ instancePath?: string
39
+ ) => ({
40
+ ...definition,
41
+ resolvedName,
42
+ resolvedInstancePath: instancePath
43
+ })
44
+
45
+ export const toDocumentDefinitions = <TDefinition>(
46
+ assets: Array<DocumentAsset<TDefinition>>
47
+ ) =>
48
+ assets.map(asset =>
49
+ definitionWithResolvedName(
50
+ asset.payload.definition,
51
+ asset.displayName,
52
+ asset.instancePath
53
+ )
54
+ )
55
+
56
+ const resolveUniqueAssetByName = <TAsset extends Extract<WorkspaceAsset, { kind: DocumentAssetKind }>>(
57
+ assets: TAsset[],
58
+ name: string
59
+ ) => {
60
+ const matches = assets.filter(asset => asset.name === name)
61
+ if (matches.length === 0) return undefined
62
+ const unscopedMatches = matches.filter(asset => asset.scope == null)
63
+ if (unscopedMatches.length === 1) {
64
+ return unscopedMatches[0]
65
+ }
66
+ if (matches.length > 1) {
67
+ throw new Error(
68
+ `Ambiguous asset reference ${name}. Candidates: ${matches.map(match => match.displayName).join(', ')}`
69
+ )
70
+ }
71
+ return matches[0]
72
+ }
73
+
74
+ const resolveScopedAsset = <TAsset extends Extract<WorkspaceAsset, { kind: DocumentAssetKind }>>(
75
+ assets: TAsset[],
76
+ scope: string,
77
+ name: string
78
+ ) => assets.find(asset => asset.scope === scope && asset.name === name)
79
+
80
+ export const findNamedAsset = <TAsset extends Extract<WorkspaceAsset, { kind: DocumentAssetKind }>>(
81
+ assets: TAsset[],
82
+ ref: string,
83
+ currentInstancePath?: string
84
+ ) => {
85
+ const scoped = parseScopedReference(ref, { pathSuffixes: ASSET_REFERENCE_PATH_SUFFIXES })
86
+ if (scoped != null) {
87
+ return resolveScopedAsset(assets, scoped.scope, scoped.name)
88
+ }
89
+
90
+ if (currentInstancePath != null) {
91
+ const local = assets.find(asset => asset.instancePath === currentInstancePath && asset.name === ref)
92
+ if (local != null) {
93
+ return local
94
+ }
95
+ }
96
+
97
+ return resolveUniqueAssetByName(assets, ref)
98
+ }
99
+
100
+ export const resolveNamedAssets = <TAsset extends Extract<WorkspaceAsset, { kind: DocumentAssetKind }>>(
101
+ assets: TAsset[],
102
+ refs: string[] | undefined,
103
+ currentInstancePath?: string
104
+ ) => {
105
+ if (refs == null || refs.length === 0) return [] as TAsset[]
106
+
107
+ const selected: TAsset[] = []
108
+ const seen = new Set<string>()
109
+
110
+ const add = (asset: TAsset) => {
111
+ if (seen.has(asset.id)) return
112
+ seen.add(asset.id)
113
+ selected.push(asset)
114
+ }
115
+
116
+ for (const ref of refs) {
117
+ const asset = findNamedAsset(assets, ref, currentInstancePath)
118
+ if (asset == null) throw new Error(`Failed to resolve asset ${ref}`)
119
+ add(asset)
120
+ }
121
+
122
+ return selected
123
+ }
124
+
125
+ const resolvePathMatchedRules = async (
126
+ bundle: WorkspaceAssetBundle,
127
+ ref: string
128
+ ) => {
129
+ const matchedPaths = new Set(
130
+ (await glob(ref, {
131
+ cwd: bundle.cwd,
132
+ absolute: true
133
+ })).map(normalizePath)
134
+ )
135
+ return bundle.rules.filter(rule => matchedPaths.has(normalizePath(rule.sourcePath)))
136
+ }
137
+
138
+ export const resolveRuleSelection = async (
139
+ bundle: WorkspaceAssetBundle,
140
+ refs: RuleReference[] | string[] | undefined,
141
+ currentInstancePath?: string
142
+ ) => {
143
+ const assets: Array<Extract<WorkspaceAsset, { kind: 'rule' }>> = []
144
+ const remoteDefinitions: Definition<Rule>[] = []
145
+ const seen = new Set<string>()
146
+
147
+ const addAsset = (asset: Extract<WorkspaceAsset, { kind: 'rule' }>) => {
148
+ if (seen.has(asset.id)) return
149
+ seen.add(asset.id)
150
+ assets.push(asset)
151
+ }
152
+
153
+ let remoteIndex = 0
154
+ for (const ref of refs ?? []) {
155
+ if (isRemoteRuleReference(ref)) {
156
+ remoteDefinitions.push(createRemoteRuleDefinition(ref, remoteIndex++))
157
+ continue
158
+ }
159
+
160
+ const value = typeof ref === 'string'
161
+ ? ref
162
+ : isLocalRuleReference(ref)
163
+ ? ref.path
164
+ : undefined
165
+ if (value == null) continue
166
+ if (
167
+ isPathLikeReference(value, {
168
+ pathSuffixes: ASSET_REFERENCE_PATH_SUFFIXES,
169
+ allowGlob: true
170
+ })
171
+ ) {
172
+ const matched = await resolvePathMatchedRules(bundle, value)
173
+ matched.forEach(addAsset)
174
+ continue
175
+ }
176
+
177
+ const asset = findNamedAsset(bundle.rules, value, currentInstancePath)
178
+ if (asset == null) throw new Error(`Failed to resolve rule ${value}`)
179
+ addAsset(asset)
180
+ }
181
+
182
+ return {
183
+ assets,
184
+ remoteDefinitions
185
+ }
186
+ }
187
+
188
+ export const resolveIncludedSkillRefs = (selection: string[] | SkillSelection | undefined) => {
189
+ if (selection == null) return undefined
190
+ if (Array.isArray(selection)) return selection
191
+ return selection.type === 'include' ? selection.list : undefined
192
+ }
193
+
194
+ export const resolveExcludedSkillRefs = (selection: string[] | SkillSelection | undefined) => {
195
+ if (selection == null || Array.isArray(selection)) return undefined
196
+ return selection.type === 'exclude' ? selection.list : undefined
197
+ }
198
+
199
+ export const resolveSelectedSkillAssets = (
200
+ assets: Array<Extract<WorkspaceAsset, { kind: 'skill' }>>,
201
+ selection?: WorkspaceSkillSelection
202
+ ) => {
203
+ if (selection == null) return assets
204
+
205
+ const included = selection.include != null && selection.include.length > 0
206
+ ? resolveNamedAssets(assets, selection.include)
207
+ : assets
208
+ const excluded = new Set(
209
+ resolveNamedAssets(assets, selection.exclude).map(asset => asset.id)
210
+ )
211
+ return included.filter(asset => !excluded.has(asset.id))
212
+ }
213
+
214
+ export const resolveSelectedMcpNames = (
215
+ bundle: WorkspaceAssetBundle,
216
+ selection: WorkspaceMcpSelection | undefined
217
+ ) => {
218
+ const allAssets = Object.values(bundle.mcpServers)
219
+ const includeRefs = selection?.include ??
220
+ (bundle.defaultIncludeMcpServers.length > 0 ? bundle.defaultIncludeMcpServers : undefined)
221
+ const excludeRefs = selection?.exclude ??
222
+ (
223
+ selection?.include == null && bundle.defaultExcludeMcpServers.length > 0
224
+ ? bundle.defaultExcludeMcpServers
225
+ : undefined
226
+ )
227
+
228
+ const resolveRefs = (refs: string[] | undefined) => {
229
+ if (refs == null || refs.length === 0) return undefined
230
+ return new Set(refs.map((ref) => {
231
+ const scoped = parseScopedReference(ref, { pathSuffixes: ASSET_REFERENCE_PATH_SUFFIXES })
232
+ if (scoped != null) {
233
+ const asset = allAssets.find(item => item.scope === scoped.scope && item.name === scoped.name)
234
+ if (asset == null) throw new Error(`Failed to resolve MCP server ${ref}`)
235
+ return asset.displayName
236
+ }
237
+
238
+ const matches = allAssets.filter(item => item.name === ref || item.displayName === ref)
239
+ if (matches.length === 0) throw new Error(`Failed to resolve MCP server ${ref}`)
240
+ if (matches.length > 1) {
241
+ throw new Error(
242
+ `Ambiguous MCP server reference ${ref}. Candidates: ${matches.map(match => match.displayName).join(', ')}`
243
+ )
244
+ }
245
+ return matches[0].displayName
246
+ }))
247
+ }
248
+
249
+ const include = resolveRefs(includeRefs)
250
+ const exclude = resolveRefs(excludeRefs) ?? new Set<string>()
251
+
252
+ return allAssets
253
+ .map(asset => asset.displayName)
254
+ .filter(name => (include == null || include.has(name)) && !exclude.has(name))
255
+ }
256
+
257
+ export const resolvePluginOverlay = (
258
+ basePlugins: PluginConfig | undefined,
259
+ overlay: PluginOverlayConfig | undefined
260
+ ) => {
261
+ if (overlay == null) return basePlugins
262
+ if (overlay.mode !== 'override' && overlay.mode !== 'extend') {
263
+ throw new Error('Invalid plugins overlay. "mode" must be "extend" or "override".')
264
+ }
265
+
266
+ const overlayList = normalizePluginConfig(overlay.list, 'plugins overlay list') ?? []
267
+ return overlay.mode === 'override'
268
+ ? overlayList
269
+ : mergePluginConfigs(basePlugins, overlayList)
270
+ }
271
+
272
+ export const pickDocumentAsset = <TAsset extends Extract<WorkspaceAsset, { kind: 'spec' | 'entity' }>>(
273
+ assets: TAsset[],
274
+ ref: string
275
+ ) => findNamedAsset(assets, ref)