total-recall-brain 3.28.2 → 3.30.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.
- package/bin/total-recall.mjs +24 -1
- package/docs/ARCHITECTURE.md +26 -18
- package/extension/background.js +259 -84
- package/extension/content-script.css +7 -13
- package/extension/content-script.js +252 -477
- package/extension/icons/icon-128.png +0 -0
- package/extension/icons/icon-16.png +0 -0
- package/extension/icons/icon-32.png +0 -0
- package/extension/icons/icon-48.png +0 -0
- package/extension/icons/mark.svg +6 -0
- package/extension/lib/brain-client.js +160 -73
- package/extension/lib/tokens.css +187 -0
- package/extension/lib/ui-helpers.js +229 -0
- package/extension/lib/ui-helpers.spec.mjs +94 -0
- package/extension/manifest.json +20 -5
- package/extension/options/options.css +64 -218
- package/extension/options/options.html +91 -43
- package/extension/options/options.js +208 -85
- package/extension/sidepanel/sidepanel.css +268 -970
- package/extension/sidepanel/sidepanel.html +200 -166
- package/extension/sidepanel/sidepanel.js +788 -942
- package/frontend/dist/assets/index-DGMNZi_1.js +5742 -0
- package/frontend/dist/index.html +1 -1
- package/frontend/src/api/plugins.ts +124 -92
- package/frontend/src/components/ResearchAgendaTab.spec.tsx +11 -0
- package/frontend/src/components/ResearchAgendaTab.tsx +19 -14
- package/frontend/src/pages/PluginsPage.spec.tsx +87 -16
- package/frontend/src/pages/PluginsPage.tsx +454 -1218
- package/package.json +2 -1
- package/plugins/code-quality/README.md +25 -0
- package/plugins/code-quality/cli.mjs +25 -0
- package/plugins/code-quality/plugin.json +17 -0
- package/plugins/git-sentinel/README.md +27 -0
- package/{scaffold/.agent/plugins → plugins}/git-sentinel/cli.mjs +8 -5
- package/plugins/git-sentinel/plugin.json +18 -0
- package/plugins/system-monitor/README.md +26 -0
- package/{scaffold/.agent/plugins → plugins}/system-monitor/cli.mjs +5 -4
- package/{scaffold/.agent/plugins → plugins}/system-monitor/generator.mjs +4 -5
- package/plugins/system-monitor/plugin.json +29 -0
- package/scaffold/.agent/skills/meta-harness/SKILL.md +4 -4
- package/scaffold/.agent/skills/plugins/SKILL.md +53 -0
- package/scaffold/.agent/skills/total-recall/scripts/brain-state.json +1 -0
- package/src/cli/agent.mjs +1 -1
- package/src/cli/app/index.mjs +493 -0
- package/src/cli/brain.mjs +33 -3
- package/src/cli/command.mjs +80 -41
- package/src/cli/harness.mjs +1 -1
- package/src/cli/init.mjs +3 -1
- package/src/cli/mesh.mjs +118 -12
- package/src/cli/plugin/create.mjs +144 -25
- package/src/cli/plugin/index.mjs +83 -53
- package/src/cli/plugin/install.mjs +10 -127
- package/src/cli/plugin/list.mjs +44 -44
- package/src/cli/plugin/remove.mjs +23 -30
- package/src/cli/plugin/scaffold-cmd.mjs +30 -0
- package/src/cli/plugin/search.mjs +108 -23
- package/src/cli/rebuild.mjs +37 -1
- package/src/cli/recall.mjs +17 -2
- package/src/cli/research.mjs +4 -6
- package/src/cli/share.mjs +2 -0
- package/src/core/app-deploy/apply.mjs +386 -0
- package/src/core/app-deploy/design-tokens.mjs +217 -0
- package/src/core/app-deploy/plan.mjs +249 -0
- package/src/core/app-deploy/resolve.mjs +295 -0
- package/src/core/app-deploy/source.mjs +337 -0
- package/src/core/app-deploy/standalone.mjs +248 -0
- package/src/core/app-deploy/upgrade.mjs +256 -0
- package/src/core/app-deploy/verify.mjs +213 -0
- package/src/core/bound-hosts.mjs +1 -1
- package/src/core/brain-registry.mjs +146 -0
- package/src/core/brain-state.json +1 -0
- package/src/core/daemon-loop.mjs +30 -26
- package/src/core/dream.mjs +5 -17
- package/src/core/fact-seeker.mjs +53 -368
- package/src/core/lan-discovery.mjs +3 -3
- package/src/core/mesh-access-sync.mjs +134 -0
- package/src/core/mesh-access.mjs +18 -3
- package/src/core/mesh-discover.mjs +173 -0
- package/src/core/mesh.mjs +131 -32
- package/src/core/optimizer.mjs +4 -41
- package/src/core/plugin-bundle.mjs +167 -0
- package/src/core/plugin-context.mjs +8 -13
- package/src/core/plugin-loader.mjs +272 -68
- package/src/core/plugin-peers.mjs +163 -0
- package/src/core/plugin-public.mjs +85 -0
- package/src/core/plugin-runner-child.mjs +24 -0
- package/src/core/plugin-runner.mjs +102 -0
- package/src/core/plugin-store.mjs +384 -0
- package/src/core/plugin-tasks.mjs +212 -0
- package/src/core/post-mortem.mjs +40 -9
- package/src/core/research-gate.mjs +182 -0
- package/src/core/research-queue.mjs +47 -7
- package/src/core/research-surface.mjs +211 -0
- package/src/core/research.mjs +25 -32
- package/src/core/scheduler.mjs +67 -153
- package/src/core/schema.mjs +129 -0
- package/src/core/search.mjs +12 -1
- package/src/core/secret-integration-research.mjs +12 -8
- package/src/core/secrets-store.mjs +32 -2
- package/src/core/session-watcher.mjs +6 -0
- package/src/core/surface.mjs +64 -3
- package/src/core/task-executors.mjs +8 -45
- package/src/core/vault-cache.mjs +70 -4
- package/src/server/api.mjs +21 -0
- package/src/server/rest.mjs +2 -0
- package/src/server/route-manifest.json +21 -5
- package/src/server/routes/collab.mjs +38 -11
- package/src/server/routes/extension.mjs +3 -2
- package/src/server/routes/memory.mjs +24 -2
- package/src/server/routes/mesh.mjs +7 -7
- package/src/server/routes/plugins-mesh.mjs +49 -0
- package/src/server/routes/plugins.mjs +91 -426
- package/src/server/routes/research.mjs +5 -2
- package/src/server/routes/share.mjs +2 -0
- package/src/server/tools.mjs +29 -2
- package/extension/icons/README.md +0 -27
- package/extension/lib/preconfigured.js +0 -5
- package/extension/popup/popup.css +0 -227
- package/extension/popup/popup.html +0 -50
- package/extension/popup/popup.js +0 -175
- package/frontend/dist/assets/index-U7o-qU99.js +0 -5742
- package/scaffold/.agent/plugins/.gitkeep +0 -0
- package/scaffold/.agent/plugins/code-quality/README.md +0 -16
- package/scaffold/.agent/plugins/code-quality/cli.mjs +0 -15
- package/scaffold/.agent/plugins/code-quality/plugin.json +0 -34
- package/scaffold/.agent/plugins/git-sentinel/README.md +0 -16
- package/scaffold/.agent/plugins/git-sentinel/plugin.json +0 -34
- package/scaffold/.agent/plugins/meta-harness/README.md +0 -16
- package/scaffold/.agent/plugins/meta-harness/cli.mjs +0 -34
- package/scaffold/.agent/plugins/meta-harness/plugin.json +0 -34
- package/scaffold/.agent/plugins/system-monitor/README.md +0 -16
- package/scaffold/.agent/plugins/system-monitor/plugin.json +0 -38
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability Deployment — Deployment Planner
|
|
3
|
+
*
|
|
4
|
+
* Implements Phase 2 of CAPABILITY_DEPLOYMENT_PLUGINS:
|
|
5
|
+
* - Deterministic, read-only plan computation
|
|
6
|
+
* - File diffing (create, modify, identical, conflict)
|
|
7
|
+
* - SSSS dry-run event envelopes
|
|
8
|
+
* - Access grant and resource declaration aggregation
|
|
9
|
+
* - Deterministic SHA-256 plan hash
|
|
10
|
+
* - Formatted JSON output for CLI (`app plan --json`)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import crypto from 'node:crypto';
|
|
16
|
+
import { resolveCapabilitySource } from './source.mjs';
|
|
17
|
+
import { resolveCapabilityGraph } from './resolve.mjs';
|
|
18
|
+
|
|
19
|
+
export const PROTECTED_APP_FILES = new Set([
|
|
20
|
+
'package.json',
|
|
21
|
+
'package-lock.json',
|
|
22
|
+
'pnpm-lock.yaml',
|
|
23
|
+
'yarn.lock',
|
|
24
|
+
'tsconfig.json',
|
|
25
|
+
'README.md',
|
|
26
|
+
'.gitignore',
|
|
27
|
+
'.env',
|
|
28
|
+
'.env.local',
|
|
29
|
+
'.env.production'
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Computes deterministic SHA-256 hash of any JSON-serializable object.
|
|
34
|
+
*/
|
|
35
|
+
export function computeDeterministicHash(obj) {
|
|
36
|
+
const canonical = JSON.stringify(obj, Object.keys(obj).sort());
|
|
37
|
+
return crypto.createHash('sha256').update(canonical).digest('hex');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Generates SSSS dry-run lifecycle event envelopes for capability installation.
|
|
42
|
+
*/
|
|
43
|
+
export function generateSsssEnvelopes(orderedPlugins, targetApp) {
|
|
44
|
+
const envelopes = [];
|
|
45
|
+
const timestamp = new Date().toISOString();
|
|
46
|
+
|
|
47
|
+
for (const plugin of orderedPlugins) {
|
|
48
|
+
// 1. Install start envelope
|
|
49
|
+
envelopes.push({
|
|
50
|
+
type: 'event',
|
|
51
|
+
domain: 'ssss.capability',
|
|
52
|
+
action: 'install_started',
|
|
53
|
+
timestamp,
|
|
54
|
+
actor: 'total-recall/app-deploy',
|
|
55
|
+
target: targetApp.dir,
|
|
56
|
+
payload: {
|
|
57
|
+
capability_id: plugin.id,
|
|
58
|
+
version: plugin.version,
|
|
59
|
+
source_sha256: plugin.sha256
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// 2. Schema registration envelopes if categories are present
|
|
64
|
+
const categories = plugin.manifest.ssss_schemas?.categories || [];
|
|
65
|
+
if (categories.length > 0) {
|
|
66
|
+
envelopes.push({
|
|
67
|
+
type: 'event',
|
|
68
|
+
domain: 'ssss.schema',
|
|
69
|
+
action: 'categories_registered',
|
|
70
|
+
timestamp,
|
|
71
|
+
actor: 'total-recall/app-deploy',
|
|
72
|
+
target: targetApp.dir,
|
|
73
|
+
payload: {
|
|
74
|
+
capability_id: plugin.id,
|
|
75
|
+
categories: categories.map((c) => ({
|
|
76
|
+
name: c.name,
|
|
77
|
+
description: c.description || '',
|
|
78
|
+
node_type: c.node_type || 'memory'
|
|
79
|
+
}))
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 3. Capability installed envelope
|
|
85
|
+
envelopes.push({
|
|
86
|
+
type: 'event',
|
|
87
|
+
domain: 'ssss.capability',
|
|
88
|
+
action: 'installed',
|
|
89
|
+
timestamp,
|
|
90
|
+
actor: 'total-recall/app-deploy',
|
|
91
|
+
target: targetApp.dir,
|
|
92
|
+
payload: {
|
|
93
|
+
capability_id: plugin.id,
|
|
94
|
+
version: plugin.version,
|
|
95
|
+
status: 'ready'
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return envelopes;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Diffs files provided by capability plugins against the target application tree.
|
|
105
|
+
*/
|
|
106
|
+
export function computeFileDiffs(orderedPlugins, targetAppDir) {
|
|
107
|
+
const fileOperations = [];
|
|
108
|
+
|
|
109
|
+
for (const plugin of orderedPlugins) {
|
|
110
|
+
for (const file of plugin.files || []) {
|
|
111
|
+
if (file.path === 'plugin.json') continue; // Manifest stays with capability record
|
|
112
|
+
|
|
113
|
+
const targetPath = path.join(targetAppDir, file.path);
|
|
114
|
+
let action = 'create';
|
|
115
|
+
let existingHash = null;
|
|
116
|
+
|
|
117
|
+
if (fs.existsSync(targetPath)) {
|
|
118
|
+
try {
|
|
119
|
+
const targetBytes = fs.readFileSync(targetPath);
|
|
120
|
+
existingHash = crypto.createHash('sha256').update(targetBytes).digest('hex');
|
|
121
|
+
|
|
122
|
+
if (existingHash === file.sha256) {
|
|
123
|
+
action = 'identical';
|
|
124
|
+
} else if (PROTECTED_APP_FILES.has(file.path) || PROTECTED_APP_FILES.has(path.basename(file.path))) {
|
|
125
|
+
action = 'conflict';
|
|
126
|
+
} else {
|
|
127
|
+
action = 'modify';
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
action = 'modify';
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
fileOperations.push({
|
|
135
|
+
action,
|
|
136
|
+
path: file.path,
|
|
137
|
+
plugin_id: plugin.id,
|
|
138
|
+
size: file.size,
|
|
139
|
+
source_sha256: file.sha256,
|
|
140
|
+
target_sha256: existingHash
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
fileOperations.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
146
|
+
return fileOperations;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Generates a full, deterministic capability deployment plan.
|
|
151
|
+
*
|
|
152
|
+
* @param {string|object} source - Capability source (URI, path, or bundle)
|
|
153
|
+
* @param {object} [options]
|
|
154
|
+
* @param {string} [options.target] - Target app directory path
|
|
155
|
+
* @param {string} [options.adapter] - Target adapter (ssss-app, nextjs, react, flask)
|
|
156
|
+
* @param {string} [options.expectedHash] - Pinned hash for source
|
|
157
|
+
* @param {Function} [options.pluginResolver] - Resolver for transitive dependencies
|
|
158
|
+
* @returns {Promise<{
|
|
159
|
+
* plan_version: string,
|
|
160
|
+
* plan_hash: string,
|
|
161
|
+
* valid: boolean,
|
|
162
|
+
* target_app: object,
|
|
163
|
+
* adapter: string,
|
|
164
|
+
* capabilities: Array<object>,
|
|
165
|
+
* file_operations: Array<object>,
|
|
166
|
+
* ssss_envelopes: Array<object>,
|
|
167
|
+
* access_grants: Array<string>,
|
|
168
|
+
* resources: object,
|
|
169
|
+
* conflicts: Array<object>,
|
|
170
|
+
* cleanup: () => void
|
|
171
|
+
* }>}
|
|
172
|
+
*/
|
|
173
|
+
export async function createDeploymentPlan(source, options = {}) {
|
|
174
|
+
const targetDir = path.resolve(options.target || process.cwd());
|
|
175
|
+
const adapter = options.adapter || 'ssss-app';
|
|
176
|
+
|
|
177
|
+
// 1. Resolve and verify root source
|
|
178
|
+
const rootSource = await resolveCapabilitySource(source, {
|
|
179
|
+
expectedHash: options.expectedHash,
|
|
180
|
+
projectRoot: targetDir
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
// 2. Resolve dependency graph and check compatibility
|
|
185
|
+
const resolvedGraph = await resolveCapabilityGraph(rootSource, {
|
|
186
|
+
adapter,
|
|
187
|
+
targetApp: {
|
|
188
|
+
dir: targetDir,
|
|
189
|
+
framework: adapter,
|
|
190
|
+
ssssVersion: '0.9.6'
|
|
191
|
+
},
|
|
192
|
+
pluginResolver: options.pluginResolver
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// 3. Compute file diffs against target directory
|
|
196
|
+
const fileOperations = computeFileDiffs(resolvedGraph.orderedPlugins, targetDir);
|
|
197
|
+
|
|
198
|
+
// 4. Generate SSSS dry-run envelopes
|
|
199
|
+
const ssssEnvelopes = generateSsssEnvelopes(resolvedGraph.orderedPlugins, resolvedGraph.targetApp);
|
|
200
|
+
|
|
201
|
+
// 5. Detect any file conflicts
|
|
202
|
+
const conflicts = fileOperations.filter((op) => op.action === 'conflict');
|
|
203
|
+
|
|
204
|
+
const capabilitiesSummary = resolvedGraph.orderedPlugins.map((p) => ({
|
|
205
|
+
id: p.id,
|
|
206
|
+
version: p.version,
|
|
207
|
+
source_sha256: p.sha256,
|
|
208
|
+
skills: (p.manifest.skills || []).map((s) => s.id),
|
|
209
|
+
commands: (p.manifest.commands || []).map((c) => c.name),
|
|
210
|
+
ui: p.manifest.ui ? Object.keys(p.manifest.ui) : []
|
|
211
|
+
}));
|
|
212
|
+
|
|
213
|
+
const planBody = {
|
|
214
|
+
plan_version: '1.0.0',
|
|
215
|
+
valid: conflicts.length === 0,
|
|
216
|
+
target_app: {
|
|
217
|
+
dir: targetDir,
|
|
218
|
+
framework: adapter,
|
|
219
|
+
ssss_version: resolvedGraph.targetApp.ssssVersion
|
|
220
|
+
},
|
|
221
|
+
adapter,
|
|
222
|
+
capabilities: capabilitiesSummary,
|
|
223
|
+
file_operations: fileOperations,
|
|
224
|
+
ssss_envelopes: ssssEnvelopes,
|
|
225
|
+
access_grants: resolvedGraph.totalGrants,
|
|
226
|
+
resources: resolvedGraph.totalResources,
|
|
227
|
+
conflicts
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const planHash = computeDeterministicHash(planBody);
|
|
231
|
+
|
|
232
|
+
const stagedSources = {};
|
|
233
|
+
for (const p of resolvedGraph.orderedPlugins) {
|
|
234
|
+
if (p.stagedDir) {
|
|
235
|
+
stagedSources[p.id] = p.stagedDir;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
...planBody,
|
|
241
|
+
plan_hash: planHash,
|
|
242
|
+
_stagedSources: stagedSources,
|
|
243
|
+
cleanup: rootSource.cleanup
|
|
244
|
+
};
|
|
245
|
+
} catch (err) {
|
|
246
|
+
rootSource.cleanup();
|
|
247
|
+
throw err;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability Deployment — Dependency DAG, Version Constraints & Collision Resolver
|
|
3
|
+
*
|
|
4
|
+
* Implements Phase 2 of CAPABILITY_DEPLOYMENT_PLUGINS:
|
|
5
|
+
* - Target and adapter compatibility checks
|
|
6
|
+
* - SSSS specification version constraints
|
|
7
|
+
* - Dependency DAG construction and topological ordering
|
|
8
|
+
* - Cycle detection and rejection
|
|
9
|
+
* - File path, command, and SSSS category ownership collision detection
|
|
10
|
+
* - Access grant and resource requirement aggregation
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Custom error types for capability resolution
|
|
17
|
+
*/
|
|
18
|
+
export class IncompatibleAdapterError extends Error {
|
|
19
|
+
constructor(message) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = 'IncompatibleAdapterError';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class IncompatibleSsssVersionError extends Error {
|
|
26
|
+
constructor(message) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = 'IncompatibleSsssVersionError';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class DependencyCycleError extends Error {
|
|
33
|
+
constructor(message, cycle = []) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = 'DependencyCycleError';
|
|
36
|
+
this.cycle = cycle;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class OwnershipCollisionError extends Error {
|
|
41
|
+
constructor(message, collisionDetails = {}) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = 'OwnershipCollisionError';
|
|
44
|
+
this.details = collisionDetails;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Lightweight semver comparator to evaluate version constraints.
|
|
50
|
+
* Supports basic operators: >=, <=, >, <, ^, ~, or exact versions.
|
|
51
|
+
*/
|
|
52
|
+
export function satisfiesVersionConstraint(version, range) {
|
|
53
|
+
if (!range || range === '*' || range === 'latest') return true;
|
|
54
|
+
if (!version) return false;
|
|
55
|
+
|
|
56
|
+
const cleanV = version.replace(/^v/, '').trim();
|
|
57
|
+
const vParts = cleanV.split('.').map(Number);
|
|
58
|
+
const major = vParts[0] || 0;
|
|
59
|
+
const minor = vParts[1] || 0;
|
|
60
|
+
const patch = vParts[2] || 0;
|
|
61
|
+
|
|
62
|
+
const cleanRange = range.trim();
|
|
63
|
+
|
|
64
|
+
// Range with >= operator: e.g. ">=0.9.3"
|
|
65
|
+
if (cleanRange.startsWith('>=')) {
|
|
66
|
+
const target = cleanRange.slice(2).trim().split('.').map(Number);
|
|
67
|
+
if (major > (target[0] || 0)) return true;
|
|
68
|
+
if (major < (target[0] || 0)) return false;
|
|
69
|
+
if (minor > (target[1] || 0)) return true;
|
|
70
|
+
if (minor < (target[1] || 0)) return false;
|
|
71
|
+
return patch >= (target[2] || 0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Caret range: e.g. "^0.9.0"
|
|
75
|
+
if (cleanRange.startsWith('^')) {
|
|
76
|
+
const target = cleanRange.slice(1).trim().split('.').map(Number);
|
|
77
|
+
if (major !== (target[0] || 0)) return false;
|
|
78
|
+
if (minor < (target[1] || 0)) return false;
|
|
79
|
+
if (minor === (target[1] || 0)) return patch >= (target[2] || 0);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Exact match: e.g. "0.9.6"
|
|
84
|
+
return cleanV === cleanRange.replace(/^v/, '');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Validates adapter compatibility between capability plugin and target app.
|
|
89
|
+
*/
|
|
90
|
+
export function checkAdapterCompatibility(manifest, adapter) {
|
|
91
|
+
if (!adapter) return;
|
|
92
|
+
const targets = manifest.deploy?.targets;
|
|
93
|
+
if (!targets || !Array.isArray(targets) || targets.length === 0) {
|
|
94
|
+
// If no targets specified, defaults to universal capability
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const isCompatible = targets.some((t) => t.toLowerCase() === adapter.toLowerCase() || t === '*');
|
|
99
|
+
if (!isCompatible) {
|
|
100
|
+
throw new IncompatibleAdapterError(
|
|
101
|
+
`Plugin '${manifest.id}' is not compatible with target adapter '${adapter}'. Supported targets: ${targets.join(', ')}`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Validates SSSS version requirement against target app's SSSS version.
|
|
108
|
+
*/
|
|
109
|
+
export function checkSsssVersionCompatibility(manifest, targetSsssVersion) {
|
|
110
|
+
const req = manifest.deploy?.required_ssss_version;
|
|
111
|
+
if (!req) return;
|
|
112
|
+
|
|
113
|
+
const currentVersion = targetSsssVersion || '0.9.6';
|
|
114
|
+
if (!satisfiesVersionConstraint(currentVersion, req)) {
|
|
115
|
+
throw new IncompatibleSsssVersionError(
|
|
116
|
+
`Plugin '${manifest.id}' requires SSSS version ${req}, but target app environment provides ${currentVersion}`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Resolves full dependency DAG, detects cycles, and sorts topologically.
|
|
123
|
+
*
|
|
124
|
+
* @param {object} rootDescriptor - Root capability source descriptor from resolveCapabilitySource
|
|
125
|
+
* @param {object} [options]
|
|
126
|
+
* @param {string} [options.adapter] - Target adapter (e.g. ssss-app, nextjs, react, flask)
|
|
127
|
+
* @param {object} [options.targetApp] - Target app info { dir, framework, ssssVersion }
|
|
128
|
+
* @param {Function} [options.pluginResolver] - Async resolver for transitive dependencies: (id) => Promise<descriptor>
|
|
129
|
+
* @returns {Promise<{
|
|
130
|
+
* adapter: string,
|
|
131
|
+
* targetApp: object,
|
|
132
|
+
* orderedPlugins: Array<object>,
|
|
133
|
+
* totalGrants: Array<string>,
|
|
134
|
+
* totalResources: object,
|
|
135
|
+
* fileOwners: Map<string, string>,
|
|
136
|
+
* categoryOwners: Map<string, string>
|
|
137
|
+
* }>}
|
|
138
|
+
*/
|
|
139
|
+
export async function resolveCapabilityGraph(rootDescriptor, options = {}) {
|
|
140
|
+
const adapter = options.adapter || options.targetApp?.framework || 'ssss-app';
|
|
141
|
+
const targetSsssVersion = options.targetApp?.ssssVersion || '0.9.6';
|
|
142
|
+
const pluginResolver = options.pluginResolver || (async () => null);
|
|
143
|
+
|
|
144
|
+
const graph = new Map(); // id -> descriptor
|
|
145
|
+
const visited = new Map(); // id -> 'unvisited' | 'visiting' | 'visited'
|
|
146
|
+
const ordered = []; // Topological order (dependencies first)
|
|
147
|
+
|
|
148
|
+
// 1. Traverse and discover all transitive dependencies
|
|
149
|
+
async function traverse(currentDescriptor, pathTrace = []) {
|
|
150
|
+
const id = currentDescriptor.id;
|
|
151
|
+
|
|
152
|
+
if (pathTrace.includes(id)) {
|
|
153
|
+
const cycle = [...pathTrace, id];
|
|
154
|
+
throw new DependencyCycleError(`Dependency cycle detected: ${cycle.join(' -> ')}`, cycle);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (graph.has(id)) return;
|
|
158
|
+
graph.set(id, currentDescriptor);
|
|
159
|
+
|
|
160
|
+
// Validate adapter and SSSS constraints for each plugin in the graph
|
|
161
|
+
checkAdapterCompatibility(currentDescriptor.manifest, adapter);
|
|
162
|
+
checkSsssVersionCompatibility(currentDescriptor.manifest, targetSsssVersion);
|
|
163
|
+
|
|
164
|
+
const depsObj = currentDescriptor.manifest.dependencies || {};
|
|
165
|
+
const depIds = Array.isArray(depsObj) ? depsObj : Object.keys(depsObj);
|
|
166
|
+
|
|
167
|
+
for (const depId of depIds) {
|
|
168
|
+
const depDescriptor = await pluginResolver(depId);
|
|
169
|
+
if (!depDescriptor) {
|
|
170
|
+
throw new Error(`Unresolvable dependency '${depId}' required by plugin '${id}'`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const versionReq = typeof depsObj === 'object' && !Array.isArray(depsObj) ? depsObj[depId] : null;
|
|
174
|
+
if (versionReq && !satisfiesVersionConstraint(depDescriptor.version, versionReq)) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`Version mismatch for dependency '${depId}': '${id}' requires ${versionReq}, but resolved ${depDescriptor.version}`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
await traverse(depDescriptor, [...pathTrace, id]);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
await traverse(rootDescriptor);
|
|
185
|
+
|
|
186
|
+
// 2. Topological sort using DFS
|
|
187
|
+
for (const id of graph.keys()) {
|
|
188
|
+
visited.set(id, 'unvisited');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function dfsTopological(id, trace = []) {
|
|
192
|
+
const state = visited.get(id);
|
|
193
|
+
if (state === 'visiting') {
|
|
194
|
+
const cycle = [...trace, id];
|
|
195
|
+
throw new DependencyCycleError(`Dependency cycle detected: ${cycle.join(' -> ')}`, cycle);
|
|
196
|
+
}
|
|
197
|
+
if (state === 'visited') return;
|
|
198
|
+
|
|
199
|
+
visited.set(id, 'visiting');
|
|
200
|
+
const desc = graph.get(id);
|
|
201
|
+
const depsObj = desc.manifest.dependencies || {};
|
|
202
|
+
const depIds = Array.isArray(depsObj) ? depsObj : Object.keys(depsObj);
|
|
203
|
+
|
|
204
|
+
for (const depId of depIds) {
|
|
205
|
+
if (graph.has(depId)) {
|
|
206
|
+
dfsTopological(depId, [...trace, id]);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
visited.set(id, 'visited');
|
|
211
|
+
ordered.push(desc);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
for (const id of graph.keys()) {
|
|
215
|
+
if (visited.get(id) === 'unvisited') {
|
|
216
|
+
dfsTopological(id);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// 3. Collision Detection across the resolved graph
|
|
221
|
+
const fileOwners = new Map(); // relPath -> pluginId
|
|
222
|
+
const categoryOwners = new Map(); // categoryName -> pluginId
|
|
223
|
+
const commandOwners = new Map(); // commandName -> pluginId
|
|
224
|
+
const grantsSet = new Set();
|
|
225
|
+
const aggregatedResources = {};
|
|
226
|
+
|
|
227
|
+
for (const plugin of ordered) {
|
|
228
|
+
const id = plugin.id;
|
|
229
|
+
const manifest = plugin.manifest;
|
|
230
|
+
|
|
231
|
+
// Check files
|
|
232
|
+
for (const file of plugin.files || []) {
|
|
233
|
+
if (file.path === 'plugin.json') continue; // plugin.json is per-plugin
|
|
234
|
+
if (fileOwners.has(file.path)) {
|
|
235
|
+
const priorOwner = fileOwners.get(file.path);
|
|
236
|
+
throw new OwnershipCollisionError(
|
|
237
|
+
`File collision detected: '${file.path}' is owned by both '${priorOwner}' and '${id}'`,
|
|
238
|
+
{ type: 'file', path: file.path, owners: [priorOwner, id] }
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
fileOwners.set(file.path, id);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Check SSSS categories
|
|
245
|
+
const categories = manifest.ssss_schemas?.categories || [];
|
|
246
|
+
for (const cat of categories) {
|
|
247
|
+
const catName = cat.name;
|
|
248
|
+
if (categoryOwners.has(catName)) {
|
|
249
|
+
const priorOwner = categoryOwners.get(catName);
|
|
250
|
+
throw new OwnershipCollisionError(
|
|
251
|
+
`SSSS schema category collision: category '${catName}' is declared by both '${priorOwner}' and '${id}'`,
|
|
252
|
+
{ type: 'category', category: catName, owners: [priorOwner, id] }
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
categoryOwners.set(catName, id);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Check CLI commands
|
|
259
|
+
const cliCmd = manifest.cli?.command;
|
|
260
|
+
if (cliCmd) {
|
|
261
|
+
if (commandOwners.has(cliCmd)) {
|
|
262
|
+
const priorOwner = commandOwners.get(cliCmd);
|
|
263
|
+
throw new OwnershipCollisionError(
|
|
264
|
+
`CLI command collision: command '${cliCmd}' is claimed by both '${priorOwner}' and '${id}'`,
|
|
265
|
+
{ type: 'command', command: cliCmd, owners: [priorOwner, id] }
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
commandOwners.set(cliCmd, id);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Accumulate access grants
|
|
272
|
+
for (const grant of (manifest.deploy?.access_grants || manifest.deploy?.grants || [])) {
|
|
273
|
+
grantsSet.add(grant);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Merge resources
|
|
277
|
+
if (manifest.deploy?.resources) {
|
|
278
|
+
Object.assign(aggregatedResources, manifest.deploy.resources);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
adapter,
|
|
284
|
+
targetApp: {
|
|
285
|
+
dir: options.targetApp?.dir || process.cwd(),
|
|
286
|
+
framework: adapter,
|
|
287
|
+
ssssVersion: targetSsssVersion
|
|
288
|
+
},
|
|
289
|
+
orderedPlugins: ordered,
|
|
290
|
+
totalGrants: Array.from(grantsSet).sort(),
|
|
291
|
+
totalResources: aggregatedResources,
|
|
292
|
+
fileOwners,
|
|
293
|
+
categoryOwners
|
|
294
|
+
};
|
|
295
|
+
}
|