@girardmedia/bootspring 1.2.0 → 2.0.3
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/README.md +107 -14
- package/bin/bootspring.js +166 -27
- package/cli/agent.js +189 -17
- package/cli/analyze.js +499 -0
- package/cli/audit.js +557 -0
- package/cli/auth.js +495 -38
- package/cli/billing.js +302 -0
- package/cli/build.js +695 -0
- package/cli/business.js +109 -26
- package/cli/checkpoint-utils.js +168 -0
- package/cli/checkpoint.js +639 -0
- package/cli/cloud-sync.js +447 -0
- package/cli/content.js +198 -0
- package/cli/context.js +1 -1
- package/cli/deploy.js +543 -0
- package/cli/fundraise.js +112 -50
- package/cli/github-cmd.js +435 -0
- package/cli/health.js +477 -0
- package/cli/init.js +84 -13
- package/cli/legal.js +107 -95
- package/cli/log.js +2 -2
- package/cli/loop.js +976 -73
- package/cli/manager.js +711 -0
- package/cli/metrics.js +480 -0
- package/cli/monitor.js +812 -0
- package/cli/onboard.js +521 -0
- package/cli/orchestrator.js +12 -24
- package/cli/prd.js +594 -0
- package/cli/preseed-start.js +1483 -0
- package/cli/preseed.js +2302 -0
- package/cli/project.js +436 -0
- package/cli/quality.js +233 -0
- package/cli/security.js +913 -0
- package/cli/seed.js +1441 -5
- package/cli/skill.js +273 -211
- package/cli/suggest.js +989 -0
- package/cli/switch.js +453 -0
- package/cli/visualize.js +527 -0
- package/cli/watch.js +769 -0
- package/cli/workspace.js +607 -0
- package/core/analyze-workflow.js +1134 -0
- package/core/api-client.js +535 -22
- package/core/audit-workflow.js +1350 -0
- package/core/build-orchestrator.js +480 -0
- package/core/build-state.js +577 -0
- package/core/checkpoint-engine.js +408 -0
- package/core/config.js +1109 -26
- package/core/context-loader.js +21 -1
- package/core/deploy-workflow.js +836 -0
- package/core/entitlements.js +93 -22
- package/core/github-sync.js +610 -0
- package/core/index.js +8 -1
- package/core/ingest.js +1111 -0
- package/core/metrics-engine.js +768 -0
- package/core/onboard-workflow.js +1007 -0
- package/core/preseed-workflow.js +934 -0
- package/core/preseed.js +1617 -0
- package/core/project-context.js +325 -0
- package/core/project-state.js +694 -0
- package/core/r2-sync.js +583 -0
- package/core/scaffold.js +525 -7
- package/core/session.js +258 -0
- package/core/task-extractor.js +758 -0
- package/core/telemetry.js +28 -6
- package/core/tier-enforcement.js +737 -0
- package/core/utils.js +38 -14
- package/generators/questionnaire.js +15 -12
- package/generators/sections/ai.js +7 -7
- package/generators/sections/content.js +300 -0
- package/generators/sections/index.js +3 -0
- package/generators/sections/plugins.js +7 -6
- package/generators/templates/build-planning.template.js +596 -0
- package/generators/templates/content.template.js +819 -0
- package/generators/templates/index.js +2 -1
- package/hooks/git-autopilot.js +1250 -0
- package/hooks/index.js +9 -0
- package/intelligence/agent-collab.js +2057 -0
- package/intelligence/auto-suggest.js +634 -0
- package/intelligence/content-gen.js +1589 -0
- package/intelligence/cross-project.js +1647 -0
- package/intelligence/index.js +184 -0
- package/intelligence/learning/insights.json +517 -7
- package/intelligence/learning/pattern-learner.js +1008 -14
- package/intelligence/memory/decision-tracker.js +1431 -31
- package/intelligence/memory/decisions.jsonl +0 -0
- package/intelligence/orchestrator.js +2896 -1
- package/intelligence/prd.js +92 -1
- package/intelligence/recommendation-weights.json +14 -2
- package/intelligence/recommendations.js +463 -9
- package/intelligence/workflow-composer.js +1451 -0
- package/marketplace/index.d.ts +324 -0
- package/marketplace/index.js +1921 -0
- package/mcp/contracts/mcp-contract.v1.json +342 -4
- package/mcp/registry.js +680 -3
- package/mcp/response-formatter.js +23 -0
- package/mcp/tools/assist-tool.js +78 -4
- package/mcp/tools/autopilot-tool.js +408 -0
- package/mcp/tools/content-tool.js +571 -0
- package/mcp/tools/dashboard-tool.js +251 -5
- package/mcp/tools/mvp-tool.js +344 -0
- package/mcp/tools/plugin-tool.js +23 -1
- package/mcp/tools/prd-tool.js +579 -0
- package/mcp/tools/seed-tool.js +447 -0
- package/mcp/tools/skill-tool.js +43 -14
- package/mcp/tools/suggest-tool.js +147 -0
- package/package.json +15 -6
- package/agents/README.md +0 -93
- package/agents/ai-integration-expert/context.md +0 -386
- package/agents/api-expert/context.md +0 -416
- package/agents/architecture-expert/context.md +0 -454
- package/agents/auth-expert/context.md +0 -399
- package/agents/backend-expert/context.md +0 -483
- package/agents/business-strategy-expert/context.md +0 -180
- package/agents/code-review-expert/context.md +0 -365
- package/agents/competitive-analysis-expert/context.md +0 -239
- package/agents/data-modeling-expert/context.md +0 -352
- package/agents/database-expert/context.md +0 -250
- package/agents/devops-expert/context.md +0 -446
- package/agents/email-expert/context.md +0 -379
- package/agents/financial-expert/context.md +0 -213
- package/agents/frontend-expert/context.md +0 -364
- package/agents/fundraising-expert/context.md +0 -257
- package/agents/growth-expert/context.md +0 -249
- package/agents/index.js +0 -140
- package/agents/investor-relations-expert/context.md +0 -266
- package/agents/legal-expert/context.md +0 -284
- package/agents/marketing-expert/context.md +0 -236
- package/agents/monitoring-expert/context.md +0 -362
- package/agents/operations-expert/context.md +0 -279
- package/agents/partnerships-expert/context.md +0 -286
- package/agents/payment-expert/context.md +0 -340
- package/agents/performance-expert/context.md +0 -377
- package/agents/private-equity-expert/context.md +0 -246
- package/agents/railway-expert/context.md +0 -284
- package/agents/research-expert/context.md +0 -245
- package/agents/sales-expert/context.md +0 -241
- package/agents/security-expert/context.md +0 -343
- package/agents/testing-expert/context.md +0 -414
- package/agents/ui-ux-expert/context.md +0 -448
- package/agents/vercel-expert/context.md +0 -426
- package/skills/index.js +0 -787
- package/skills/patterns/README.md +0 -163
- package/skills/patterns/ai/agents.md +0 -281
- package/skills/patterns/ai/claude.md +0 -138
- package/skills/patterns/ai/embeddings.md +0 -150
- package/skills/patterns/ai/rag.md +0 -266
- package/skills/patterns/ai/streaming.md +0 -170
- package/skills/patterns/ai/structured-output.md +0 -162
- package/skills/patterns/ai/tools.md +0 -154
- package/skills/patterns/analytics/tracking.md +0 -220
- package/skills/patterns/api/errors.md +0 -296
- package/skills/patterns/api/graphql.md +0 -440
- package/skills/patterns/api/middleware.md +0 -279
- package/skills/patterns/api/openapi.md +0 -285
- package/skills/patterns/api/rate-limiting.md +0 -231
- package/skills/patterns/api/route-handler.md +0 -217
- package/skills/patterns/api/server-action.md +0 -249
- package/skills/patterns/api/versioning.md +0 -443
- package/skills/patterns/api/webhooks.md +0 -247
- package/skills/patterns/auth/clerk.md +0 -132
- package/skills/patterns/auth/mfa.md +0 -313
- package/skills/patterns/auth/nextauth.md +0 -140
- package/skills/patterns/auth/oauth.md +0 -237
- package/skills/patterns/auth/rbac.md +0 -152
- package/skills/patterns/auth/session-management.md +0 -367
- package/skills/patterns/auth/session.md +0 -120
- package/skills/patterns/database/audit.md +0 -177
- package/skills/patterns/database/migrations.md +0 -177
- package/skills/patterns/database/pagination.md +0 -230
- package/skills/patterns/database/pooling.md +0 -357
- package/skills/patterns/database/prisma.md +0 -180
- package/skills/patterns/database/relations.md +0 -187
- package/skills/patterns/database/seeding.md +0 -246
- package/skills/patterns/database/soft-delete.md +0 -153
- package/skills/patterns/database/transactions.md +0 -162
- package/skills/patterns/deployment/ci-cd.md +0 -231
- package/skills/patterns/deployment/docker.md +0 -188
- package/skills/patterns/deployment/monitoring.md +0 -387
- package/skills/patterns/deployment/vercel.md +0 -160
- package/skills/patterns/email/resend.md +0 -143
- package/skills/patterns/email/templates.md +0 -245
- package/skills/patterns/email/transactional.md +0 -503
- package/skills/patterns/email/verification.md +0 -176
- package/skills/patterns/files/download.md +0 -243
- package/skills/patterns/files/upload.md +0 -239
- package/skills/patterns/i18n/nextintl.md +0 -188
- package/skills/patterns/logging/structured.md +0 -292
- package/skills/patterns/notifications/email-queue.md +0 -248
- package/skills/patterns/notifications/push.md +0 -279
- package/skills/patterns/payments/checkout.md +0 -303
- package/skills/patterns/payments/invoices.md +0 -287
- package/skills/patterns/payments/portal.md +0 -245
- package/skills/patterns/payments/stripe.md +0 -272
- package/skills/patterns/payments/subscriptions.md +0 -300
- package/skills/patterns/payments/usage.md +0 -279
- package/skills/patterns/performance/caching.md +0 -276
- package/skills/patterns/performance/code-splitting.md +0 -233
- package/skills/patterns/performance/edge.md +0 -254
- package/skills/patterns/performance/isr.md +0 -266
- package/skills/patterns/performance/lazy-loading.md +0 -281
- package/skills/patterns/realtime/sse.md +0 -327
- package/skills/patterns/realtime/websockets.md +0 -336
- package/skills/patterns/search/filtering.md +0 -329
- package/skills/patterns/search/fulltext.md +0 -260
- package/skills/patterns/security/audit-logging.md +0 -444
- package/skills/patterns/security/csrf.md +0 -234
- package/skills/patterns/security/headers.md +0 -252
- package/skills/patterns/security/sanitization.md +0 -258
- package/skills/patterns/security/secrets.md +0 -261
- package/skills/patterns/security/validation.md +0 -268
- package/skills/patterns/security/xss.md +0 -229
- package/skills/patterns/seo/metadata.md +0 -252
- package/skills/patterns/state/context.md +0 -349
- package/skills/patterns/state/react-query.md +0 -313
- package/skills/patterns/state/url-state.md +0 -482
- package/skills/patterns/state/zustand.md +0 -262
- package/skills/patterns/testing/api.md +0 -259
- package/skills/patterns/testing/component.md +0 -233
- package/skills/patterns/testing/coverage.md +0 -207
- package/skills/patterns/testing/fixtures.md +0 -225
- package/skills/patterns/testing/integration.md +0 -436
- package/skills/patterns/testing/mocking.md +0 -177
- package/skills/patterns/testing/playwright.md +0 -162
- package/skills/patterns/testing/snapshot.md +0 -175
- package/skills/patterns/testing/vitest.md +0 -307
- package/skills/patterns/ui/accordions.md +0 -395
- package/skills/patterns/ui/cards.md +0 -299
- package/skills/patterns/ui/dropdowns.md +0 -476
- package/skills/patterns/ui/empty-states.md +0 -320
- package/skills/patterns/ui/forms.md +0 -405
- package/skills/patterns/ui/inputs.md +0 -319
- package/skills/patterns/ui/layouts.md +0 -282
- package/skills/patterns/ui/loading.md +0 -291
- package/skills/patterns/ui/modals.md +0 -338
- package/skills/patterns/ui/navigation.md +0 -374
- package/skills/patterns/ui/tables.md +0 -407
- package/skills/patterns/ui/toasts.md +0 -300
- package/skills/patterns/ui/tooltips.md +0 -396
- package/skills/patterns/utils/dates.md +0 -435
- package/skills/patterns/utils/errors.md +0 -451
- package/skills/patterns/utils/formatting.md +0 -345
- package/skills/patterns/utils/validation.md +0 -434
- package/templates/bootspring.config.js +0 -83
- package/templates/business/business-model-canvas.md +0 -246
- package/templates/business/business-plan.md +0 -266
- package/templates/business/competitive-analysis.md +0 -312
- package/templates/fundraising/data-room-checklist.md +0 -300
- package/templates/fundraising/investor-research.md +0 -243
- package/templates/fundraising/pitch-deck-outline.md +0 -253
- package/templates/legal/gdpr-checklist.md +0 -339
- package/templates/legal/privacy-policy.md +0 -285
- package/templates/legal/terms-of-service.md +0 -222
- package/templates/mcp.json +0 -9
|
@@ -0,0 +1,1647 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bootspring Cross-Project Learning
|
|
3
|
+
*
|
|
4
|
+
* Enables sharing of anonymized learnings across projects (opt-in).
|
|
5
|
+
* Aggregates patterns and insights to improve recommendations for all users.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - extractAnonymizedLearnings() - Extract learnings without sensitive data
|
|
9
|
+
* - shareLearnings(learnings) - Upload to shared pool
|
|
10
|
+
* - fetchCommunityPatterns() - Download community patterns
|
|
11
|
+
* - applyRelevantPatterns(projectContext) - Apply relevant patterns
|
|
12
|
+
*
|
|
13
|
+
* @package bootspring
|
|
14
|
+
* @module intelligence/cross-project
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const crypto = require('crypto');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Cross-project learning configuration
|
|
23
|
+
* Can be overridden via bootspring.config.js
|
|
24
|
+
*/
|
|
25
|
+
const CROSS_PROJECT_CONFIG = {
|
|
26
|
+
enabled: false, // Must be explicitly enabled
|
|
27
|
+
anonymize: true, // Always anonymize by default
|
|
28
|
+
sharePatterns: true, // Share learned patterns
|
|
29
|
+
shareDecisions: true, // Share decision outcomes
|
|
30
|
+
shareWorkflows: true, // Share workflow completions
|
|
31
|
+
consentRequired: true, // Require explicit consent
|
|
32
|
+
// API configuration for central sharing service
|
|
33
|
+
apiEndpoint: process.env.BOOTSPRING_SHARING_API || 'https://api.bootspring.dev/v1/learnings',
|
|
34
|
+
apiKey: process.env.BOOTSPRING_SHARING_KEY || null,
|
|
35
|
+
// Local caching
|
|
36
|
+
cacheExpiry: 24 * 60 * 60 * 1000, // 24 hours in ms
|
|
37
|
+
maxCachedPatterns: 500,
|
|
38
|
+
// Privacy settings
|
|
39
|
+
stripFilePaths: true,
|
|
40
|
+
stripUsernames: true,
|
|
41
|
+
stripProjectNames: true,
|
|
42
|
+
minPatternOccurrences: 3 // Only share patterns seen at least this many times
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Load configuration from bootspring.config.js if available
|
|
47
|
+
*/
|
|
48
|
+
function loadConfig() {
|
|
49
|
+
const projectRoot = process.cwd();
|
|
50
|
+
const configPath = path.join(projectRoot, 'bootspring.config.js');
|
|
51
|
+
|
|
52
|
+
if (fs.existsSync(configPath)) {
|
|
53
|
+
try {
|
|
54
|
+
// Clear require cache to get fresh config
|
|
55
|
+
delete require.cache[require.resolve(configPath)];
|
|
56
|
+
const userConfig = require(configPath);
|
|
57
|
+
|
|
58
|
+
if (userConfig.crossProject) {
|
|
59
|
+
return {
|
|
60
|
+
...CROSS_PROJECT_CONFIG,
|
|
61
|
+
...userConfig.crossProject
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// Fall through to default config
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return CROSS_PROJECT_CONFIG;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get current configuration
|
|
74
|
+
*/
|
|
75
|
+
function getConfig() {
|
|
76
|
+
return loadConfig();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Fields to always remove during anonymization
|
|
81
|
+
*/
|
|
82
|
+
const SENSITIVE_FIELDS = [
|
|
83
|
+
'email', 'password', 'secret', 'key', 'token', 'api_key', 'apiKey',
|
|
84
|
+
'auth', 'credential', 'private', 'phone', 'address', 'name',
|
|
85
|
+
'username', 'user', 'account', 'ssn', 'credit', 'payment',
|
|
86
|
+
'company', 'domain', 'host', 'ip', 'client', 'customer'
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Patterns that identify project-specific content to strip
|
|
91
|
+
*/
|
|
92
|
+
const PROJECT_SPECIFIC_PATTERNS = [
|
|
93
|
+
/\/Users\/[^/]+/g, // macOS user paths
|
|
94
|
+
/\/home\/[^/]+/g, // Linux user paths
|
|
95
|
+
/C:\\Users\\[^\\]+/g, // Windows user paths
|
|
96
|
+
/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, // Email addresses
|
|
97
|
+
/https?:\/\/[^\s]+/g, // URLs
|
|
98
|
+
/[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/g, // IP addresses
|
|
99
|
+
/[a-f0-9]{32,}/gi // Long hex strings (likely tokens/hashes)
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get paths for cross-project learning
|
|
104
|
+
*/
|
|
105
|
+
function getPaths() {
|
|
106
|
+
const projectRoot = process.cwd();
|
|
107
|
+
return {
|
|
108
|
+
projectRoot,
|
|
109
|
+
learningsDir: path.join(projectRoot, '.bootspring', 'cross-project'),
|
|
110
|
+
consentPath: path.join(projectRoot, '.bootspring', 'cross-project', 'consent.json'),
|
|
111
|
+
contributionsPath: path.join(projectRoot, '.bootspring', 'cross-project', 'contributions.json'),
|
|
112
|
+
receivedPath: path.join(projectRoot, '.bootspring', 'cross-project', 'received.json')
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Ensure cross-project directories exist
|
|
118
|
+
*/
|
|
119
|
+
function ensureDirectories() {
|
|
120
|
+
const paths = getPaths();
|
|
121
|
+
if (!fs.existsSync(paths.learningsDir)) {
|
|
122
|
+
fs.mkdirSync(paths.learningsDir, { recursive: true });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Generate anonymous project ID
|
|
128
|
+
* @param {string} projectRoot - Project root path
|
|
129
|
+
*/
|
|
130
|
+
function generateProjectId(projectRoot) {
|
|
131
|
+
const hash = crypto.createHash('sha256');
|
|
132
|
+
hash.update(projectRoot);
|
|
133
|
+
hash.update(Date.now().toString());
|
|
134
|
+
return `proj_${hash.digest('hex').substring(0, 16)}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Load consent status
|
|
139
|
+
*/
|
|
140
|
+
function loadConsent() {
|
|
141
|
+
const paths = getPaths();
|
|
142
|
+
if (fs.existsSync(paths.consentPath)) {
|
|
143
|
+
try {
|
|
144
|
+
return JSON.parse(fs.readFileSync(paths.consentPath, 'utf8'));
|
|
145
|
+
} catch {
|
|
146
|
+
return createDefaultConsent();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return createDefaultConsent();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Create default consent state
|
|
154
|
+
*/
|
|
155
|
+
function createDefaultConsent() {
|
|
156
|
+
return {
|
|
157
|
+
given: false,
|
|
158
|
+
givenAt: null,
|
|
159
|
+
projectId: null,
|
|
160
|
+
sharePatterns: false,
|
|
161
|
+
shareDecisions: false,
|
|
162
|
+
shareWorkflows: false,
|
|
163
|
+
revokedAt: null
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Save consent status
|
|
169
|
+
*/
|
|
170
|
+
function saveConsent(consent) {
|
|
171
|
+
const paths = getPaths();
|
|
172
|
+
ensureDirectories();
|
|
173
|
+
consent.lastUpdated = new Date().toISOString();
|
|
174
|
+
fs.writeFileSync(paths.consentPath, JSON.stringify(consent, null, 2));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Grant consent for cross-project learning
|
|
179
|
+
* @param {object} options - Consent options
|
|
180
|
+
*/
|
|
181
|
+
function grantConsent(options = {}) {
|
|
182
|
+
const paths = getPaths();
|
|
183
|
+
const consent = loadConsent();
|
|
184
|
+
|
|
185
|
+
consent.given = true;
|
|
186
|
+
consent.givenAt = new Date().toISOString();
|
|
187
|
+
consent.projectId = consent.projectId || generateProjectId(paths.projectRoot);
|
|
188
|
+
consent.sharePatterns = options.sharePatterns !== false;
|
|
189
|
+
consent.shareDecisions = options.shareDecisions !== false;
|
|
190
|
+
consent.shareWorkflows = options.shareWorkflows !== false;
|
|
191
|
+
consent.revokedAt = null;
|
|
192
|
+
|
|
193
|
+
saveConsent(consent);
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
success: true,
|
|
197
|
+
projectId: consent.projectId,
|
|
198
|
+
sharing: {
|
|
199
|
+
patterns: consent.sharePatterns,
|
|
200
|
+
decisions: consent.shareDecisions,
|
|
201
|
+
workflows: consent.shareWorkflows
|
|
202
|
+
},
|
|
203
|
+
message: 'Cross-project learning consent granted'
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Revoke consent for cross-project learning
|
|
209
|
+
*/
|
|
210
|
+
function revokeConsent() {
|
|
211
|
+
const consent = loadConsent();
|
|
212
|
+
|
|
213
|
+
if (!consent.given) {
|
|
214
|
+
return { success: false, error: 'Consent was not previously granted' };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
consent.given = false;
|
|
218
|
+
consent.revokedAt = new Date().toISOString();
|
|
219
|
+
consent.sharePatterns = false;
|
|
220
|
+
consent.shareDecisions = false;
|
|
221
|
+
consent.shareWorkflows = false;
|
|
222
|
+
|
|
223
|
+
saveConsent(consent);
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
success: true,
|
|
227
|
+
revokedAt: consent.revokedAt,
|
|
228
|
+
message: 'Cross-project learning consent revoked. Your data will no longer be shared.'
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Check if consent is active
|
|
234
|
+
*/
|
|
235
|
+
function hasConsent() {
|
|
236
|
+
const consent = loadConsent();
|
|
237
|
+
return consent.given && !consent.revokedAt;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get current consent status
|
|
242
|
+
*/
|
|
243
|
+
function getConsentStatus() {
|
|
244
|
+
const consent = loadConsent();
|
|
245
|
+
return {
|
|
246
|
+
active: consent.given && !consent.revokedAt,
|
|
247
|
+
projectId: consent.given ? consent.projectId : null,
|
|
248
|
+
givenAt: consent.givenAt,
|
|
249
|
+
revokedAt: consent.revokedAt,
|
|
250
|
+
sharing: consent.given ? {
|
|
251
|
+
patterns: consent.sharePatterns,
|
|
252
|
+
decisions: consent.shareDecisions,
|
|
253
|
+
workflows: consent.shareWorkflows
|
|
254
|
+
} : null
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Anonymize a data object by removing sensitive fields and hashing identifiers
|
|
260
|
+
* @param {object} data - Data to anonymize
|
|
261
|
+
*/
|
|
262
|
+
function anonymize(data) {
|
|
263
|
+
if (!data || typeof data !== 'object') {
|
|
264
|
+
return data;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (Array.isArray(data)) {
|
|
268
|
+
return data.map(item => anonymize(item));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const result = {};
|
|
272
|
+
|
|
273
|
+
for (const [key, value] of Object.entries(data)) {
|
|
274
|
+
const keyLower = key.toLowerCase();
|
|
275
|
+
|
|
276
|
+
// Skip sensitive fields
|
|
277
|
+
if (SENSITIVE_FIELDS.some(field => keyLower.includes(field))) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Hash identifiable strings that look like paths or names
|
|
282
|
+
if (typeof value === 'string') {
|
|
283
|
+
if (value.startsWith('/') && value.includes('/Users/')) {
|
|
284
|
+
// Anonymize file paths
|
|
285
|
+
result[key] = hashPath(value);
|
|
286
|
+
} else if (value.includes('@') || keyLower.includes('id')) {
|
|
287
|
+
// Hash potential identifiers
|
|
288
|
+
result[key] = hashString(value);
|
|
289
|
+
} else {
|
|
290
|
+
result[key] = value;
|
|
291
|
+
}
|
|
292
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
293
|
+
// Recursively anonymize nested objects
|
|
294
|
+
result[key] = anonymize(value);
|
|
295
|
+
} else {
|
|
296
|
+
result[key] = value;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Hash a string for anonymization
|
|
305
|
+
* @param {string} str - String to hash
|
|
306
|
+
*/
|
|
307
|
+
function hashString(str) {
|
|
308
|
+
const hash = crypto.createHash('sha256');
|
|
309
|
+
hash.update(str);
|
|
310
|
+
return `anon_${hash.digest('hex').substring(0, 12)}`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Anonymize a file path
|
|
315
|
+
* @param {string} filePath - File path to anonymize
|
|
316
|
+
*/
|
|
317
|
+
function hashPath(filePath) {
|
|
318
|
+
// Keep the file extension and relative structure
|
|
319
|
+
const parts = filePath.split('/');
|
|
320
|
+
const filename = parts[parts.length - 1];
|
|
321
|
+
const ext = path.extname(filename);
|
|
322
|
+
const dir = parts.slice(-2, -1)[0] || 'root';
|
|
323
|
+
|
|
324
|
+
return `[PROJECT]/${dir}/${hashString(filename).substring(0, 8)}${ext}`;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Deeply anonymize a string by removing all project-specific content
|
|
329
|
+
* @param {string} str - String to anonymize
|
|
330
|
+
* @returns {string} Anonymized string
|
|
331
|
+
*/
|
|
332
|
+
function anonymizeString(str) {
|
|
333
|
+
if (!str || typeof str !== 'string') return str;
|
|
334
|
+
|
|
335
|
+
let result = str;
|
|
336
|
+
|
|
337
|
+
// Remove project-specific patterns
|
|
338
|
+
for (const pattern of PROJECT_SPECIFIC_PATTERNS) {
|
|
339
|
+
result = result.replace(pattern, '[REDACTED]');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return result;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ============================================================================
|
|
346
|
+
// MAIN CROSS-PROJECT LEARNING FUNCTIONS
|
|
347
|
+
// ============================================================================
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Extract anonymized learnings from the current project
|
|
351
|
+
* Gathers patterns, decisions, and workflow data while stripping sensitive info
|
|
352
|
+
*
|
|
353
|
+
* @param {object} options - Extraction options
|
|
354
|
+
* @param {boolean} options.includePatterns - Include learned patterns (default: true)
|
|
355
|
+
* @param {boolean} options.includeDecisions - Include decision outcomes (default: true)
|
|
356
|
+
* @param {boolean} options.includeWorkflows - Include workflow completions (default: true)
|
|
357
|
+
* @param {number} options.minConfidence - Minimum confidence threshold (default: 0.5)
|
|
358
|
+
* @param {number} options.minOccurrences - Minimum pattern occurrences (default: 3)
|
|
359
|
+
* @returns {object} Anonymized learnings ready for sharing
|
|
360
|
+
*/
|
|
361
|
+
function extractAnonymizedLearnings(options = {}) {
|
|
362
|
+
const config = getConfig();
|
|
363
|
+
const {
|
|
364
|
+
includePatterns = config.sharePatterns,
|
|
365
|
+
includeDecisions = config.shareDecisions,
|
|
366
|
+
includeWorkflows = config.shareWorkflows,
|
|
367
|
+
minConfidence = 0.5,
|
|
368
|
+
minOccurrences = config.minPatternOccurrences || 3
|
|
369
|
+
} = options;
|
|
370
|
+
|
|
371
|
+
const learnings = {
|
|
372
|
+
version: '1.0.0',
|
|
373
|
+
extractedAt: new Date().toISOString(),
|
|
374
|
+
projectId: getAnonymousProjectId(),
|
|
375
|
+
techStack: extractAnonymizedStack(),
|
|
376
|
+
patterns: [],
|
|
377
|
+
decisions: [],
|
|
378
|
+
workflows: [],
|
|
379
|
+
aggregates: {}
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
// Extract patterns from decision tracker
|
|
383
|
+
if (includePatterns) {
|
|
384
|
+
try {
|
|
385
|
+
const decisionTracker = require('./memory/decision-tracker');
|
|
386
|
+
const patterns = decisionTracker.getPatterns() || [];
|
|
387
|
+
|
|
388
|
+
learnings.patterns = patterns
|
|
389
|
+
.filter(p => p.occurrences >= minOccurrences && (p.confidence || 0.5) >= minConfidence)
|
|
390
|
+
.map(p => anonymizePattern(p));
|
|
391
|
+
} catch {
|
|
392
|
+
// Decision tracker not available
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Extract decision outcomes
|
|
397
|
+
if (includeDecisions) {
|
|
398
|
+
try {
|
|
399
|
+
const decisionTracker = require('./memory/decision-tracker');
|
|
400
|
+
const decisions = decisionTracker.readAllDecisions() || [];
|
|
401
|
+
|
|
402
|
+
// Only include completed decisions with outcomes
|
|
403
|
+
const completedDecisions = decisions.filter(d => d.outcome && d.outcome.status);
|
|
404
|
+
|
|
405
|
+
// Aggregate by type for privacy
|
|
406
|
+
const aggregatedDecisions = aggregateDecisionsByType(completedDecisions);
|
|
407
|
+
learnings.decisions = aggregatedDecisions;
|
|
408
|
+
|
|
409
|
+
// Also include high-impact patterns
|
|
410
|
+
const impactPatterns = decisionTracker.getImpactBoostedPatterns
|
|
411
|
+
? decisionTracker.getImpactBoostedPatterns({ minScore: 3.0, minSuccessRate: 0.6 })
|
|
412
|
+
: [];
|
|
413
|
+
|
|
414
|
+
learnings.aggregates.impactPatterns = impactPatterns.map(p => ({
|
|
415
|
+
type: p.type,
|
|
416
|
+
occurrences: p.occurrences,
|
|
417
|
+
averageImpactScore: p.averageImpactScore,
|
|
418
|
+
successRate: p.successRate,
|
|
419
|
+
recommendation: p.recommendation
|
|
420
|
+
}));
|
|
421
|
+
} catch {
|
|
422
|
+
// Decision tracker not available
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Extract workflow completions
|
|
427
|
+
if (includeWorkflows) {
|
|
428
|
+
try {
|
|
429
|
+
const workflowData = extractWorkflowData();
|
|
430
|
+
learnings.workflows = workflowData;
|
|
431
|
+
} catch {
|
|
432
|
+
// Workflow data not available
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Add learning summary
|
|
437
|
+
learnings.aggregates.summary = {
|
|
438
|
+
totalPatterns: learnings.patterns.length,
|
|
439
|
+
totalDecisionTypes: learnings.decisions.length,
|
|
440
|
+
totalWorkflows: learnings.workflows.length,
|
|
441
|
+
overallSuccessRate: calculateOverallSuccessRate(learnings.decisions)
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
return learnings;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Get anonymous project identifier (hash of project path)
|
|
449
|
+
*/
|
|
450
|
+
function getAnonymousProjectId() {
|
|
451
|
+
const consent = loadConsent();
|
|
452
|
+
return consent.projectId || generateProjectId(process.cwd());
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Extract anonymized tech stack information
|
|
457
|
+
*/
|
|
458
|
+
function extractAnonymizedStack() {
|
|
459
|
+
const projectRoot = process.cwd();
|
|
460
|
+
const stack = {
|
|
461
|
+
framework: null,
|
|
462
|
+
language: null,
|
|
463
|
+
database: null,
|
|
464
|
+
testing: null
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
// Detect framework from package.json
|
|
468
|
+
const packageJsonPath = path.join(projectRoot, 'package.json');
|
|
469
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
470
|
+
try {
|
|
471
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
472
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
473
|
+
|
|
474
|
+
// Framework detection
|
|
475
|
+
if (deps.next) stack.framework = 'nextjs';
|
|
476
|
+
else if (deps.react) stack.framework = 'react';
|
|
477
|
+
else if (deps.vue) stack.framework = 'vue';
|
|
478
|
+
else if (deps['@angular/core']) stack.framework = 'angular';
|
|
479
|
+
else if (deps.express) stack.framework = 'express';
|
|
480
|
+
else if (deps.fastify) stack.framework = 'fastify';
|
|
481
|
+
|
|
482
|
+
// Language detection
|
|
483
|
+
if (deps.typescript || fs.existsSync(path.join(projectRoot, 'tsconfig.json'))) {
|
|
484
|
+
stack.language = 'typescript';
|
|
485
|
+
} else {
|
|
486
|
+
stack.language = 'javascript';
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Database detection
|
|
490
|
+
if (deps.prisma || deps['@prisma/client']) stack.database = 'prisma';
|
|
491
|
+
else if (deps.mongoose) stack.database = 'mongodb';
|
|
492
|
+
else if (deps.pg || deps['node-postgres']) stack.database = 'postgresql';
|
|
493
|
+
else if (deps.mysql2) stack.database = 'mysql';
|
|
494
|
+
else if (deps.drizzle) stack.database = 'drizzle';
|
|
495
|
+
|
|
496
|
+
// Testing detection
|
|
497
|
+
if (deps.vitest) stack.testing = 'vitest';
|
|
498
|
+
else if (deps.jest) stack.testing = 'jest';
|
|
499
|
+
else if (deps.mocha) stack.testing = 'mocha';
|
|
500
|
+
else if (deps.playwright) stack.testing = 'playwright';
|
|
501
|
+
} catch {
|
|
502
|
+
// Package.json parsing failed
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return stack;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Anonymize a single pattern for sharing
|
|
511
|
+
*/
|
|
512
|
+
function anonymizePattern(pattern) {
|
|
513
|
+
return {
|
|
514
|
+
type: pattern.type,
|
|
515
|
+
category: categorizePattern(pattern),
|
|
516
|
+
successRate: pattern.success_rate,
|
|
517
|
+
occurrences: pattern.occurrences,
|
|
518
|
+
confidence: pattern.confidence,
|
|
519
|
+
recommendation: pattern.recommendation,
|
|
520
|
+
contextHints: (pattern.context_hints || []).map(h => anonymizeString(h))
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Categorize a pattern for cross-project relevance
|
|
526
|
+
*/
|
|
527
|
+
function categorizePattern(pattern) {
|
|
528
|
+
const type = (pattern.type || '').toLowerCase();
|
|
529
|
+
const decision = (pattern.decision || '').toLowerCase();
|
|
530
|
+
|
|
531
|
+
if (type.includes('database') || decision.includes('migration') || decision.includes('schema')) {
|
|
532
|
+
return 'database';
|
|
533
|
+
}
|
|
534
|
+
if (type.includes('security') || decision.includes('auth') || decision.includes('security')) {
|
|
535
|
+
return 'security';
|
|
536
|
+
}
|
|
537
|
+
if (type.includes('api') || decision.includes('endpoint') || decision.includes('route')) {
|
|
538
|
+
return 'api';
|
|
539
|
+
}
|
|
540
|
+
if (type.includes('test') || decision.includes('test') || decision.includes('coverage')) {
|
|
541
|
+
return 'testing';
|
|
542
|
+
}
|
|
543
|
+
if (type.includes('deploy') || decision.includes('deploy') || decision.includes('ci')) {
|
|
544
|
+
return 'deployment';
|
|
545
|
+
}
|
|
546
|
+
if (type.includes('perf') || decision.includes('performance') || decision.includes('optimize')) {
|
|
547
|
+
return 'performance';
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return 'general';
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Aggregate decisions by type for privacy
|
|
555
|
+
*/
|
|
556
|
+
function aggregateDecisionsByType(decisions) {
|
|
557
|
+
const aggregates = {};
|
|
558
|
+
|
|
559
|
+
for (const d of decisions) {
|
|
560
|
+
const type = d.type || 'unknown';
|
|
561
|
+
if (!aggregates[type]) {
|
|
562
|
+
aggregates[type] = {
|
|
563
|
+
type,
|
|
564
|
+
count: 0,
|
|
565
|
+
successCount: 0,
|
|
566
|
+
partialCount: 0,
|
|
567
|
+
failureCount: 0,
|
|
568
|
+
averageConfidence: 0,
|
|
569
|
+
totalConfidence: 0
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
aggregates[type].count++;
|
|
574
|
+
aggregates[type].totalConfidence += (d.confidence || 0.5);
|
|
575
|
+
|
|
576
|
+
if (d.outcome.status === 'success') aggregates[type].successCount++;
|
|
577
|
+
else if (d.outcome.status === 'partial') aggregates[type].partialCount++;
|
|
578
|
+
else if (d.outcome.status === 'failure') aggregates[type].failureCount++;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Calculate final metrics
|
|
582
|
+
return Object.values(aggregates).map(a => ({
|
|
583
|
+
type: a.type,
|
|
584
|
+
count: a.count,
|
|
585
|
+
successRate: a.count > 0 ? Math.round((a.successCount / a.count) * 100) / 100 : 0,
|
|
586
|
+
partialRate: a.count > 0 ? Math.round((a.partialCount / a.count) * 100) / 100 : 0,
|
|
587
|
+
failureRate: a.count > 0 ? Math.round((a.failureCount / a.count) * 100) / 100 : 0,
|
|
588
|
+
averageConfidence: a.count > 0 ? Math.round((a.totalConfidence / a.count) * 100) / 100 : 0
|
|
589
|
+
}));
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Extract workflow completion data
|
|
594
|
+
*/
|
|
595
|
+
function extractWorkflowData() {
|
|
596
|
+
const paths = getPaths();
|
|
597
|
+
const workflowDataPath = path.join(paths.projectRoot, '.bootspring', 'workflow-history.json');
|
|
598
|
+
|
|
599
|
+
if (!fs.existsSync(workflowDataPath)) {
|
|
600
|
+
return [];
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
try {
|
|
604
|
+
const data = JSON.parse(fs.readFileSync(workflowDataPath, 'utf8'));
|
|
605
|
+
const workflows = data.completed || [];
|
|
606
|
+
|
|
607
|
+
return workflows.map(w => ({
|
|
608
|
+
workflowType: w.workflow || w.type || 'unknown',
|
|
609
|
+
phasesCompleted: w.phasesCompleted || w.completedPhases || 0,
|
|
610
|
+
totalPhases: w.totalPhases || w.phases?.length || 0,
|
|
611
|
+
duration: w.duration, // In minutes
|
|
612
|
+
success: w.success !== false,
|
|
613
|
+
tier: w.tier || 'free'
|
|
614
|
+
}));
|
|
615
|
+
} catch {
|
|
616
|
+
return [];
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Calculate overall success rate from aggregated decisions
|
|
622
|
+
*/
|
|
623
|
+
function calculateOverallSuccessRate(aggregatedDecisions) {
|
|
624
|
+
if (!aggregatedDecisions || aggregatedDecisions.length === 0) return 0;
|
|
625
|
+
|
|
626
|
+
let totalCount = 0;
|
|
627
|
+
let totalSuccess = 0;
|
|
628
|
+
|
|
629
|
+
for (const d of aggregatedDecisions) {
|
|
630
|
+
totalCount += d.count;
|
|
631
|
+
totalSuccess += d.count * d.successRate;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return totalCount > 0 ? Math.round((totalSuccess / totalCount) * 100) / 100 : 0;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Share learnings to the central pool
|
|
639
|
+
* Uploads anonymized learnings to the sharing service (opt-in)
|
|
640
|
+
*
|
|
641
|
+
* @param {object} learnings - Learnings to share (from extractAnonymizedLearnings)
|
|
642
|
+
* @param {object} options - Sharing options
|
|
643
|
+
* @param {boolean} options.dryRun - If true, validate but don't actually share
|
|
644
|
+
* @returns {Promise<object>} Sharing result
|
|
645
|
+
*/
|
|
646
|
+
async function shareLearnings(learnings, options = {}) {
|
|
647
|
+
const { dryRun = false } = options;
|
|
648
|
+
const config = getConfig();
|
|
649
|
+
|
|
650
|
+
// Validate consent
|
|
651
|
+
if (!hasConsent()) {
|
|
652
|
+
return {
|
|
653
|
+
success: false,
|
|
654
|
+
error: 'Consent not granted for cross-project learning',
|
|
655
|
+
hint: 'Use grantConsent() to enable sharing'
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Validate config
|
|
660
|
+
if (!config.enabled) {
|
|
661
|
+
return {
|
|
662
|
+
success: false,
|
|
663
|
+
error: 'Cross-project learning is not enabled',
|
|
664
|
+
hint: 'Set crossProject.enabled = true in bootspring.config.js'
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Ensure learnings are properly anonymized
|
|
669
|
+
const sanitizedLearnings = {
|
|
670
|
+
...learnings,
|
|
671
|
+
patterns: learnings.patterns.map(p => anonymize(p)),
|
|
672
|
+
decisions: learnings.decisions.map(d => anonymize(d)),
|
|
673
|
+
workflows: learnings.workflows.map(w => anonymize(w))
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
// Record what we're sharing locally
|
|
677
|
+
const contribution = {
|
|
678
|
+
id: `contrib_${Date.now()}_${Math.random().toString(36).substring(7)}`,
|
|
679
|
+
sharedAt: new Date().toISOString(),
|
|
680
|
+
learningsVersion: sanitizedLearnings.version,
|
|
681
|
+
patternCount: sanitizedLearnings.patterns.length,
|
|
682
|
+
decisionTypeCount: sanitizedLearnings.decisions.length,
|
|
683
|
+
workflowCount: sanitizedLearnings.workflows.length,
|
|
684
|
+
dryRun
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
if (dryRun) {
|
|
688
|
+
return {
|
|
689
|
+
success: true,
|
|
690
|
+
dryRun: true,
|
|
691
|
+
contribution,
|
|
692
|
+
learnings: sanitizedLearnings,
|
|
693
|
+
message: 'Dry run complete - learnings validated but not shared'
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// In a real implementation, this would POST to the API endpoint
|
|
698
|
+
// For now, we simulate the upload and store locally
|
|
699
|
+
try {
|
|
700
|
+
// Simulate API call (in production, this would be actual HTTP request)
|
|
701
|
+
const apiResult = await simulateApiUpload(sanitizedLearnings, config);
|
|
702
|
+
|
|
703
|
+
if (apiResult.success) {
|
|
704
|
+
// Save contribution record
|
|
705
|
+
const contributions = loadContributions();
|
|
706
|
+
contributions.contributions.push(contribution);
|
|
707
|
+
contributions.count++;
|
|
708
|
+
saveContributions(contributions);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
return {
|
|
712
|
+
success: apiResult.success,
|
|
713
|
+
contribution,
|
|
714
|
+
apiResponse: apiResult,
|
|
715
|
+
message: apiResult.success
|
|
716
|
+
? 'Learnings shared successfully'
|
|
717
|
+
: 'Failed to share learnings'
|
|
718
|
+
};
|
|
719
|
+
} catch (error) {
|
|
720
|
+
return {
|
|
721
|
+
success: false,
|
|
722
|
+
error: error.message || 'Failed to share learnings',
|
|
723
|
+
contribution
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Simulate API upload (placeholder for real implementation)
|
|
730
|
+
* In production, this would make an actual HTTP request
|
|
731
|
+
*/
|
|
732
|
+
async function simulateApiUpload(learnings, config) {
|
|
733
|
+
// Simulate network delay
|
|
734
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
735
|
+
|
|
736
|
+
// Validate learnings structure
|
|
737
|
+
if (!learnings.projectId || !learnings.extractedAt) {
|
|
738
|
+
return { success: false, error: 'Invalid learnings structure' };
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// In production, this would be:
|
|
742
|
+
// const response = await fetch(config.apiEndpoint, {
|
|
743
|
+
// method: 'POST',
|
|
744
|
+
// headers: {
|
|
745
|
+
// 'Content-Type': 'application/json',
|
|
746
|
+
// 'Authorization': `Bearer ${config.apiKey}`
|
|
747
|
+
// },
|
|
748
|
+
// body: JSON.stringify(learnings)
|
|
749
|
+
// });
|
|
750
|
+
|
|
751
|
+
return {
|
|
752
|
+
success: true,
|
|
753
|
+
received: learnings.patterns.length + learnings.decisions.length + learnings.workflows.length,
|
|
754
|
+
timestamp: new Date().toISOString()
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Fetch community patterns from the shared pool
|
|
760
|
+
* Downloads aggregated patterns learned from the community
|
|
761
|
+
*
|
|
762
|
+
* @param {object} options - Fetch options
|
|
763
|
+
* @param {string} options.category - Filter by category (database, security, api, etc.)
|
|
764
|
+
* @param {number} options.minConfidence - Minimum confidence threshold
|
|
765
|
+
* @param {number} options.limit - Maximum patterns to fetch
|
|
766
|
+
* @param {boolean} options.useCache - Use cached patterns if available (default: true)
|
|
767
|
+
* @returns {Promise<object>} Community patterns
|
|
768
|
+
*/
|
|
769
|
+
async function fetchCommunityPatterns(options = {}) {
|
|
770
|
+
const config = getConfig();
|
|
771
|
+
const {
|
|
772
|
+
category = null,
|
|
773
|
+
minConfidence = 0.5,
|
|
774
|
+
limit = 100,
|
|
775
|
+
useCache = true
|
|
776
|
+
} = options;
|
|
777
|
+
|
|
778
|
+
const paths = getPaths();
|
|
779
|
+
const cachePath = path.join(paths.learningsDir, 'community-patterns-cache.json');
|
|
780
|
+
|
|
781
|
+
// Check cache first
|
|
782
|
+
if (useCache && fs.existsSync(cachePath)) {
|
|
783
|
+
try {
|
|
784
|
+
const cached = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
|
|
785
|
+
const cacheAge = Date.now() - new Date(cached.fetchedAt).getTime();
|
|
786
|
+
|
|
787
|
+
if (cacheAge < config.cacheExpiry) {
|
|
788
|
+
const filteredPatterns = filterPatterns(cached.patterns, { category, minConfidence, limit });
|
|
789
|
+
return {
|
|
790
|
+
success: true,
|
|
791
|
+
fromCache: true,
|
|
792
|
+
cacheAge: Math.round(cacheAge / 1000 / 60), // Minutes
|
|
793
|
+
patterns: filteredPatterns,
|
|
794
|
+
totalAvailable: cached.patterns.length,
|
|
795
|
+
fetchedAt: cached.fetchedAt
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
} catch {
|
|
799
|
+
// Cache invalid, fetch fresh
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Fetch fresh patterns
|
|
804
|
+
try {
|
|
805
|
+
// In production, this would be:
|
|
806
|
+
// const response = await fetch(`${config.apiEndpoint}/patterns`, {
|
|
807
|
+
// method: 'GET',
|
|
808
|
+
// headers: { 'Authorization': `Bearer ${config.apiKey}` }
|
|
809
|
+
// });
|
|
810
|
+
// const data = await response.json();
|
|
811
|
+
|
|
812
|
+
// For now, combine local community insights with simulated external data
|
|
813
|
+
const patterns = await fetchPatternsFromSources(config);
|
|
814
|
+
|
|
815
|
+
// Cache the results
|
|
816
|
+
ensureDirectories();
|
|
817
|
+
const cacheData = {
|
|
818
|
+
fetchedAt: new Date().toISOString(),
|
|
819
|
+
patterns
|
|
820
|
+
};
|
|
821
|
+
fs.writeFileSync(cachePath, JSON.stringify(cacheData, null, 2));
|
|
822
|
+
|
|
823
|
+
const filteredPatterns = filterPatterns(patterns, { category, minConfidence, limit });
|
|
824
|
+
|
|
825
|
+
return {
|
|
826
|
+
success: true,
|
|
827
|
+
fromCache: false,
|
|
828
|
+
patterns: filteredPatterns,
|
|
829
|
+
totalAvailable: patterns.length,
|
|
830
|
+
fetchedAt: cacheData.fetchedAt
|
|
831
|
+
};
|
|
832
|
+
} catch (error) {
|
|
833
|
+
// Fall back to built-in community insights
|
|
834
|
+
const filteredInsights = filterPatterns(
|
|
835
|
+
Object.values(COMMUNITY_INSIGHTS).flat(),
|
|
836
|
+
{ category, minConfidence, limit }
|
|
837
|
+
);
|
|
838
|
+
|
|
839
|
+
return {
|
|
840
|
+
success: true,
|
|
841
|
+
fromCache: false,
|
|
842
|
+
fallback: true,
|
|
843
|
+
patterns: filteredInsights,
|
|
844
|
+
totalAvailable: filteredInsights.length,
|
|
845
|
+
error: error.message
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Fetch patterns from available sources
|
|
852
|
+
*/
|
|
853
|
+
async function fetchPatternsFromSources(config) {
|
|
854
|
+
const allPatterns = [];
|
|
855
|
+
|
|
856
|
+
// Add built-in community insights
|
|
857
|
+
for (const [source, patterns] of Object.entries(COMMUNITY_INSIGHTS)) {
|
|
858
|
+
for (const pattern of patterns) {
|
|
859
|
+
allPatterns.push({
|
|
860
|
+
...pattern,
|
|
861
|
+
source: 'community',
|
|
862
|
+
sourceType: source
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// In production, fetch from API and merge
|
|
868
|
+
|
|
869
|
+
return allPatterns;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Filter patterns based on criteria
|
|
874
|
+
*/
|
|
875
|
+
function filterPatterns(patterns, options) {
|
|
876
|
+
const { category, minConfidence, limit } = options;
|
|
877
|
+
|
|
878
|
+
let filtered = [...patterns];
|
|
879
|
+
|
|
880
|
+
if (category) {
|
|
881
|
+
filtered = filtered.filter(p =>
|
|
882
|
+
p.category === category ||
|
|
883
|
+
p.type === category ||
|
|
884
|
+
p.sourceType === category
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
if (minConfidence) {
|
|
889
|
+
filtered = filtered.filter(p => (p.confidence || 0.5) >= minConfidence);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Sort by confidence and sample size
|
|
893
|
+
filtered.sort((a, b) => {
|
|
894
|
+
const scoreA = (a.confidence || 0.5) * Math.log10((a.sampleSize || 1) + 1);
|
|
895
|
+
const scoreB = (b.confidence || 0.5) * Math.log10((b.sampleSize || 1) + 1);
|
|
896
|
+
return scoreB - scoreA;
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
return filtered.slice(0, limit);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* Apply relevant patterns to the current project context
|
|
904
|
+
* Matches community patterns against project needs and provides recommendations
|
|
905
|
+
*
|
|
906
|
+
* @param {object} projectContext - Current project context
|
|
907
|
+
* @param {string} projectContext.framework - Framework being used
|
|
908
|
+
* @param {string} projectContext.database - Database being used
|
|
909
|
+
* @param {string} projectContext.phase - Current project phase
|
|
910
|
+
* @param {Array<string>} projectContext.features - Features being developed
|
|
911
|
+
* @param {Array<string>} projectContext.challenges - Current challenges
|
|
912
|
+
* @param {object} options - Application options
|
|
913
|
+
* @param {number} options.maxPatterns - Maximum patterns to return (default: 10)
|
|
914
|
+
* @param {number} options.minRelevance - Minimum relevance score (default: 0.3)
|
|
915
|
+
* @returns {Promise<object>} Relevant patterns with recommendations
|
|
916
|
+
*/
|
|
917
|
+
async function applyRelevantPatterns(projectContext, options = {}) {
|
|
918
|
+
const {
|
|
919
|
+
maxPatterns = 10,
|
|
920
|
+
minRelevance = 0.3
|
|
921
|
+
} = options;
|
|
922
|
+
|
|
923
|
+
// Fetch community patterns
|
|
924
|
+
const communityResult = await fetchCommunityPatterns({ useCache: true });
|
|
925
|
+
|
|
926
|
+
if (!communityResult.success) {
|
|
927
|
+
return {
|
|
928
|
+
success: false,
|
|
929
|
+
error: 'Failed to fetch community patterns',
|
|
930
|
+
details: communityResult.error
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const patterns = communityResult.patterns;
|
|
935
|
+
|
|
936
|
+
// Calculate relevance for each pattern
|
|
937
|
+
const scoredPatterns = patterns.map(pattern => ({
|
|
938
|
+
pattern,
|
|
939
|
+
relevance: calculatePatternRelevance(pattern, projectContext),
|
|
940
|
+
applicability: determineApplicability(pattern, projectContext)
|
|
941
|
+
}));
|
|
942
|
+
|
|
943
|
+
// Filter and sort by relevance
|
|
944
|
+
const relevantPatterns = scoredPatterns
|
|
945
|
+
.filter(sp => sp.relevance >= minRelevance)
|
|
946
|
+
.sort((a, b) => b.relevance - a.relevance)
|
|
947
|
+
.slice(0, maxPatterns);
|
|
948
|
+
|
|
949
|
+
// Group by category for easier consumption
|
|
950
|
+
const groupedPatterns = groupPatternsByCategory(relevantPatterns);
|
|
951
|
+
|
|
952
|
+
// Generate recommendations based on patterns
|
|
953
|
+
const recommendations = generatePatternRecommendations(relevantPatterns, projectContext);
|
|
954
|
+
|
|
955
|
+
return {
|
|
956
|
+
success: true,
|
|
957
|
+
projectContext: {
|
|
958
|
+
framework: projectContext.framework,
|
|
959
|
+
database: projectContext.database,
|
|
960
|
+
phase: projectContext.phase
|
|
961
|
+
},
|
|
962
|
+
totalPatternsAnalyzed: patterns.length,
|
|
963
|
+
relevantPatterns: relevantPatterns.map(sp => ({
|
|
964
|
+
...sp.pattern,
|
|
965
|
+
relevanceScore: Math.round(sp.relevance * 100) / 100,
|
|
966
|
+
applicability: sp.applicability
|
|
967
|
+
})),
|
|
968
|
+
groupedByCategory: groupedPatterns,
|
|
969
|
+
recommendations,
|
|
970
|
+
appliedAt: new Date().toISOString()
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
/**
|
|
975
|
+
* Calculate relevance of a pattern to the project context
|
|
976
|
+
*/
|
|
977
|
+
function calculatePatternRelevance(pattern, context) {
|
|
978
|
+
let score = 0.5; // Base relevance
|
|
979
|
+
|
|
980
|
+
// Category/type matching
|
|
981
|
+
const category = pattern.category || pattern.type || '';
|
|
982
|
+
|
|
983
|
+
// Check feature alignment
|
|
984
|
+
if (context.features) {
|
|
985
|
+
for (const feature of context.features) {
|
|
986
|
+
const featureLower = feature.toLowerCase();
|
|
987
|
+
if (category.includes(featureLower) || (pattern.insight || '').toLowerCase().includes(featureLower)) {
|
|
988
|
+
score += 0.15;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Check challenge alignment
|
|
994
|
+
if (context.challenges) {
|
|
995
|
+
for (const challenge of context.challenges) {
|
|
996
|
+
const challengeLower = challenge.toLowerCase();
|
|
997
|
+
if ((pattern.insight || '').toLowerCase().includes(challengeLower)) {
|
|
998
|
+
score += 0.2;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Phase relevance
|
|
1004
|
+
if (context.phase) {
|
|
1005
|
+
const phaseRelevance = {
|
|
1006
|
+
planning: ['architecture', 'database', 'api'],
|
|
1007
|
+
development: ['testing', 'api', 'feature'],
|
|
1008
|
+
testing: ['testing', 'security'],
|
|
1009
|
+
deployment: ['deployment', 'security', 'performance'],
|
|
1010
|
+
maintenance: ['performance', 'security', 'testing']
|
|
1011
|
+
};
|
|
1012
|
+
|
|
1013
|
+
const relevantCategories = phaseRelevance[context.phase] || [];
|
|
1014
|
+
if (relevantCategories.some(c => category.includes(c))) {
|
|
1015
|
+
score += 0.15;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// Confidence bonus
|
|
1020
|
+
score += (pattern.confidence || 0.5) * 0.1;
|
|
1021
|
+
|
|
1022
|
+
// Sample size bonus (capped)
|
|
1023
|
+
score += Math.min((pattern.sampleSize || 0) / 10000, 0.1);
|
|
1024
|
+
|
|
1025
|
+
return Math.min(score, 1.0);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* Determine how applicable a pattern is to the current context
|
|
1030
|
+
*/
|
|
1031
|
+
function determineApplicability(pattern, context) {
|
|
1032
|
+
const factors = [];
|
|
1033
|
+
|
|
1034
|
+
// Framework match
|
|
1035
|
+
if (context.framework && pattern.insight) {
|
|
1036
|
+
const insightLower = pattern.insight.toLowerCase();
|
|
1037
|
+
if (insightLower.includes(context.framework.toLowerCase())) {
|
|
1038
|
+
factors.push('framework_match');
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// Database match
|
|
1043
|
+
if (context.database && (pattern.category === 'database' || pattern.type === 'database')) {
|
|
1044
|
+
factors.push('database_relevant');
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// High confidence
|
|
1048
|
+
if ((pattern.confidence || 0) >= 0.85) {
|
|
1049
|
+
factors.push('high_confidence');
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// Large sample size
|
|
1053
|
+
if ((pattern.sampleSize || 0) >= 500) {
|
|
1054
|
+
factors.push('well_validated');
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
return {
|
|
1058
|
+
score: factors.length / 4, // Normalize to 0-1
|
|
1059
|
+
factors
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/**
|
|
1064
|
+
* Group patterns by category
|
|
1065
|
+
*/
|
|
1066
|
+
function groupPatternsByCategory(scoredPatterns) {
|
|
1067
|
+
const groups = {};
|
|
1068
|
+
|
|
1069
|
+
for (const sp of scoredPatterns) {
|
|
1070
|
+
const category = sp.pattern.category || sp.pattern.type || 'general';
|
|
1071
|
+
if (!groups[category]) {
|
|
1072
|
+
groups[category] = [];
|
|
1073
|
+
}
|
|
1074
|
+
groups[category].push({
|
|
1075
|
+
insight: sp.pattern.insight,
|
|
1076
|
+
confidence: sp.pattern.confidence,
|
|
1077
|
+
relevance: Math.round(sp.relevance * 100) / 100
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
return groups;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/**
|
|
1085
|
+
* Generate actionable recommendations from patterns
|
|
1086
|
+
*/
|
|
1087
|
+
function generatePatternRecommendations(scoredPatterns, context) {
|
|
1088
|
+
const recommendations = [];
|
|
1089
|
+
|
|
1090
|
+
// High priority: High relevance + high confidence patterns
|
|
1091
|
+
const highPriority = scoredPatterns.filter(
|
|
1092
|
+
sp => sp.relevance >= 0.7 && (sp.pattern.confidence || 0) >= 0.85
|
|
1093
|
+
);
|
|
1094
|
+
|
|
1095
|
+
for (const sp of highPriority.slice(0, 3)) {
|
|
1096
|
+
recommendations.push({
|
|
1097
|
+
priority: 'high',
|
|
1098
|
+
category: sp.pattern.category || sp.pattern.type,
|
|
1099
|
+
insight: sp.pattern.insight,
|
|
1100
|
+
confidence: sp.pattern.confidence,
|
|
1101
|
+
action: generateActionFromInsight(sp.pattern, context),
|
|
1102
|
+
source: `Based on ${sp.pattern.sampleSize || 'community'} projects`
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// Medium priority: Good relevance patterns
|
|
1107
|
+
const mediumPriority = scoredPatterns.filter(
|
|
1108
|
+
sp => sp.relevance >= 0.5 && sp.relevance < 0.7
|
|
1109
|
+
);
|
|
1110
|
+
|
|
1111
|
+
for (const sp of mediumPriority.slice(0, 3)) {
|
|
1112
|
+
recommendations.push({
|
|
1113
|
+
priority: 'medium',
|
|
1114
|
+
category: sp.pattern.category || sp.pattern.type,
|
|
1115
|
+
insight: sp.pattern.insight,
|
|
1116
|
+
confidence: sp.pattern.confidence,
|
|
1117
|
+
action: generateActionFromInsight(sp.pattern, context)
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// Add phase-specific recommendation if available
|
|
1122
|
+
if (context.phase) {
|
|
1123
|
+
const phasePatterns = scoredPatterns.filter(
|
|
1124
|
+
sp => determineApplicability(sp.pattern, context).factors.includes('high_confidence')
|
|
1125
|
+
);
|
|
1126
|
+
|
|
1127
|
+
if (phasePatterns.length > 0) {
|
|
1128
|
+
recommendations.push({
|
|
1129
|
+
priority: 'info',
|
|
1130
|
+
category: 'phase_guidance',
|
|
1131
|
+
insight: `For the ${context.phase} phase, community data suggests focusing on: ${phasePatterns.map(p => p.pattern.category).join(', ')}`,
|
|
1132
|
+
action: 'Review patterns in these categories for your current phase'
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
return recommendations;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
/**
|
|
1141
|
+
* Generate an actionable suggestion from a pattern insight
|
|
1142
|
+
*/
|
|
1143
|
+
function generateActionFromInsight(pattern, context) {
|
|
1144
|
+
const insight = pattern.insight || '';
|
|
1145
|
+
|
|
1146
|
+
// Extract percentages and metrics from insights
|
|
1147
|
+
const percentMatch = insight.match(/(\d+(?:\.\d+)?)\s*%/);
|
|
1148
|
+
const metric = percentMatch ? percentMatch[1] : null;
|
|
1149
|
+
|
|
1150
|
+
// Generate action based on category
|
|
1151
|
+
const category = pattern.category || pattern.type || 'general';
|
|
1152
|
+
|
|
1153
|
+
const actionTemplates = {
|
|
1154
|
+
database: metric
|
|
1155
|
+
? `Consider implementing this pattern to potentially achieve ${metric}% improvement`
|
|
1156
|
+
: 'Review your database practices against this community learning',
|
|
1157
|
+
security: 'Implement this security pattern early in development',
|
|
1158
|
+
testing: 'Add this testing practice to your workflow',
|
|
1159
|
+
performance: 'Apply this optimization during your performance review',
|
|
1160
|
+
deployment: 'Include this in your deployment checklist',
|
|
1161
|
+
api: 'Review your API design against this pattern'
|
|
1162
|
+
};
|
|
1163
|
+
|
|
1164
|
+
return actionTemplates[category] || 'Consider applying this community-learned pattern';
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
/**
|
|
1168
|
+
* Load contributions (data we've shared)
|
|
1169
|
+
*/
|
|
1170
|
+
function loadContributions() {
|
|
1171
|
+
const paths = getPaths();
|
|
1172
|
+
if (fs.existsSync(paths.contributionsPath)) {
|
|
1173
|
+
try {
|
|
1174
|
+
return JSON.parse(fs.readFileSync(paths.contributionsPath, 'utf8'));
|
|
1175
|
+
} catch {
|
|
1176
|
+
return { contributions: [], count: 0 };
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
return { contributions: [], count: 0 };
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/**
|
|
1183
|
+
* Save contributions
|
|
1184
|
+
*/
|
|
1185
|
+
function saveContributions(contributions) {
|
|
1186
|
+
const paths = getPaths();
|
|
1187
|
+
ensureDirectories();
|
|
1188
|
+
contributions.lastUpdated = new Date().toISOString();
|
|
1189
|
+
fs.writeFileSync(paths.contributionsPath, JSON.stringify(contributions, null, 2));
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
/**
|
|
1193
|
+
* Contribute a pattern to cross-project learning
|
|
1194
|
+
* @param {object} pattern - Pattern to contribute
|
|
1195
|
+
*/
|
|
1196
|
+
function contributePattern(pattern) {
|
|
1197
|
+
if (!hasConsent()) {
|
|
1198
|
+
return { success: false, error: 'Consent not granted for cross-project learning' };
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
const consent = loadConsent();
|
|
1202
|
+
if (!consent.sharePatterns) {
|
|
1203
|
+
return { success: false, error: 'Pattern sharing not enabled' };
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
const contributions = loadContributions();
|
|
1207
|
+
const anonymizedPattern = anonymize(pattern);
|
|
1208
|
+
|
|
1209
|
+
const contribution = {
|
|
1210
|
+
id: `pattern_${Date.now()}_${Math.random().toString(36).substring(7)}`,
|
|
1211
|
+
type: 'pattern',
|
|
1212
|
+
projectId: consent.projectId,
|
|
1213
|
+
data: anonymizedPattern,
|
|
1214
|
+
contributedAt: new Date().toISOString()
|
|
1215
|
+
};
|
|
1216
|
+
|
|
1217
|
+
contributions.contributions.push(contribution);
|
|
1218
|
+
contributions.count++;
|
|
1219
|
+
saveContributions(contributions);
|
|
1220
|
+
|
|
1221
|
+
return {
|
|
1222
|
+
success: true,
|
|
1223
|
+
contributionId: contribution.id,
|
|
1224
|
+
type: 'pattern',
|
|
1225
|
+
message: 'Pattern contributed to cross-project learning'
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
/**
|
|
1230
|
+
* Contribute a decision outcome to cross-project learning
|
|
1231
|
+
* @param {object} decision - Decision with outcome
|
|
1232
|
+
*/
|
|
1233
|
+
function contributeDecision(decision) {
|
|
1234
|
+
if (!hasConsent()) {
|
|
1235
|
+
return { success: false, error: 'Consent not granted for cross-project learning' };
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
const consent = loadConsent();
|
|
1239
|
+
if (!consent.shareDecisions) {
|
|
1240
|
+
return { success: false, error: 'Decision sharing not enabled' };
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
const contributions = loadContributions();
|
|
1244
|
+
const anonymizedDecision = anonymize(decision);
|
|
1245
|
+
|
|
1246
|
+
// Extract only relevant learning data
|
|
1247
|
+
const learningData = {
|
|
1248
|
+
type: anonymizedDecision.type,
|
|
1249
|
+
category: anonymizedDecision.category,
|
|
1250
|
+
outcome: anonymizedDecision.outcome,
|
|
1251
|
+
confidence: anonymizedDecision.confidence,
|
|
1252
|
+
impactScore: anonymizedDecision.impactScore,
|
|
1253
|
+
stack: anonymizedDecision.stack
|
|
1254
|
+
};
|
|
1255
|
+
|
|
1256
|
+
const contribution = {
|
|
1257
|
+
id: `decision_${Date.now()}_${Math.random().toString(36).substring(7)}`,
|
|
1258
|
+
type: 'decision',
|
|
1259
|
+
projectId: consent.projectId,
|
|
1260
|
+
data: learningData,
|
|
1261
|
+
contributedAt: new Date().toISOString()
|
|
1262
|
+
};
|
|
1263
|
+
|
|
1264
|
+
contributions.contributions.push(contribution);
|
|
1265
|
+
contributions.count++;
|
|
1266
|
+
saveContributions(contributions);
|
|
1267
|
+
|
|
1268
|
+
return {
|
|
1269
|
+
success: true,
|
|
1270
|
+
contributionId: contribution.id,
|
|
1271
|
+
type: 'decision',
|
|
1272
|
+
message: 'Decision outcome contributed to cross-project learning'
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
/**
|
|
1277
|
+
* Contribute workflow completion to cross-project learning
|
|
1278
|
+
* @param {object} workflow - Completed workflow data
|
|
1279
|
+
*/
|
|
1280
|
+
function contributeWorkflow(workflow) {
|
|
1281
|
+
if (!hasConsent()) {
|
|
1282
|
+
return { success: false, error: 'Consent not granted for cross-project learning' };
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
const consent = loadConsent();
|
|
1286
|
+
if (!consent.shareWorkflows) {
|
|
1287
|
+
return { success: false, error: 'Workflow sharing not enabled' };
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
const contributions = loadContributions();
|
|
1291
|
+
|
|
1292
|
+
// Extract only relevant workflow data
|
|
1293
|
+
const workflowData = {
|
|
1294
|
+
workflowName: workflow.name,
|
|
1295
|
+
phases: workflow.phases?.length,
|
|
1296
|
+
completedPhases: workflow.completedPhases,
|
|
1297
|
+
duration: workflow.duration,
|
|
1298
|
+
signalsCompleted: workflow.signalsCompleted,
|
|
1299
|
+
tier: workflow.tier,
|
|
1300
|
+
success: workflow.success
|
|
1301
|
+
};
|
|
1302
|
+
|
|
1303
|
+
const contribution = {
|
|
1304
|
+
id: `workflow_${Date.now()}_${Math.random().toString(36).substring(7)}`,
|
|
1305
|
+
type: 'workflow',
|
|
1306
|
+
projectId: consent.projectId,
|
|
1307
|
+
data: workflowData,
|
|
1308
|
+
contributedAt: new Date().toISOString()
|
|
1309
|
+
};
|
|
1310
|
+
|
|
1311
|
+
contributions.contributions.push(contribution);
|
|
1312
|
+
contributions.count++;
|
|
1313
|
+
saveContributions(contributions);
|
|
1314
|
+
|
|
1315
|
+
return {
|
|
1316
|
+
success: true,
|
|
1317
|
+
contributionId: contribution.id,
|
|
1318
|
+
type: 'workflow',
|
|
1319
|
+
message: 'Workflow completion contributed to cross-project learning'
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
/**
|
|
1324
|
+
* Get contribution statistics
|
|
1325
|
+
*/
|
|
1326
|
+
function getContributionStats() {
|
|
1327
|
+
const contributions = loadContributions();
|
|
1328
|
+
|
|
1329
|
+
const byType = contributions.contributions.reduce((acc, c) => {
|
|
1330
|
+
acc[c.type] = (acc[c.type] || 0) + 1;
|
|
1331
|
+
return acc;
|
|
1332
|
+
}, {});
|
|
1333
|
+
|
|
1334
|
+
return {
|
|
1335
|
+
totalContributions: contributions.count,
|
|
1336
|
+
byType,
|
|
1337
|
+
lastContribution: contributions.contributions[contributions.contributions.length - 1]?.contributedAt || null
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
/**
|
|
1342
|
+
* Load received community learnings
|
|
1343
|
+
*/
|
|
1344
|
+
function loadReceived() {
|
|
1345
|
+
const paths = getPaths();
|
|
1346
|
+
if (fs.existsSync(paths.receivedPath)) {
|
|
1347
|
+
try {
|
|
1348
|
+
return JSON.parse(fs.readFileSync(paths.receivedPath, 'utf8'));
|
|
1349
|
+
} catch {
|
|
1350
|
+
return { learnings: [], lastFetch: null };
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
return { learnings: [], lastFetch: null };
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
/**
|
|
1357
|
+
* Save received learnings
|
|
1358
|
+
*/
|
|
1359
|
+
function saveReceived(received) {
|
|
1360
|
+
const paths = getPaths();
|
|
1361
|
+
ensureDirectories();
|
|
1362
|
+
received.lastUpdated = new Date().toISOString();
|
|
1363
|
+
fs.writeFileSync(paths.receivedPath, JSON.stringify(received, null, 2));
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
/**
|
|
1367
|
+
* Aggregated community insights (simulated)
|
|
1368
|
+
* In a full implementation, this would come from a central service
|
|
1369
|
+
*/
|
|
1370
|
+
const COMMUNITY_INSIGHTS = {
|
|
1371
|
+
patterns: [
|
|
1372
|
+
{
|
|
1373
|
+
id: 'insight_001',
|
|
1374
|
+
type: 'pattern',
|
|
1375
|
+
category: 'database',
|
|
1376
|
+
insight: 'Using database migrations reduces schema-related deployment failures by 85%',
|
|
1377
|
+
confidence: 0.92,
|
|
1378
|
+
sampleSize: 1250,
|
|
1379
|
+
updatedAt: '2026-02-01'
|
|
1380
|
+
},
|
|
1381
|
+
{
|
|
1382
|
+
id: 'insight_002',
|
|
1383
|
+
type: 'pattern',
|
|
1384
|
+
category: 'testing',
|
|
1385
|
+
insight: 'Projects with >80% test coverage have 60% fewer production bugs',
|
|
1386
|
+
confidence: 0.88,
|
|
1387
|
+
sampleSize: 890,
|
|
1388
|
+
updatedAt: '2026-02-01'
|
|
1389
|
+
},
|
|
1390
|
+
{
|
|
1391
|
+
id: 'insight_003',
|
|
1392
|
+
type: 'pattern',
|
|
1393
|
+
category: 'security',
|
|
1394
|
+
insight: 'Early security audits catch 3x more vulnerabilities than pre-launch audits',
|
|
1395
|
+
confidence: 0.95,
|
|
1396
|
+
sampleSize: 560,
|
|
1397
|
+
updatedAt: '2026-02-01'
|
|
1398
|
+
}
|
|
1399
|
+
],
|
|
1400
|
+
decisions: [
|
|
1401
|
+
{
|
|
1402
|
+
id: 'insight_004',
|
|
1403
|
+
type: 'decision',
|
|
1404
|
+
category: 'auth',
|
|
1405
|
+
insight: 'OAuth providers reduce auth-related support tickets by 70%',
|
|
1406
|
+
confidence: 0.85,
|
|
1407
|
+
sampleSize: 720,
|
|
1408
|
+
updatedAt: '2026-02-01'
|
|
1409
|
+
},
|
|
1410
|
+
{
|
|
1411
|
+
id: 'insight_005',
|
|
1412
|
+
type: 'decision',
|
|
1413
|
+
category: 'payments',
|
|
1414
|
+
insight: 'Stripe integration with webhooks has 99.2% success rate',
|
|
1415
|
+
confidence: 0.94,
|
|
1416
|
+
sampleSize: 450,
|
|
1417
|
+
updatedAt: '2026-02-01'
|
|
1418
|
+
}
|
|
1419
|
+
],
|
|
1420
|
+
workflows: [
|
|
1421
|
+
{
|
|
1422
|
+
id: 'insight_006',
|
|
1423
|
+
type: 'workflow',
|
|
1424
|
+
workflow: 'feature-development',
|
|
1425
|
+
insight: 'Feature development workflow average completion: 4.2 days',
|
|
1426
|
+
confidence: 0.90,
|
|
1427
|
+
sampleSize: 1800,
|
|
1428
|
+
updatedAt: '2026-02-01'
|
|
1429
|
+
},
|
|
1430
|
+
{
|
|
1431
|
+
id: 'insight_007',
|
|
1432
|
+
type: 'workflow',
|
|
1433
|
+
workflow: 'security-audit',
|
|
1434
|
+
insight: 'Security audits find average 2.3 vulnerabilities per project',
|
|
1435
|
+
confidence: 0.87,
|
|
1436
|
+
sampleSize: 340,
|
|
1437
|
+
updatedAt: '2026-02-01'
|
|
1438
|
+
}
|
|
1439
|
+
]
|
|
1440
|
+
};
|
|
1441
|
+
|
|
1442
|
+
/**
|
|
1443
|
+
* Fetch community insights
|
|
1444
|
+
* In a full implementation, this would call a central service
|
|
1445
|
+
*/
|
|
1446
|
+
function fetchCommunityInsights() {
|
|
1447
|
+
if (!hasConsent()) {
|
|
1448
|
+
// Still allow viewing public insights without contributing
|
|
1449
|
+
return {
|
|
1450
|
+
success: true,
|
|
1451
|
+
insights: COMMUNITY_INSIGHTS,
|
|
1452
|
+
message: 'Viewing public insights (enable sharing to contribute)'
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
const received = loadReceived();
|
|
1457
|
+
received.learnings = COMMUNITY_INSIGHTS;
|
|
1458
|
+
received.lastFetch = new Date().toISOString();
|
|
1459
|
+
saveReceived(received);
|
|
1460
|
+
|
|
1461
|
+
return {
|
|
1462
|
+
success: true,
|
|
1463
|
+
insights: COMMUNITY_INSIGHTS,
|
|
1464
|
+
totalInsights: Object.values(COMMUNITY_INSIGHTS).flat().length,
|
|
1465
|
+
message: 'Community insights fetched'
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
/**
|
|
1470
|
+
* Get insights relevant to a specific category or context
|
|
1471
|
+
* @param {string} category - Category to filter by
|
|
1472
|
+
*/
|
|
1473
|
+
function getRelevantInsights(category) {
|
|
1474
|
+
const allInsights = [
|
|
1475
|
+
...COMMUNITY_INSIGHTS.patterns,
|
|
1476
|
+
...COMMUNITY_INSIGHTS.decisions,
|
|
1477
|
+
...COMMUNITY_INSIGHTS.workflows
|
|
1478
|
+
];
|
|
1479
|
+
|
|
1480
|
+
if (!category) {
|
|
1481
|
+
return allInsights;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
return allInsights.filter(insight =>
|
|
1485
|
+
insight.category === category ||
|
|
1486
|
+
insight.workflow === category ||
|
|
1487
|
+
insight.type === category
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
/**
|
|
1492
|
+
* Apply community insights to local recommendations
|
|
1493
|
+
* @param {object} recommendation - Local recommendation
|
|
1494
|
+
*/
|
|
1495
|
+
function enhanceWithCommunityInsights(recommendation) {
|
|
1496
|
+
if (!recommendation || !recommendation.type) {
|
|
1497
|
+
return recommendation;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
const relevantInsights = getRelevantInsights(recommendation.type);
|
|
1501
|
+
|
|
1502
|
+
if (relevantInsights.length === 0) {
|
|
1503
|
+
return recommendation;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// Add community context to recommendation
|
|
1507
|
+
return {
|
|
1508
|
+
...recommendation,
|
|
1509
|
+
communityInsights: relevantInsights.slice(0, 3),
|
|
1510
|
+
communityConfidence: relevantInsights.reduce((sum, i) => sum + i.confidence, 0) / relevantInsights.length
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
/**
|
|
1515
|
+
* Get cross-project learning status
|
|
1516
|
+
*/
|
|
1517
|
+
function getStatus() {
|
|
1518
|
+
const consent = getConsentStatus();
|
|
1519
|
+
const contributions = getContributionStats();
|
|
1520
|
+
|
|
1521
|
+
return {
|
|
1522
|
+
enabled: consent.active,
|
|
1523
|
+
consent,
|
|
1524
|
+
contributions: consent.active ? contributions : null,
|
|
1525
|
+
communityInsights: Object.values(COMMUNITY_INSIGHTS).flat().length
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
/**
|
|
1530
|
+
* Export data for transparency
|
|
1531
|
+
* Returns all data that would be/has been shared
|
|
1532
|
+
*/
|
|
1533
|
+
function exportMyData() {
|
|
1534
|
+
const consent = loadConsent();
|
|
1535
|
+
const contributions = loadContributions();
|
|
1536
|
+
|
|
1537
|
+
return {
|
|
1538
|
+
consent: {
|
|
1539
|
+
projectId: consent.projectId,
|
|
1540
|
+
givenAt: consent.givenAt,
|
|
1541
|
+
sharing: {
|
|
1542
|
+
patterns: consent.sharePatterns,
|
|
1543
|
+
decisions: consent.shareDecisions,
|
|
1544
|
+
workflows: consent.shareWorkflows
|
|
1545
|
+
}
|
|
1546
|
+
},
|
|
1547
|
+
contributions: contributions.contributions.map(c => ({
|
|
1548
|
+
id: c.id,
|
|
1549
|
+
type: c.type,
|
|
1550
|
+
contributedAt: c.contributedAt,
|
|
1551
|
+
data: c.data
|
|
1552
|
+
})),
|
|
1553
|
+
totalContributions: contributions.count
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
/**
|
|
1558
|
+
* Delete all shared data (right to be forgotten)
|
|
1559
|
+
*/
|
|
1560
|
+
function deleteMyData() {
|
|
1561
|
+
const paths = getPaths();
|
|
1562
|
+
|
|
1563
|
+
// Revoke consent
|
|
1564
|
+
revokeConsent();
|
|
1565
|
+
|
|
1566
|
+
// Clear local contribution records
|
|
1567
|
+
if (fs.existsSync(paths.contributionsPath)) {
|
|
1568
|
+
fs.unlinkSync(paths.contributionsPath);
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
// In a full implementation, this would also:
|
|
1572
|
+
// 1. Call the central service to delete server-side data
|
|
1573
|
+
// 2. Request removal from aggregated statistics
|
|
1574
|
+
|
|
1575
|
+
return {
|
|
1576
|
+
success: true,
|
|
1577
|
+
message: 'All cross-project learning data deleted. Consent revoked.',
|
|
1578
|
+
note: 'Your contributions have been removed from local storage.'
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
module.exports = {
|
|
1583
|
+
// ============================================================================
|
|
1584
|
+
// MAIN CROSS-PROJECT LEARNING API
|
|
1585
|
+
// ============================================================================
|
|
1586
|
+
|
|
1587
|
+
// Core learning functions (the 4 main APIs)
|
|
1588
|
+
extractAnonymizedLearnings, // Extract learnings without sensitive data
|
|
1589
|
+
shareLearnings, // Upload to shared pool (async)
|
|
1590
|
+
fetchCommunityPatterns, // Download community patterns (async)
|
|
1591
|
+
applyRelevantPatterns, // Apply relevant patterns to project context (async)
|
|
1592
|
+
|
|
1593
|
+
// ============================================================================
|
|
1594
|
+
// CONSENT & CONFIGURATION
|
|
1595
|
+
// ============================================================================
|
|
1596
|
+
|
|
1597
|
+
// Consent management
|
|
1598
|
+
grantConsent,
|
|
1599
|
+
revokeConsent,
|
|
1600
|
+
hasConsent,
|
|
1601
|
+
getConsentStatus,
|
|
1602
|
+
|
|
1603
|
+
// Configuration
|
|
1604
|
+
getConfig,
|
|
1605
|
+
loadConfig,
|
|
1606
|
+
|
|
1607
|
+
// ============================================================================
|
|
1608
|
+
// DATA HANDLING
|
|
1609
|
+
// ============================================================================
|
|
1610
|
+
|
|
1611
|
+
// Data anonymization
|
|
1612
|
+
anonymize,
|
|
1613
|
+
anonymizeString,
|
|
1614
|
+
|
|
1615
|
+
// Contributing individual items
|
|
1616
|
+
contributePattern,
|
|
1617
|
+
contributeDecision,
|
|
1618
|
+
contributeWorkflow,
|
|
1619
|
+
getContributionStats,
|
|
1620
|
+
|
|
1621
|
+
// ============================================================================
|
|
1622
|
+
// COMMUNITY INSIGHTS
|
|
1623
|
+
// ============================================================================
|
|
1624
|
+
|
|
1625
|
+
// Receiving insights (legacy API, prefer fetchCommunityPatterns)
|
|
1626
|
+
fetchCommunityInsights,
|
|
1627
|
+
getRelevantInsights,
|
|
1628
|
+
enhanceWithCommunityInsights,
|
|
1629
|
+
|
|
1630
|
+
// ============================================================================
|
|
1631
|
+
// STATUS & TRANSPARENCY
|
|
1632
|
+
// ============================================================================
|
|
1633
|
+
|
|
1634
|
+
// Status and data management
|
|
1635
|
+
getStatus,
|
|
1636
|
+
exportMyData,
|
|
1637
|
+
deleteMyData,
|
|
1638
|
+
|
|
1639
|
+
// ============================================================================
|
|
1640
|
+
// CONSTANTS
|
|
1641
|
+
// ============================================================================
|
|
1642
|
+
|
|
1643
|
+
CROSS_PROJECT_CONFIG,
|
|
1644
|
+
COMMUNITY_INSIGHTS,
|
|
1645
|
+
SENSITIVE_FIELDS,
|
|
1646
|
+
PROJECT_SPECIFIC_PATTERNS
|
|
1647
|
+
};
|