@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.
@@ -435,6 +435,313 @@ describe('resolvePromptAssetSelection', () => {
435
435
  expect(options.systemPrompt).not.toContain('<skill-content>\n先读 README.md\n</skill-content>')
436
436
  })
437
437
 
438
+ it('appends conventional prompt files when loading a directory entity', async () => {
439
+ const workspace = await createWorkspace()
440
+
441
+ await writeDocument(
442
+ join(workspace, '.ai/entities/reviewer/README.md'),
443
+ [
444
+ '---',
445
+ 'description: 代码评审实体',
446
+ '---',
447
+ '基础身份。'
448
+ ].join('\n')
449
+ )
450
+ await writeDocument(
451
+ join(workspace, '.ai/entities/reviewer/INTRODUCTION.md'),
452
+ '负责发现行为回归。'
453
+ )
454
+ await writeDocument(
455
+ join(workspace, '.ai/entities/reviewer/PERSONALITY.md'),
456
+ '说话克制,先给出高风险问题。'
457
+ )
458
+ await writeDocument(
459
+ join(workspace, '.ai/entities/reviewer/MEMORY.md'),
460
+ '记住上次评审指出过缺少验证。'
461
+ )
462
+
463
+ const bundle = await resolveWorkspaceAssetBundle({
464
+ cwd: workspace,
465
+ useDefaultVibeForgeMcpServer: false
466
+ })
467
+ const [data, options] = await resolvePromptAssetSelection({
468
+ bundle,
469
+ type: 'entity',
470
+ name: 'reviewer'
471
+ })
472
+
473
+ expect(data.targetBody).toContain('基础身份。')
474
+ expect(data.targetBody).toContain('## Introduction\n\n负责发现行为回归。')
475
+ expect(data.targetBody).toContain('## Personality\n\n说话克制,先给出高风险问题。')
476
+ expect(data.targetBody).toContain('## Memory\n\n记住上次评审指出过缺少验证。')
477
+ expect(options.systemPrompt).toContain('## Introduction\n\n负责发现行为回归。')
478
+ expect(data.targetBody.indexOf('基础身份。')).toBeLessThan(data.targetBody.indexOf('## Introduction'))
479
+ expect(data.targetBody.indexOf('## Introduction')).toBeLessThan(data.targetBody.indexOf('## Personality'))
480
+ expect(data.targetBody.indexOf('## Personality')).toBeLessThan(data.targetBody.indexOf('## Memory'))
481
+ })
482
+
483
+ it('appends alias prompt files for index json entities', async () => {
484
+ const workspace = await createWorkspace()
485
+
486
+ await writeDocument(
487
+ join(workspace, '.ai/entities/reviewer/index.json'),
488
+ JSON.stringify(
489
+ {
490
+ description: '代码评审实体',
491
+ prompt: '基础身份。'
492
+ },
493
+ null,
494
+ 2
495
+ )
496
+ )
497
+ await writeDocument(
498
+ join(workspace, '.ai/entities/reviewer/介绍.md'),
499
+ '负责发现行为回归。'
500
+ )
501
+ await writeDocument(
502
+ join(workspace, '.ai/entities/reviewer/personality.md'),
503
+ '说话克制,先给出高风险问题。'
504
+ )
505
+ await writeDocument(
506
+ join(workspace, '.ai/entities/reviewer/记忆.md'),
507
+ '记住上次评审指出过缺少验证。'
508
+ )
509
+
510
+ const bundle = await resolveWorkspaceAssetBundle({
511
+ cwd: workspace,
512
+ useDefaultVibeForgeMcpServer: false
513
+ })
514
+ const [data] = await resolvePromptAssetSelection({
515
+ bundle,
516
+ type: 'entity',
517
+ name: 'reviewer'
518
+ })
519
+
520
+ expect(data.targetBody).toContain('基础身份。')
521
+ expect(data.targetBody).toContain('## Introduction\n\n负责发现行为回归。')
522
+ expect(data.targetBody).toContain('## Personality\n\n说话克制,先给出高风险问题。')
523
+ expect(data.targetBody).toContain('## Memory\n\n记住上次评审指出过缺少验证。')
524
+ })
525
+
526
+ it('inherits prompt, rules, and skills from scoped plugin entities', async () => {
527
+ const workspace = await createWorkspace()
528
+
529
+ await installPluginPackage(workspace, '@vibe-forge/plugin-standard-dev', {
530
+ 'package.json': JSON.stringify(
531
+ {
532
+ name: '@vibe-forge/plugin-standard-dev',
533
+ version: '1.0.0'
534
+ },
535
+ null,
536
+ 2
537
+ ),
538
+ 'rules/base-review.md': '---\ndescription: 标准评审规则\n---\n父规则正文',
539
+ 'skills/base-review/SKILL.md': '---\ndescription: 标准评审技能\n---\n父技能正文',
540
+ 'entities/base-reviewer/README.md': [
541
+ '---',
542
+ 'description: 标准评审实体',
543
+ 'rules:',
544
+ ' - base-review',
545
+ 'skills:',
546
+ ' - base-review',
547
+ 'tools:',
548
+ ' include:',
549
+ ' - Read',
550
+ '---',
551
+ '父实体提示。'
552
+ ].join('\n')
553
+ })
554
+ await writeDocument(
555
+ join(workspace, '.ai/rules/base-review.md'),
556
+ '---\ndescription: 同名本地规则\n---\n同名本地规则正文'
557
+ )
558
+ await writeDocument(
559
+ join(workspace, '.ai/rules/frontend.md'),
560
+ '---\ndescription: 前端规则\n---\n子规则正文'
561
+ )
562
+ await writeDocument(
563
+ join(workspace, '.ai/skills/base-review/SKILL.md'),
564
+ '---\ndescription: 同名本地技能\n---\n同名本地技能正文'
565
+ )
566
+ await writeDocument(
567
+ join(workspace, '.ai/skills/frontend/SKILL.md'),
568
+ '---\ndescription: 前端技能\n---\n子技能正文'
569
+ )
570
+ await writeDocument(
571
+ join(workspace, '.ai/entities/frontend-reviewer/README.md'),
572
+ [
573
+ '---',
574
+ 'description: 前端评审实体',
575
+ 'extends: std/base-reviewer',
576
+ 'rules:',
577
+ ' - frontend',
578
+ 'skills:',
579
+ ' - frontend',
580
+ 'tools:',
581
+ ' include:',
582
+ ' - Grep',
583
+ '---',
584
+ '子实体提示。'
585
+ ].join('\n')
586
+ )
587
+
588
+ const bundle = await resolveWorkspaceAssetBundle({
589
+ cwd: workspace,
590
+ configs: [{
591
+ plugins: [
592
+ { id: 'standard-dev', scope: 'std' }
593
+ ]
594
+ }, undefined],
595
+ useDefaultVibeForgeMcpServer: false
596
+ })
597
+ const [data, options] = await resolvePromptAssetSelection({
598
+ bundle,
599
+ type: 'entity',
600
+ name: 'frontend-reviewer'
601
+ })
602
+ const parentEntityId = bundle.entities.find(asset => asset.displayName === 'std/base-reviewer')?.id
603
+ const childEntityId = bundle.entities.find(asset => asset.displayName === 'frontend-reviewer')?.id
604
+
605
+ expect(data.targetBody).toContain('父实体提示。\n\n子实体提示。')
606
+ expect(data.targetSkills.map(skill => skill.resolvedName)).toEqual(['std/base-review', 'frontend'])
607
+ expect(options.systemPrompt).toContain('> 父规则正文')
608
+ expect(options.systemPrompt).toContain('> 子规则正文')
609
+ expect(options.systemPrompt).not.toContain('> 同名本地规则正文')
610
+ expect(options.systemPrompt).not.toContain('<skill-content>\n同名本地技能正文\n</skill-content>')
611
+ expect(options.tools).toEqual({ include: ['Grep'] })
612
+ expect(options.promptAssetIds).toEqual(expect.arrayContaining([parentEntityId, childEntityId]))
613
+ })
614
+
615
+ it('composes multiple parent entities in extends order', async () => {
616
+ const workspace = await createWorkspace()
617
+
618
+ await writeDocument(
619
+ join(workspace, '.ai/entities/base-a.md'),
620
+ [
621
+ '---',
622
+ 'description: 父实体 A',
623
+ 'tools:',
624
+ ' include:',
625
+ ' - Read',
626
+ '---',
627
+ '父实体 A 提示。'
628
+ ].join('\n')
629
+ )
630
+ await writeDocument(
631
+ join(workspace, '.ai/entities/base-b.md'),
632
+ [
633
+ '---',
634
+ 'description: 父实体 B',
635
+ 'tools:',
636
+ ' include:',
637
+ ' - Bash',
638
+ '---',
639
+ '父实体 B 提示。'
640
+ ].join('\n')
641
+ )
642
+ await writeDocument(
643
+ join(workspace, '.ai/entities/child.md'),
644
+ [
645
+ '---',
646
+ 'description: 子实体',
647
+ 'extends:',
648
+ ' - base-a',
649
+ ' - base-b',
650
+ '---',
651
+ '子实体提示。'
652
+ ].join('\n')
653
+ )
654
+
655
+ const bundle = await resolveWorkspaceAssetBundle({
656
+ cwd: workspace,
657
+ useDefaultVibeForgeMcpServer: false
658
+ })
659
+ const [data, options] = await resolvePromptAssetSelection({
660
+ bundle,
661
+ type: 'entity',
662
+ name: 'child'
663
+ })
664
+
665
+ expect(data.targetBody).toContain('父实体 A 提示。\n\n父实体 B 提示。\n\n子实体提示。')
666
+ expect(options.tools).toEqual({ include: ['Bash'] })
667
+ })
668
+
669
+ it('lets the current entity replace inherited rules', async () => {
670
+ const workspace = await createWorkspace()
671
+
672
+ await writeDocument(
673
+ join(workspace, '.ai/rules/base.md'),
674
+ '---\ndescription: 父规则\n---\n父规则正文'
675
+ )
676
+ await writeDocument(
677
+ join(workspace, '.ai/rules/child.md'),
678
+ '---\ndescription: 子规则\n---\n子规则正文'
679
+ )
680
+ await writeDocument(
681
+ join(workspace, '.ai/entities/base.md'),
682
+ [
683
+ '---',
684
+ 'description: 父实体',
685
+ 'rules:',
686
+ ' - base',
687
+ '---',
688
+ '父实体提示。'
689
+ ].join('\n')
690
+ )
691
+ await writeDocument(
692
+ join(workspace, '.ai/entities/child.md'),
693
+ [
694
+ '---',
695
+ 'description: 子实体',
696
+ 'extends: base',
697
+ 'inherit:',
698
+ ' rules: replace',
699
+ 'rules:',
700
+ ' - child',
701
+ '---',
702
+ '子实体提示。'
703
+ ].join('\n')
704
+ )
705
+
706
+ const bundle = await resolveWorkspaceAssetBundle({
707
+ cwd: workspace,
708
+ useDefaultVibeForgeMcpServer: false
709
+ })
710
+ const [, options] = await resolvePromptAssetSelection({
711
+ bundle,
712
+ type: 'entity',
713
+ name: 'child'
714
+ })
715
+
716
+ expect(options.systemPrompt).toContain('> 子规则正文')
717
+ expect(options.systemPrompt).toContain('> Use when: 父规则')
718
+ expect(options.systemPrompt).not.toContain('> 父规则正文')
719
+ })
720
+
721
+ it('rejects circular entity inheritance', async () => {
722
+ const workspace = await createWorkspace()
723
+
724
+ await writeDocument(
725
+ join(workspace, '.ai/entities/a.md'),
726
+ '---\ndescription: A\nextends: b\n---\nA'
727
+ )
728
+ await writeDocument(
729
+ join(workspace, '.ai/entities/b.md'),
730
+ '---\ndescription: B\nextends: a\n---\nB'
731
+ )
732
+
733
+ const bundle = await resolveWorkspaceAssetBundle({
734
+ cwd: workspace,
735
+ useDefaultVibeForgeMcpServer: false
736
+ })
737
+
738
+ await expect(resolvePromptAssetSelection({
739
+ bundle,
740
+ type: 'entity',
741
+ name: 'a'
742
+ })).rejects.toThrow('Circular entity inheritance detected: a -> b -> a')
743
+ })
744
+
438
745
  it('does not preload all skills when the target entity omits skill references', async () => {
439
746
  const workspace = await createWorkspace()
440
747
 
@@ -169,6 +169,7 @@ const summarizeDiagnostics = (
169
169
  assetId: assetIdMap.get(diagnostic.assetId) ?? diagnostic.assetId,
170
170
  adapter: diagnostic.adapter,
171
171
  status: diagnostic.status,
172
+ source: diagnostic.source,
172
173
  reason: diagnostic.reason,
173
174
  scope: diagnostic.scope,
174
175
  packageId: diagnostic.packageId
@@ -5,10 +5,14 @@ import { dirname, join } from 'node:path'
5
5
  import { afterEach } from 'vitest'
6
6
 
7
7
  const tempDirs: string[] = []
8
+ const originalRealHome = process.env.__VF_PROJECT_REAL_HOME__
8
9
 
9
10
  export const createWorkspace = async () => {
10
11
  const dir = await mkdtemp(join(tmpdir(), 'workspace-assets-'))
12
+ const realHome = await mkdtemp(join(tmpdir(), 'workspace-assets-home-'))
11
13
  tempDirs.push(dir)
14
+ tempDirs.push(realHome)
15
+ process.env.__VF_PROJECT_REAL_HOME__ = realHome
12
16
  return dir
13
17
  }
14
18
 
@@ -32,4 +36,9 @@ export const installPluginPackage = async (
32
36
 
33
37
  afterEach(async () => {
34
38
  await Promise.all(tempDirs.splice(0).map(dir => rm(dir, { recursive: true, force: true })))
39
+ if (originalRealHome == null) {
40
+ delete process.env.__VF_PROJECT_REAL_HOME__
41
+ } else {
42
+ process.env.__VF_PROJECT_REAL_HOME__ = originalRealHome
43
+ }
35
44
  })
@@ -189,7 +189,7 @@ describe('workspace assets snapshots', () => {
189
189
  })
190
190
 
191
191
  const adapters = ['claude-code', 'codex', 'gemini', 'opencode'] as const
192
- const plans = adapters.map(adapter => (
192
+ const plans = await Promise.all(adapters.map(adapter => (
193
193
  buildAdapterAssetPlan({
194
194
  adapter,
195
195
  bundle,
@@ -201,7 +201,7 @@ describe('workspace assets snapshots', () => {
201
201
  }
202
202
  }
203
203
  })
204
- ))
204
+ )))
205
205
 
206
206
  await expect(serializeWorkspaceAssetsSnapshot({
207
207
  cwd: workspace,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-forge/workspace-assets",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "Workspace asset resolution and adapter asset planning for Vibe Forge",
5
5
  "imports": {
6
6
  "#~/*.js": {
@@ -26,13 +26,13 @@
26
26
  "./package.json": "./package.json"
27
27
  },
28
28
  "dependencies": {
29
+ "@vibe-forge/config": "^2.0.2",
29
30
  "fast-glob": "^3.3.3",
30
31
  "front-matter": "^4.0.2",
31
32
  "js-yaml": "^4.1.1",
32
- "@vibe-forge/utils": "^2.0.0",
33
33
  "@vibe-forge/definition-core": "^2.0.0",
34
- "@vibe-forge/types": "^2.0.0",
35
- "@vibe-forge/config": "^2.0.0"
34
+ "@vibe-forge/types": "^2.0.2",
35
+ "@vibe-forge/utils": "^2.0.3"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/js-yaml": "^4.0.9"
@@ -11,9 +11,10 @@ import type {
11
11
  } from '@vibe-forge/types'
12
12
 
13
13
  import { resolveNativeSkillDiagnosticReason, supportsNativeProjectSkills } from './adapter-capabilities'
14
- import { resolveSelectedMcpNames, resolveSelectedSkillAssets } from './selection-internal'
14
+ import { resolveWorkspaceAssetSource } from './asset-source'
15
+ import { resolveSelectedMcpNames, resolveSelectedSkillAssetsWithDependencies } from './selection-internal'
15
16
 
16
- export function buildAdapterAssetPlan(params: {
17
+ export async function buildAdapterAssetPlan(params: {
17
18
  adapter: WorkspaceAssetAdapter
18
19
  bundle: WorkspaceAssetBundle
19
20
  options: {
@@ -21,17 +22,24 @@ export function buildAdapterAssetPlan(params: {
21
22
  skills?: WorkspaceSkillSelection
22
23
  promptAssetIds?: string[]
23
24
  }
24
- }): AdapterAssetPlan {
25
+ }): Promise<AdapterAssetPlan> {
25
26
  const diagnostics: AssetDiagnostic[] = []
26
-
27
- for (const assetId of params.options.promptAssetIds ?? []) {
28
- const asset = params.bundle.assets.find(item => item.id === assetId)
29
- if (asset == null || asset.kind === 'mcpServer') continue
27
+ const pushDiagnostic = (
28
+ asset: Parameters<typeof resolveWorkspaceAssetSource>[0] & {
29
+ id: string
30
+ packageId?: string
31
+ scope?: string
32
+ instancePath?: string
33
+ taskOverlaySource?: string
34
+ },
35
+ diagnostic: Pick<AssetDiagnostic, 'adapter' | 'status' | 'reason'>
36
+ ) => {
30
37
  diagnostics.push({
31
- assetId,
32
- adapter: params.adapter,
33
- status: 'prompt',
34
- reason: 'Mapped into the generated system prompt.',
38
+ assetId: asset.id,
39
+ adapter: diagnostic.adapter,
40
+ status: diagnostic.status,
41
+ reason: diagnostic.reason,
42
+ source: resolveWorkspaceAssetSource(asset),
35
43
  packageId: asset.packageId,
36
44
  scope: asset.scope,
37
45
  instancePath: asset.instancePath,
@@ -41,6 +49,16 @@ export function buildAdapterAssetPlan(params: {
41
49
  })
42
50
  }
43
51
 
52
+ for (const assetId of params.options.promptAssetIds ?? []) {
53
+ const asset = params.bundle.assets.find(item => item.id === assetId)
54
+ if (asset == null || asset.kind === 'mcpServer') continue
55
+ pushDiagnostic(asset, {
56
+ adapter: params.adapter,
57
+ status: 'prompt',
58
+ reason: 'Mapped into the generated system prompt.'
59
+ })
60
+ }
61
+
44
62
  const selectedMcpNames = resolveSelectedMcpNames(params.bundle, params.options.mcpServers)
45
63
  const mcpServers = Object.fromEntries(
46
64
  selectedMcpNames.map(name => [name, params.bundle.mcpServers[name].payload.config])
@@ -48,25 +66,17 @@ export function buildAdapterAssetPlan(params: {
48
66
 
49
67
  selectedMcpNames.forEach((name) => {
50
68
  const asset = params.bundle.mcpServers[name]
51
- diagnostics.push({
52
- assetId: asset.id,
69
+ pushDiagnostic(asset, {
53
70
  adapter: params.adapter,
54
71
  status: params.adapter === 'claude-code' ? 'native' : 'translated',
55
72
  reason: params.adapter === 'claude-code'
56
73
  ? 'Mapped into adapter MCP settings.'
57
- : 'Translated into adapter-specific MCP configuration.',
58
- packageId: asset.packageId,
59
- scope: asset.scope,
60
- instancePath: asset.instancePath,
61
- origin: asset.origin,
62
- resolvedBy: asset.resolvedBy,
63
- taskOverlaySource: asset.taskOverlaySource
74
+ : 'Translated into adapter-specific MCP configuration.'
64
75
  })
65
76
  })
66
77
 
67
78
  params.bundle.hookPlugins.forEach((asset) => {
68
- diagnostics.push({
69
- assetId: asset.id,
79
+ pushDiagnostic(asset, {
70
80
  adapter: params.adapter,
71
81
  status: params.adapter === 'copilot' ? 'translated' : 'native',
72
82
  reason: params.adapter === 'claude-code'
@@ -79,80 +89,46 @@ export function buildAdapterAssetPlan(params: {
79
89
  ? 'Handled by the Vibe Forge task hook bridge.'
80
90
  : params.adapter === 'kimi'
81
91
  ? 'Mapped into the Kimi native hooks bridge.'
82
- : 'Mapped into the OpenCode native hooks bridge.',
83
- packageId: asset.packageId,
84
- scope: asset.scope,
85
- instancePath: asset.instancePath,
86
- origin: asset.origin,
87
- resolvedBy: asset.resolvedBy,
88
- taskOverlaySource: asset.taskOverlaySource
92
+ : 'Mapped into the OpenCode native hooks bridge.'
89
93
  })
90
94
  })
91
95
 
92
- const selectedSkillAssets = resolveSelectedSkillAssets(params.bundle.skills, params.options.skills)
96
+ const selectedSkillAssets = await resolveSelectedSkillAssetsWithDependencies(params.bundle, params.options.skills)
93
97
  if (supportsNativeProjectSkills(params.adapter)) {
94
98
  selectedSkillAssets.forEach((asset) => {
95
- diagnostics.push({
96
- assetId: asset.id,
99
+ pushDiagnostic(asset, {
97
100
  adapter: params.adapter,
98
101
  status: 'native',
99
- reason: resolveNativeSkillDiagnosticReason(params.adapter),
100
- packageId: asset.packageId,
101
- scope: asset.scope,
102
- instancePath: asset.instancePath,
103
- origin: asset.origin,
104
- resolvedBy: asset.resolvedBy,
105
- taskOverlaySource: asset.taskOverlaySource
102
+ reason: resolveNativeSkillDiagnosticReason(params.adapter)
106
103
  })
107
104
  })
108
105
  }
109
106
  if (params.adapter === 'opencode') {
110
107
  params.bundle.opencodeOverlayAssets.forEach((asset) => {
111
- diagnostics.push({
112
- assetId: asset.id,
108
+ pushDiagnostic(asset, {
113
109
  adapter: params.adapter,
114
110
  status: 'native',
115
- reason: 'Mirrored into OPENCODE_CONFIG_DIR as a native OpenCode asset.',
116
- packageId: asset.packageId,
117
- scope: asset.scope,
118
- instancePath: asset.instancePath,
119
- origin: asset.origin,
120
- resolvedBy: asset.resolvedBy,
121
- taskOverlaySource: asset.taskOverlaySource
111
+ reason: 'Mirrored into OPENCODE_CONFIG_DIR as a native OpenCode asset.'
122
112
  })
123
113
  })
124
114
  } else if (params.adapter === 'codex' || params.adapter === 'copilot' || params.adapter === 'kimi') {
125
115
  params.bundle.opencodeOverlayAssets.forEach((asset) => {
126
- diagnostics.push({
127
- assetId: asset.id,
116
+ pushDiagnostic(asset, {
128
117
  adapter: params.adapter,
129
118
  status: 'skipped',
130
119
  reason: params.adapter === 'codex'
131
120
  ? 'No stable native Codex mapping exists for this asset kind in V1.'
132
121
  : params.adapter === 'copilot'
133
122
  ? 'No stable native Copilot mapping exists for this asset kind in V1.'
134
- : 'No stable native Kimi mapping exists for this asset kind in V1.',
135
- packageId: asset.packageId,
136
- scope: asset.scope,
137
- instancePath: asset.instancePath,
138
- origin: asset.origin,
139
- resolvedBy: asset.resolvedBy,
140
- taskOverlaySource: asset.taskOverlaySource
123
+ : 'No stable native Kimi mapping exists for this asset kind in V1.'
141
124
  })
142
125
  })
143
126
  } else if (params.adapter === 'gemini') {
144
127
  params.bundle.opencodeOverlayAssets.forEach((asset) => {
145
- diagnostics.push({
146
- assetId: asset.id,
128
+ pushDiagnostic(asset, {
147
129
  adapter: params.adapter,
148
130
  status: 'skipped',
149
- reason: 'No stable native Gemini mapping exists for this asset kind in V1.',
150
- packageId: asset.packageId,
151
- scope: asset.scope,
152
- instancePath: asset.instancePath,
153
- origin: asset.origin,
154
- resolvedBy: asset.resolvedBy,
155
- taskOverlaySource: asset.taskOverlaySource
131
+ reason: 'No stable native Gemini mapping exists for this asset kind in V1.'
156
132
  })
157
133
  })
158
134
  }
@@ -0,0 +1,13 @@
1
+ import type { DefinitionSource, WorkspaceAsset } from '@vibe-forge/types'
2
+
3
+ import { HOME_BRIDGE_RESOLVED_BY } from './home-bridge'
4
+
5
+ export const resolveWorkspaceAssetSource = (
6
+ asset: Pick<WorkspaceAsset, 'origin' | 'resolvedBy'>
7
+ ): DefinitionSource => (
8
+ asset.resolvedBy === HOME_BRIDGE_RESOLVED_BY
9
+ ? 'home'
10
+ : asset.origin === 'plugin'
11
+ ? 'plugin'
12
+ : 'project'
13
+ )