total-recall-brain 3.28.2 → 3.30.1

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.
Files changed (134) hide show
  1. package/bin/total-recall.mjs +24 -1
  2. package/docs/ARCHITECTURE.md +26 -18
  3. package/extension/background.js +259 -84
  4. package/extension/content-script.css +7 -13
  5. package/extension/content-script.js +252 -477
  6. package/extension/icons/icon-128.png +0 -0
  7. package/extension/icons/icon-16.png +0 -0
  8. package/extension/icons/icon-32.png +0 -0
  9. package/extension/icons/icon-48.png +0 -0
  10. package/extension/icons/mark.svg +6 -0
  11. package/extension/lib/brain-client.js +160 -73
  12. package/extension/lib/tokens.css +187 -0
  13. package/extension/lib/ui-helpers.js +229 -0
  14. package/extension/lib/ui-helpers.spec.mjs +94 -0
  15. package/extension/manifest.json +20 -5
  16. package/extension/options/options.css +64 -218
  17. package/extension/options/options.html +91 -43
  18. package/extension/options/options.js +208 -85
  19. package/extension/sidepanel/sidepanel.css +268 -970
  20. package/extension/sidepanel/sidepanel.html +200 -166
  21. package/extension/sidepanel/sidepanel.js +788 -942
  22. package/frontend/dist/assets/index-DGMNZi_1.js +5742 -0
  23. package/frontend/dist/index.html +1 -1
  24. package/frontend/src/api/plugins.ts +124 -92
  25. package/frontend/src/components/ResearchAgendaTab.spec.tsx +11 -0
  26. package/frontend/src/components/ResearchAgendaTab.tsx +19 -14
  27. package/frontend/src/pages/PluginsPage.spec.tsx +87 -16
  28. package/frontend/src/pages/PluginsPage.tsx +454 -1218
  29. package/package.json +2 -1
  30. package/plugins/code-quality/README.md +25 -0
  31. package/plugins/code-quality/cli.mjs +25 -0
  32. package/plugins/code-quality/plugin.json +17 -0
  33. package/plugins/git-sentinel/README.md +27 -0
  34. package/{scaffold/.agent/plugins → plugins}/git-sentinel/cli.mjs +8 -5
  35. package/plugins/git-sentinel/plugin.json +18 -0
  36. package/plugins/system-monitor/README.md +26 -0
  37. package/{scaffold/.agent/plugins → plugins}/system-monitor/cli.mjs +5 -4
  38. package/{scaffold/.agent/plugins → plugins}/system-monitor/generator.mjs +4 -5
  39. package/plugins/system-monitor/plugin.json +29 -0
  40. package/scaffold/.agent/skills/meta-harness/SKILL.md +4 -4
  41. package/scaffold/.agent/skills/plugins/SKILL.md +53 -0
  42. package/scaffold/.agent/skills/total-recall/SKILL.md +22 -6
  43. package/scaffold/.agent/skills/total-recall/scripts/brain-state.json +1 -0
  44. package/src/cli/agent.mjs +1 -1
  45. package/src/cli/app/index.mjs +493 -0
  46. package/src/cli/brain.mjs +33 -3
  47. package/src/cli/command.mjs +80 -41
  48. package/src/cli/connect.mjs +10 -1
  49. package/src/cli/harness.mjs +1 -1
  50. package/src/cli/init.mjs +3 -1
  51. package/src/cli/mesh.mjs +118 -12
  52. package/src/cli/plugin/create.mjs +144 -25
  53. package/src/cli/plugin/index.mjs +83 -53
  54. package/src/cli/plugin/install.mjs +10 -127
  55. package/src/cli/plugin/list.mjs +44 -44
  56. package/src/cli/plugin/remove.mjs +23 -30
  57. package/src/cli/plugin/scaffold-cmd.mjs +30 -0
  58. package/src/cli/plugin/search.mjs +108 -23
  59. package/src/cli/rebuild.mjs +37 -1
  60. package/src/cli/recall.mjs +17 -2
  61. package/src/cli/research.mjs +4 -6
  62. package/src/cli/share.mjs +2 -0
  63. package/src/core/app-deploy/apply.mjs +386 -0
  64. package/src/core/app-deploy/design-tokens.mjs +217 -0
  65. package/src/core/app-deploy/plan.mjs +249 -0
  66. package/src/core/app-deploy/resolve.mjs +295 -0
  67. package/src/core/app-deploy/source.mjs +337 -0
  68. package/src/core/app-deploy/standalone.mjs +248 -0
  69. package/src/core/app-deploy/upgrade.mjs +256 -0
  70. package/src/core/app-deploy/verify.mjs +213 -0
  71. package/src/core/bound-hosts.mjs +1 -1
  72. package/src/core/brain-registry.mjs +146 -0
  73. package/src/core/brain-state.json +1 -0
  74. package/src/core/daemon-loop.mjs +30 -26
  75. package/src/core/dream.mjs +5 -17
  76. package/src/core/fact-seeker.mjs +53 -368
  77. package/src/core/lan-discovery.mjs +3 -3
  78. package/src/core/mesh-access-sync.mjs +134 -0
  79. package/src/core/mesh-access.mjs +18 -3
  80. package/src/core/mesh-discover.mjs +173 -0
  81. package/src/core/mesh.mjs +131 -32
  82. package/src/core/optimizer.mjs +4 -41
  83. package/src/core/plugin-bundle.mjs +167 -0
  84. package/src/core/plugin-context.mjs +8 -13
  85. package/src/core/plugin-loader.mjs +272 -68
  86. package/src/core/plugin-peers.mjs +163 -0
  87. package/src/core/plugin-public.mjs +85 -0
  88. package/src/core/plugin-runner-child.mjs +24 -0
  89. package/src/core/plugin-runner.mjs +102 -0
  90. package/src/core/plugin-store.mjs +384 -0
  91. package/src/core/plugin-tasks.mjs +212 -0
  92. package/src/core/post-mortem.mjs +40 -9
  93. package/src/core/research-gate.mjs +182 -0
  94. package/src/core/research-queue.mjs +47 -7
  95. package/src/core/research-surface.mjs +211 -0
  96. package/src/core/research.mjs +25 -32
  97. package/src/core/scheduler.mjs +67 -153
  98. package/src/core/schema.mjs +129 -0
  99. package/src/core/search.mjs +12 -1
  100. package/src/core/secret-integration-research.mjs +12 -8
  101. package/src/core/secrets-store.mjs +76 -2
  102. package/src/core/session-watcher.mjs +6 -0
  103. package/src/core/surface.mjs +64 -3
  104. package/src/core/task-executors.mjs +8 -45
  105. package/src/core/vault-cache.mjs +70 -4
  106. package/src/server/api.mjs +21 -0
  107. package/src/server/rest.mjs +2 -0
  108. package/src/server/route-manifest.json +21 -5
  109. package/src/server/routes/collab.mjs +38 -11
  110. package/src/server/routes/extension.mjs +3 -2
  111. package/src/server/routes/memory.mjs +24 -2
  112. package/src/server/routes/mesh.mjs +7 -7
  113. package/src/server/routes/plugins-mesh.mjs +49 -0
  114. package/src/server/routes/plugins.mjs +91 -426
  115. package/src/server/routes/research.mjs +5 -2
  116. package/src/server/routes/share.mjs +2 -0
  117. package/src/server/tools.mjs +29 -2
  118. package/extension/icons/README.md +0 -27
  119. package/extension/lib/preconfigured.js +0 -5
  120. package/extension/popup/popup.css +0 -227
  121. package/extension/popup/popup.html +0 -50
  122. package/extension/popup/popup.js +0 -175
  123. package/frontend/dist/assets/index-U7o-qU99.js +0 -5742
  124. package/scaffold/.agent/plugins/.gitkeep +0 -0
  125. package/scaffold/.agent/plugins/code-quality/README.md +0 -16
  126. package/scaffold/.agent/plugins/code-quality/cli.mjs +0 -15
  127. package/scaffold/.agent/plugins/code-quality/plugin.json +0 -34
  128. package/scaffold/.agent/plugins/git-sentinel/README.md +0 -16
  129. package/scaffold/.agent/plugins/git-sentinel/plugin.json +0 -34
  130. package/scaffold/.agent/plugins/meta-harness/README.md +0 -16
  131. package/scaffold/.agent/plugins/meta-harness/cli.mjs +0 -34
  132. package/scaffold/.agent/plugins/meta-harness/plugin.json +0 -34
  133. package/scaffold/.agent/plugins/system-monitor/README.md +0 -16
  134. 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
+ }