Skip to content

Commit cd93f81

Browse files
Copilothotlong
andcommitted
feat: add dependency resolution protocol and namespace collision detection
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent e76ee94 commit cd93f81

5 files changed

Lines changed: 520 additions & 0 deletions

File tree

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
import { describe, it, expect } from 'vitest';
2+
import {
3+
DependencyStatusEnum,
4+
ResolvedDependencySchema,
5+
RequiredActionSchema,
6+
DependencyResolutionResultSchema,
7+
} from './dependency-resolution.zod';
8+
9+
describe('DependencyStatusEnum', () => {
10+
it('should accept all valid statuses', () => {
11+
const statuses = ['satisfied', 'needs_install', 'needs_upgrade', 'conflict'];
12+
statuses.forEach(status => {
13+
expect(() => DependencyStatusEnum.parse(status)).not.toThrow();
14+
});
15+
});
16+
17+
it('should reject invalid statuses', () => {
18+
expect(() => DependencyStatusEnum.parse('unknown')).toThrow();
19+
expect(() => DependencyStatusEnum.parse('')).toThrow();
20+
});
21+
});
22+
23+
describe('ResolvedDependencySchema', () => {
24+
it('should accept satisfied dependency', () => {
25+
const dep = {
26+
packageId: 'com.acme.auth',
27+
requiredRange: '^2.0.0',
28+
resolvedVersion: '2.1.0',
29+
installedVersion: '2.1.0',
30+
status: 'satisfied' as const,
31+
};
32+
const parsed = ResolvedDependencySchema.parse(dep);
33+
expect(parsed.status).toBe('satisfied');
34+
expect(parsed.resolvedVersion).toBe('2.1.0');
35+
});
36+
37+
it('should accept needs_install dependency', () => {
38+
const dep = {
39+
packageId: 'com.acme.storage',
40+
requiredRange: '>=1.0.0',
41+
resolvedVersion: '1.5.0',
42+
status: 'needs_install' as const,
43+
};
44+
const parsed = ResolvedDependencySchema.parse(dep);
45+
expect(parsed.status).toBe('needs_install');
46+
expect(parsed.installedVersion).toBeUndefined();
47+
});
48+
49+
it('should accept needs_upgrade dependency', () => {
50+
const dep = {
51+
packageId: 'com.acme.core',
52+
requiredRange: '^3.0.0',
53+
resolvedVersion: '3.0.0',
54+
installedVersion: '2.5.0',
55+
status: 'needs_upgrade' as const,
56+
};
57+
const parsed = ResolvedDependencySchema.parse(dep);
58+
expect(parsed.status).toBe('needs_upgrade');
59+
expect(parsed.installedVersion).toBe('2.5.0');
60+
});
61+
62+
it('should accept conflict dependency with reason', () => {
63+
const dep = {
64+
packageId: 'com.acme.utils',
65+
requiredRange: '^2.0.0',
66+
installedVersion: '1.3.0',
67+
status: 'conflict' as const,
68+
conflictReason: 'Package com.acme.crm requires com.acme.utils@^1.0.0',
69+
};
70+
const parsed = ResolvedDependencySchema.parse(dep);
71+
expect(parsed.status).toBe('conflict');
72+
expect(parsed.conflictReason).toContain('com.acme.crm');
73+
});
74+
75+
it('should reject missing required fields', () => {
76+
expect(() => ResolvedDependencySchema.parse({})).toThrow();
77+
expect(() => ResolvedDependencySchema.parse({ packageId: 'test' })).toThrow();
78+
});
79+
});
80+
81+
describe('RequiredActionSchema', () => {
82+
it('should accept install action', () => {
83+
const action = {
84+
type: 'install' as const,
85+
packageId: 'com.acme.auth',
86+
description: 'Install com.acme.auth@2.1.0 (required by com.acme.crm)',
87+
};
88+
const parsed = RequiredActionSchema.parse(action);
89+
expect(parsed.type).toBe('install');
90+
});
91+
92+
it('should accept upgrade action', () => {
93+
const action = {
94+
type: 'upgrade' as const,
95+
packageId: 'com.acme.core',
96+
description: 'Upgrade com.acme.core from 2.5.0 to 3.0.0',
97+
};
98+
const parsed = RequiredActionSchema.parse(action);
99+
expect(parsed.type).toBe('upgrade');
100+
});
101+
102+
it('should accept confirm_conflict action', () => {
103+
const action = {
104+
type: 'confirm_conflict' as const,
105+
packageId: 'com.acme.utils',
106+
description: 'Version conflict: com.acme.crm needs ^1.0.0 but com.acme.hr needs ^2.0.0',
107+
};
108+
expect(() => RequiredActionSchema.parse(action)).not.toThrow();
109+
});
110+
});
111+
112+
describe('DependencyResolutionResultSchema', () => {
113+
it('should accept resolution with all dependencies satisfied', () => {
114+
const result = {
115+
dependencies: [
116+
{
117+
packageId: 'com.acme.auth',
118+
requiredRange: '^2.0.0',
119+
resolvedVersion: '2.1.0',
120+
installedVersion: '2.1.0',
121+
status: 'satisfied' as const,
122+
},
123+
],
124+
canProceed: true,
125+
requiredActions: [],
126+
installOrder: ['com.acme.crm'],
127+
};
128+
const parsed = DependencyResolutionResultSchema.parse(result);
129+
expect(parsed.canProceed).toBe(true);
130+
expect(parsed.requiredActions).toHaveLength(0);
131+
});
132+
133+
it('should accept resolution with required actions', () => {
134+
const result = {
135+
dependencies: [
136+
{
137+
packageId: 'com.acme.auth',
138+
requiredRange: '^2.0.0',
139+
resolvedVersion: '2.1.0',
140+
status: 'needs_install' as const,
141+
},
142+
{
143+
packageId: 'com.acme.core',
144+
requiredRange: '^3.0.0',
145+
resolvedVersion: '3.0.0',
146+
installedVersion: '2.5.0',
147+
status: 'needs_upgrade' as const,
148+
},
149+
],
150+
canProceed: true,
151+
requiredActions: [
152+
{
153+
type: 'install' as const,
154+
packageId: 'com.acme.auth',
155+
description: 'Install com.acme.auth@2.1.0',
156+
},
157+
{
158+
type: 'upgrade' as const,
159+
packageId: 'com.acme.core',
160+
description: 'Upgrade com.acme.core from 2.5.0 to 3.0.0',
161+
},
162+
],
163+
installOrder: ['com.acme.core', 'com.acme.auth', 'com.acme.crm'],
164+
};
165+
const parsed = DependencyResolutionResultSchema.parse(result);
166+
expect(parsed.canProceed).toBe(true);
167+
expect(parsed.requiredActions).toHaveLength(2);
168+
expect(parsed.installOrder).toHaveLength(3);
169+
});
170+
171+
it('should accept resolution with conflicts', () => {
172+
const result = {
173+
dependencies: [
174+
{
175+
packageId: 'com.acme.utils',
176+
requiredRange: '^2.0.0',
177+
installedVersion: '1.3.0',
178+
status: 'conflict' as const,
179+
conflictReason: 'com.acme.crm requires ^1.0.0',
180+
},
181+
],
182+
canProceed: false,
183+
requiredActions: [
184+
{
185+
type: 'confirm_conflict' as const,
186+
packageId: 'com.acme.utils',
187+
description: 'Resolve version conflict for com.acme.utils',
188+
},
189+
],
190+
installOrder: [],
191+
};
192+
const parsed = DependencyResolutionResultSchema.parse(result);
193+
expect(parsed.canProceed).toBe(false);
194+
});
195+
196+
it('should accept resolution with circular dependencies', () => {
197+
const result = {
198+
dependencies: [],
199+
canProceed: false,
200+
requiredActions: [],
201+
installOrder: [],
202+
circularDependencies: [
203+
['com.acme.a', 'com.acme.b', 'com.acme.a'],
204+
],
205+
};
206+
const parsed = DependencyResolutionResultSchema.parse(result);
207+
expect(parsed.circularDependencies).toHaveLength(1);
208+
expect(parsed.circularDependencies![0]).toHaveLength(3);
209+
});
210+
211+
it('should reject missing required fields', () => {
212+
expect(() => DependencyResolutionResultSchema.parse({})).toThrow();
213+
});
214+
});
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { z } from 'zod';
4+
5+
/**
6+
* # Dependency Resolution Protocol
7+
*
8+
* Defines schemas for runtime dependency resolution when installing,
9+
* upgrading, or managing packages. Provides a standardized way to
10+
* express dependency conflicts, resolution results, and installation order.
11+
*
12+
* ## Architecture Alignment
13+
* - **npm**: Dependency tree resolution with conflict detection
14+
* - **Helm**: Dependency management with version constraints
15+
* - **Salesforce**: Package dependency validation at install time
16+
*
17+
* ## Resolution Flow
18+
* ```
19+
* 1. Parse manifest.dependencies (SemVer ranges)
20+
* 2. Check installed packages registry
21+
* 3. Resolve each dependency → satisfied | needs_install | needs_upgrade | conflict
22+
* 4. Detect circular dependencies
23+
* 5. Compute topological install order
24+
* 6. Return resolution result with required actions
25+
* ```
26+
*/
27+
28+
// ==========================================
29+
// Dependency Resolution Status
30+
// ==========================================
31+
32+
/**
33+
* Resolution status for a single dependency.
34+
*/
35+
export const DependencyStatusEnum = z.enum([
36+
'satisfied', // Already installed and version compatible
37+
'needs_install', // Not installed, needs to be installed
38+
'needs_upgrade', // Installed but version incompatible, needs upgrade
39+
'conflict', // Conflicts with another package's dependency
40+
]).describe('Resolution status for a dependency');
41+
42+
export type DependencyStatus = z.infer<typeof DependencyStatusEnum>;
43+
44+
// ==========================================
45+
// Resolved Dependency
46+
// ==========================================
47+
48+
/**
49+
* Single dependency resolution result.
50+
* Describes the state of one dependency after resolution.
51+
*/
52+
export const ResolvedDependencySchema = z.object({
53+
/** Package identifier of the dependency */
54+
packageId: z.string().describe('Dependency package identifier'),
55+
56+
/** SemVer range required by the parent package */
57+
requiredRange: z.string().describe('SemVer range required (e.g. "^2.0.0")'),
58+
59+
/** Actual version resolved (if available) */
60+
resolvedVersion: z.string().optional()
61+
.describe('Actual version resolved from registry'),
62+
63+
/** Currently installed version (if any) */
64+
installedVersion: z.string().optional()
65+
.describe('Currently installed version'),
66+
67+
/** Resolution status */
68+
status: DependencyStatusEnum.describe('Resolution status'),
69+
70+
/** Conflict details (when status is "conflict") */
71+
conflictReason: z.string().optional()
72+
.describe('Explanation of the conflict'),
73+
}).describe('Resolution result for a single dependency');
74+
75+
export type ResolvedDependency = z.infer<typeof ResolvedDependencySchema>;
76+
77+
// ==========================================
78+
// Required Action
79+
// ==========================================
80+
81+
/**
82+
* An action required before installation can proceed.
83+
*/
84+
export const RequiredActionSchema = z.object({
85+
/** Type of action required */
86+
type: z.enum(['install', 'upgrade', 'confirm_conflict'])
87+
.describe('Type of action required'),
88+
89+
/** Target package identifier */
90+
packageId: z.string().describe('Target package identifier'),
91+
92+
/** Human-readable description of the action */
93+
description: z.string().describe('Human-readable action description'),
94+
}).describe('Action required before installation can proceed');
95+
96+
export type RequiredAction = z.infer<typeof RequiredActionSchema>;
97+
98+
// ==========================================
99+
// Dependency Resolution Result
100+
// ==========================================
101+
102+
/**
103+
* Complete dependency resolution result.
104+
* Aggregates all dependency statuses and computes installation feasibility.
105+
*/
106+
export const DependencyResolutionResultSchema = z.object({
107+
/** All dependencies and their resolution results */
108+
dependencies: z.array(ResolvedDependencySchema)
109+
.describe('Resolution result for each dependency'),
110+
111+
/** Whether installation can proceed without conflicts */
112+
canProceed: z.boolean()
113+
.describe('Whether installation can proceed'),
114+
115+
/** Actions that require user confirmation or system execution */
116+
requiredActions: z.array(RequiredActionSchema)
117+
.describe('Actions required before proceeding'),
118+
119+
/** Topologically sorted package IDs for installation order */
120+
installOrder: z.array(z.string())
121+
.describe('Topologically sorted package IDs for installation'),
122+
123+
/** Detected circular dependency chains */
124+
circularDependencies: z.array(z.array(z.string())).optional()
125+
.describe('Circular dependency chains detected (e.g. [["A", "B", "A"]])'),
126+
}).describe('Complete dependency resolution result');
127+
128+
export type DependencyResolutionResult = z.infer<typeof DependencyResolutionResultSchema>;

packages/spec/src/kernel/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
export * from './cli-extension.zod';
44
export * from './context.zod';
5+
export * from './dependency-resolution.zod';
56
export * from './dev-plugin.zod';
67
export * from './events.zod';
78
export * from './feature.zod';

0 commit comments

Comments
 (0)