@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
|
@@ -15,7 +15,18 @@ const crypto = require('crypto');
|
|
|
15
15
|
// Paths
|
|
16
16
|
const memoryDir = __dirname;
|
|
17
17
|
const decisionsFile = path.join(memoryDir, 'decisions.jsonl');
|
|
18
|
+
const outcomesFile = path.join(memoryDir, 'outcomes.jsonl');
|
|
18
19
|
const patternsFile = path.join(memoryDir, 'patterns.json');
|
|
20
|
+
const indexFile = path.join(memoryDir, 'decisions.index.json');
|
|
21
|
+
|
|
22
|
+
// Compaction thresholds
|
|
23
|
+
const COMPACTION_THRESHOLD_COUNT = 100; // Compact after this many outcomes
|
|
24
|
+
const COMPACTION_THRESHOLD_BYTES = 1024 * 1024; // Compact when outcomes file exceeds 1MB
|
|
25
|
+
|
|
26
|
+
// In-memory decision index for O(1) lookups
|
|
27
|
+
// Maps decisionId -> { lineOffset, timestamp, type }
|
|
28
|
+
let decisionIndex = null;
|
|
29
|
+
let indexDirty = false;
|
|
19
30
|
|
|
20
31
|
/**
|
|
21
32
|
* Decision types that can be tracked
|
|
@@ -75,6 +86,258 @@ function ensureDecisionsFile() {
|
|
|
75
86
|
}
|
|
76
87
|
}
|
|
77
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Load the decision index from disk or rebuild from decisions file
|
|
91
|
+
* The index provides O(1) lookups for decision existence checks
|
|
92
|
+
* @returns {Map} The decision index
|
|
93
|
+
*/
|
|
94
|
+
function loadDecisionIndex() {
|
|
95
|
+
if (decisionIndex !== null) {
|
|
96
|
+
return decisionIndex;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
decisionIndex = new Map();
|
|
100
|
+
|
|
101
|
+
// Try to load from index file first
|
|
102
|
+
if (fs.existsSync(indexFile)) {
|
|
103
|
+
try {
|
|
104
|
+
const indexData = JSON.parse(fs.readFileSync(indexFile, 'utf-8'));
|
|
105
|
+
if (indexData.version === '1.0.0' && indexData.entries) {
|
|
106
|
+
for (const [id, meta] of Object.entries(indexData.entries)) {
|
|
107
|
+
decisionIndex.set(id, meta);
|
|
108
|
+
}
|
|
109
|
+
return decisionIndex;
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
// Index corrupted, will rebuild
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Rebuild index from decisions file
|
|
117
|
+
rebuildIndex();
|
|
118
|
+
return decisionIndex;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Rebuild the decision index from the decisions file
|
|
123
|
+
* Called when index is missing/corrupted or after compaction
|
|
124
|
+
*/
|
|
125
|
+
function rebuildIndex() {
|
|
126
|
+
decisionIndex = new Map();
|
|
127
|
+
ensureDecisionsFile();
|
|
128
|
+
|
|
129
|
+
const content = fs.readFileSync(decisionsFile, 'utf-8').trim();
|
|
130
|
+
if (!content) return;
|
|
131
|
+
|
|
132
|
+
let lineOffset = 0;
|
|
133
|
+
content.split('\n').filter(Boolean).forEach((line, idx) => {
|
|
134
|
+
try {
|
|
135
|
+
const d = JSON.parse(line);
|
|
136
|
+
decisionIndex.set(d.id, {
|
|
137
|
+
lineOffset: idx,
|
|
138
|
+
timestamp: d.timestamp,
|
|
139
|
+
type: d.type,
|
|
140
|
+
confidence: d.confidence
|
|
141
|
+
});
|
|
142
|
+
} catch {
|
|
143
|
+
// Skip malformed lines
|
|
144
|
+
}
|
|
145
|
+
lineOffset++;
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Save the rebuilt index
|
|
149
|
+
saveIndex();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Save the decision index to disk
|
|
154
|
+
*/
|
|
155
|
+
function saveIndex() {
|
|
156
|
+
const indexData = {
|
|
157
|
+
version: '1.0.0',
|
|
158
|
+
updated: getTimestamp(),
|
|
159
|
+
count: decisionIndex.size,
|
|
160
|
+
entries: Object.fromEntries(decisionIndex)
|
|
161
|
+
};
|
|
162
|
+
fs.writeFileSync(indexFile, JSON.stringify(indexData));
|
|
163
|
+
indexDirty = false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Add a decision to the index
|
|
168
|
+
* @param {string} id Decision ID
|
|
169
|
+
* @param {object} meta Decision metadata
|
|
170
|
+
*/
|
|
171
|
+
function addToIndex(id, meta) {
|
|
172
|
+
loadDecisionIndex();
|
|
173
|
+
decisionIndex.set(id, meta);
|
|
174
|
+
indexDirty = true;
|
|
175
|
+
|
|
176
|
+
// Batch index saves - only save every 10 additions or on explicit save
|
|
177
|
+
if (decisionIndex.size % 10 === 0) {
|
|
178
|
+
saveIndex();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Check if a decision exists in the index (O(1) lookup)
|
|
184
|
+
* @param {string} decisionId Decision ID
|
|
185
|
+
* @returns {boolean} Whether the decision exists
|
|
186
|
+
*/
|
|
187
|
+
function decisionExists(decisionId) {
|
|
188
|
+
loadDecisionIndex();
|
|
189
|
+
return decisionIndex.has(decisionId);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get decision metadata from index (O(1) lookup)
|
|
194
|
+
* @param {string} decisionId Decision ID
|
|
195
|
+
* @returns {object|null} Decision metadata or null
|
|
196
|
+
*/
|
|
197
|
+
function getDecisionMeta(decisionId) {
|
|
198
|
+
loadDecisionIndex();
|
|
199
|
+
return decisionIndex.get(decisionId) || null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Invalidate the in-memory index (forces reload on next access)
|
|
204
|
+
*/
|
|
205
|
+
function invalidateIndex() {
|
|
206
|
+
decisionIndex = null;
|
|
207
|
+
indexDirty = false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Flush any pending index changes to disk
|
|
212
|
+
*/
|
|
213
|
+
function flushIndex() {
|
|
214
|
+
if (indexDirty && decisionIndex !== null) {
|
|
215
|
+
saveIndex();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Ensure outcomes file exists
|
|
221
|
+
*/
|
|
222
|
+
function ensureOutcomesFile() {
|
|
223
|
+
if (!fs.existsSync(memoryDir)) {
|
|
224
|
+
fs.mkdirSync(memoryDir, { recursive: true });
|
|
225
|
+
}
|
|
226
|
+
if (!fs.existsSync(outcomesFile)) {
|
|
227
|
+
fs.writeFileSync(outcomesFile, '');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Read all outcomes from the outcomes file
|
|
233
|
+
* @returns {Map} Map of decisionId -> outcome
|
|
234
|
+
*/
|
|
235
|
+
function readOutcomes() {
|
|
236
|
+
ensureOutcomesFile();
|
|
237
|
+
const content = fs.readFileSync(outcomesFile, 'utf-8').trim();
|
|
238
|
+
if (!content) return new Map();
|
|
239
|
+
|
|
240
|
+
const outcomes = new Map();
|
|
241
|
+
content.split('\n').filter(Boolean).forEach(line => {
|
|
242
|
+
try {
|
|
243
|
+
const entry = JSON.parse(line);
|
|
244
|
+
outcomes.set(entry.decisionId, entry);
|
|
245
|
+
} catch {
|
|
246
|
+
// Skip malformed lines
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
return outcomes;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Get the number of outcome entries
|
|
254
|
+
* @returns {number} Number of outcome entries
|
|
255
|
+
*/
|
|
256
|
+
function getOutcomeCount() {
|
|
257
|
+
ensureOutcomesFile();
|
|
258
|
+
const content = fs.readFileSync(outcomesFile, 'utf-8').trim();
|
|
259
|
+
if (!content) return 0;
|
|
260
|
+
return content.split('\n').filter(Boolean).length;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Get the size of the outcomes file in bytes
|
|
265
|
+
* @returns {number} File size in bytes
|
|
266
|
+
*/
|
|
267
|
+
function getOutcomesFileSize() {
|
|
268
|
+
ensureOutcomesFile();
|
|
269
|
+
try {
|
|
270
|
+
const stats = fs.statSync(outcomesFile);
|
|
271
|
+
return stats.size;
|
|
272
|
+
} catch {
|
|
273
|
+
return 0;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Check if compaction is needed based on thresholds
|
|
279
|
+
* @returns {boolean} Whether compaction should be performed
|
|
280
|
+
*/
|
|
281
|
+
function shouldCompact() {
|
|
282
|
+
const count = getOutcomeCount();
|
|
283
|
+
const size = getOutcomesFileSize();
|
|
284
|
+
return count >= COMPACTION_THRESHOLD_COUNT || size >= COMPACTION_THRESHOLD_BYTES;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Compact outcomes into decisions file
|
|
289
|
+
* Merges all outcomes into the main decisions file and clears outcomes
|
|
290
|
+
* @param {object} options Compaction options
|
|
291
|
+
* @param {boolean} options.force Force compaction even if thresholds not met
|
|
292
|
+
* @returns {object} Compaction results
|
|
293
|
+
*/
|
|
294
|
+
function compact(options = {}) {
|
|
295
|
+
ensureDecisionsFile();
|
|
296
|
+
ensureOutcomesFile();
|
|
297
|
+
|
|
298
|
+
const outcomes = readOutcomes();
|
|
299
|
+
if (outcomes.size === 0) return { compacted: 0, skipped: true };
|
|
300
|
+
|
|
301
|
+
// Read raw decisions
|
|
302
|
+
const content = fs.readFileSync(decisionsFile, 'utf-8').trim();
|
|
303
|
+
if (!content) return { compacted: 0, skipped: true };
|
|
304
|
+
|
|
305
|
+
const decisions = content.split('\n').filter(Boolean).map(line => {
|
|
306
|
+
try {
|
|
307
|
+
return JSON.parse(line);
|
|
308
|
+
} catch {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
}).filter(Boolean);
|
|
312
|
+
|
|
313
|
+
// Merge outcomes into decisions
|
|
314
|
+
let compactedCount = 0;
|
|
315
|
+
const updatedDecisions = decisions.map(d => {
|
|
316
|
+
const outcome = outcomes.get(d.id);
|
|
317
|
+
if (outcome) {
|
|
318
|
+
d.outcome = outcome.outcome;
|
|
319
|
+
d.outcome_recorded_at = outcome.outcome_recorded_at;
|
|
320
|
+
compactedCount++;
|
|
321
|
+
}
|
|
322
|
+
return d;
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// Rewrite decisions file with merged data
|
|
326
|
+
fs.writeFileSync(decisionsFile, updatedDecisions.map(d => JSON.stringify(d)).join('\n') + '\n');
|
|
327
|
+
|
|
328
|
+
// Clear outcomes file
|
|
329
|
+
fs.writeFileSync(outcomesFile, '');
|
|
330
|
+
|
|
331
|
+
// Rebuild index after compaction (line offsets may have changed due to outcome data)
|
|
332
|
+
rebuildIndex();
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
compacted: compactedCount,
|
|
336
|
+
totalDecisions: decisions.length,
|
|
337
|
+
skipped: false
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
78
341
|
/**
|
|
79
342
|
* Ensure patterns file exists
|
|
80
343
|
*/
|
|
@@ -126,9 +389,16 @@ function logDecision(options) {
|
|
|
126
389
|
files_affected: options.files_affected || []
|
|
127
390
|
};
|
|
128
391
|
|
|
129
|
-
// Append to JSONL file
|
|
392
|
+
// Append to JSONL file (O(1) operation)
|
|
130
393
|
fs.appendFileSync(decisionsFile, JSON.stringify(decision) + '\n');
|
|
131
394
|
|
|
395
|
+
// Add to index for O(1) lookups
|
|
396
|
+
addToIndex(decision.id, {
|
|
397
|
+
timestamp: decision.timestamp,
|
|
398
|
+
type: decision.type,
|
|
399
|
+
confidence: decision.confidence
|
|
400
|
+
});
|
|
401
|
+
|
|
132
402
|
// Update patterns stats
|
|
133
403
|
updateStats('total_decisions', 1);
|
|
134
404
|
|
|
@@ -137,6 +407,7 @@ function logDecision(options) {
|
|
|
137
407
|
|
|
138
408
|
/**
|
|
139
409
|
* Record the outcome of a decision
|
|
410
|
+
* Uses O(1) index lookup to verify decision exists, then O(1) append for outcome
|
|
140
411
|
* @param {string} decisionId The decision ID
|
|
141
412
|
* @param {Object} outcome Outcome details
|
|
142
413
|
* @param {string} outcome.status Status (success, partial, failure, reverted)
|
|
@@ -146,46 +417,111 @@ function logDecision(options) {
|
|
|
146
417
|
*/
|
|
147
418
|
function recordOutcome(decisionId, outcome) {
|
|
148
419
|
ensureDecisionsFile();
|
|
420
|
+
ensureOutcomesFile();
|
|
149
421
|
|
|
150
|
-
|
|
151
|
-
|
|
422
|
+
// O(1) lookup using index instead of O(n) file scan
|
|
423
|
+
const decisionMeta = getDecisionMeta(decisionId);
|
|
424
|
+
if (!decisionMeta) {
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
152
427
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
428
|
+
// Create outcome entry
|
|
429
|
+
const outcomeEntry = {
|
|
430
|
+
decisionId,
|
|
431
|
+
outcome: {
|
|
432
|
+
status: outcome.status || OUTCOME_STATUS.PENDING,
|
|
433
|
+
metrics: outcome.metrics || {},
|
|
434
|
+
notes: outcome.notes || '',
|
|
435
|
+
actual_confidence: outcome.actual_confidence || decisionMeta.confidence || 0.5
|
|
436
|
+
},
|
|
437
|
+
outcome_recorded_at: getTimestamp()
|
|
438
|
+
};
|
|
163
439
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
440
|
+
// Append to outcomes file (O(1) operation)
|
|
441
|
+
fs.appendFileSync(outcomesFile, JSON.stringify(outcomeEntry) + '\n');
|
|
442
|
+
|
|
443
|
+
// Update stats based on outcome
|
|
444
|
+
if (outcome.status === OUTCOME_STATUS.SUCCESS) {
|
|
445
|
+
updateStats('successful_decisions', 1);
|
|
446
|
+
} else if (outcome.status === OUTCOME_STATUS.FAILURE) {
|
|
447
|
+
updateStats('failed_decisions', 1);
|
|
448
|
+
}
|
|
173
449
|
|
|
174
|
-
if (
|
|
450
|
+
// Auto-compact if thresholds reached (count or size)
|
|
451
|
+
if (shouldCompact()) {
|
|
452
|
+
compact();
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// For pattern checking and return value, we need the full decision
|
|
456
|
+
// This is acceptable because it's only done once per outcome recording
|
|
457
|
+
// and pattern checking is a secondary operation
|
|
458
|
+
const fullDecision = getDecisionById(decisionId);
|
|
459
|
+
if (fullDecision) {
|
|
460
|
+
const completeDecision = {
|
|
461
|
+
...fullDecision,
|
|
462
|
+
outcome: outcomeEntry.outcome,
|
|
463
|
+
outcome_recorded_at: outcomeEntry.outcome_recorded_at
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
// Check if this creates a pattern
|
|
467
|
+
checkForPatterns(completeDecision);
|
|
468
|
+
|
|
469
|
+
return completeDecision;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Fallback return with minimal data
|
|
473
|
+
return {
|
|
474
|
+
id: decisionId,
|
|
475
|
+
...decisionMeta,
|
|
476
|
+
outcome: outcomeEntry.outcome,
|
|
477
|
+
outcome_recorded_at: outcomeEntry.outcome_recorded_at
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Get a single decision by ID
|
|
483
|
+
* Optimized to use index for existence check, then reads full data if needed
|
|
484
|
+
* @param {string} decisionId Decision ID
|
|
485
|
+
* @returns {Object|null} Decision or null if not found
|
|
486
|
+
*/
|
|
487
|
+
function getDecisionById(decisionId) {
|
|
488
|
+
// O(1) check if decision exists
|
|
489
|
+
if (!decisionExists(decisionId)) {
|
|
175
490
|
return null;
|
|
176
491
|
}
|
|
177
492
|
|
|
178
|
-
|
|
179
|
-
fs.
|
|
493
|
+
ensureDecisionsFile();
|
|
494
|
+
const content = fs.readFileSync(decisionsFile, 'utf-8').trim();
|
|
495
|
+
if (!content) return null;
|
|
180
496
|
|
|
181
|
-
//
|
|
182
|
-
|
|
497
|
+
// Find the decision in the file
|
|
498
|
+
const lines = content.split('\n').filter(Boolean);
|
|
499
|
+
for (const line of lines) {
|
|
500
|
+
try {
|
|
501
|
+
const d = JSON.parse(line);
|
|
502
|
+
if (d.id === decisionId) {
|
|
503
|
+
// Merge with any pending outcome
|
|
504
|
+
const outcomes = readOutcomes();
|
|
505
|
+
const pendingOutcome = outcomes.get(decisionId);
|
|
506
|
+
if (pendingOutcome) {
|
|
507
|
+
return {
|
|
508
|
+
...d,
|
|
509
|
+
outcome: pendingOutcome.outcome,
|
|
510
|
+
outcome_recorded_at: pendingOutcome.outcome_recorded_at
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
return d;
|
|
514
|
+
}
|
|
515
|
+
} catch {
|
|
516
|
+
// Skip malformed lines
|
|
517
|
+
}
|
|
518
|
+
}
|
|
183
519
|
|
|
184
|
-
return
|
|
520
|
+
return null;
|
|
185
521
|
}
|
|
186
522
|
|
|
187
523
|
/**
|
|
188
|
-
* Read all decisions from the JSONL file
|
|
524
|
+
* Read all decisions from the JSONL file, merged with any pending outcomes
|
|
189
525
|
* @returns {Array} Array of decisions
|
|
190
526
|
*/
|
|
191
527
|
function readAllDecisions() {
|
|
@@ -194,13 +530,31 @@ function readAllDecisions() {
|
|
|
194
530
|
const content = fs.readFileSync(decisionsFile, 'utf-8').trim();
|
|
195
531
|
if (!content) return [];
|
|
196
532
|
|
|
197
|
-
|
|
533
|
+
const decisions = content.split('\n').filter(Boolean).map(line => {
|
|
198
534
|
try {
|
|
199
535
|
return JSON.parse(line);
|
|
200
536
|
} catch {
|
|
201
537
|
return null;
|
|
202
538
|
}
|
|
203
539
|
}).filter(Boolean);
|
|
540
|
+
|
|
541
|
+
// Merge with any pending outcomes
|
|
542
|
+
const outcomes = readOutcomes();
|
|
543
|
+
if (outcomes.size > 0) {
|
|
544
|
+
return decisions.map(d => {
|
|
545
|
+
const outcome = outcomes.get(d.id);
|
|
546
|
+
if (outcome) {
|
|
547
|
+
return {
|
|
548
|
+
...d,
|
|
549
|
+
outcome: outcome.outcome,
|
|
550
|
+
outcome_recorded_at: outcome.outcome_recorded_at
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
return d;
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return decisions;
|
|
204
558
|
}
|
|
205
559
|
|
|
206
560
|
/**
|
|
@@ -407,6 +761,12 @@ function clearHistory() {
|
|
|
407
761
|
if (fs.existsSync(decisionsFile)) {
|
|
408
762
|
fs.writeFileSync(decisionsFile, '');
|
|
409
763
|
}
|
|
764
|
+
if (fs.existsSync(outcomesFile)) {
|
|
765
|
+
fs.writeFileSync(outcomesFile, '');
|
|
766
|
+
}
|
|
767
|
+
if (fs.existsSync(indexFile)) {
|
|
768
|
+
fs.unlinkSync(indexFile);
|
|
769
|
+
}
|
|
410
770
|
if (fs.existsSync(patternsFile)) {
|
|
411
771
|
fs.writeFileSync(patternsFile, JSON.stringify({
|
|
412
772
|
version: '1.0.0',
|
|
@@ -420,14 +780,1028 @@ function clearHistory() {
|
|
|
420
780
|
}
|
|
421
781
|
}, null, 2));
|
|
422
782
|
}
|
|
783
|
+
// Clear in-memory index
|
|
784
|
+
invalidateIndex();
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Impact scoring constants
|
|
789
|
+
*/
|
|
790
|
+
const IMPACT_LEVELS = {
|
|
791
|
+
CRITICAL: 5, // System-wide or security impact
|
|
792
|
+
HIGH: 4, // Major feature or performance impact
|
|
793
|
+
MEDIUM: 3, // Moderate feature impact
|
|
794
|
+
LOW: 2, // Minor feature impact
|
|
795
|
+
MINIMAL: 1 // Cosmetic or trivial impact
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Time decay configuration for recency weighting
|
|
800
|
+
*/
|
|
801
|
+
const TIME_DECAY = {
|
|
802
|
+
HALF_LIFE_DAYS: 14, // Score halves every 14 days
|
|
803
|
+
MIN_WEIGHT: 0.1, // Minimum weight for very old decisions
|
|
804
|
+
MAX_AGE_DAYS: 180 // Decisions older than this use minimum weight
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Impact factors used in scoring
|
|
809
|
+
*/
|
|
810
|
+
const IMPACT_FACTORS = {
|
|
811
|
+
// Type-based base scores
|
|
812
|
+
typeWeights: {
|
|
813
|
+
[DECISION_TYPES.ARCHITECTURE]: 1.5,
|
|
814
|
+
[DECISION_TYPES.DATABASE]: 1.4,
|
|
815
|
+
[DECISION_TYPES.SECURITY]: 1.6,
|
|
816
|
+
[DECISION_TYPES.API]: 1.3,
|
|
817
|
+
[DECISION_TYPES.UI]: 1.0,
|
|
818
|
+
[DECISION_TYPES.TESTING]: 1.1,
|
|
819
|
+
[DECISION_TYPES.DEPLOYMENT]: 1.4,
|
|
820
|
+
[DECISION_TYPES.PERFORMANCE]: 1.3,
|
|
821
|
+
[DECISION_TYPES.AGENT_SELECTION]: 1.0,
|
|
822
|
+
[DECISION_TYPES.WORKFLOW_CHOICE]: 1.1,
|
|
823
|
+
[DECISION_TYPES.SKILL_USAGE]: 1.0,
|
|
824
|
+
[DECISION_TYPES.REFACTOR]: 1.2,
|
|
825
|
+
[DECISION_TYPES.BUG_FIX]: 1.1
|
|
826
|
+
},
|
|
827
|
+
// Outcome multipliers
|
|
828
|
+
outcomeMultipliers: {
|
|
829
|
+
[OUTCOME_STATUS.SUCCESS]: 1.0,
|
|
830
|
+
[OUTCOME_STATUS.PARTIAL]: 0.7,
|
|
831
|
+
[OUTCOME_STATUS.FAILURE]: 0.5,
|
|
832
|
+
[OUTCOME_STATUS.REVERTED]: 0.3,
|
|
833
|
+
[OUTCOME_STATUS.PENDING]: 0.8
|
|
834
|
+
},
|
|
835
|
+
// Downstream impact multipliers based on outcome propagation
|
|
836
|
+
downstreamSuccessBonus: 0.15, // Bonus per successful downstream decision
|
|
837
|
+
downstreamFailurePenalty: 0.1, // Penalty per failed downstream decision
|
|
838
|
+
maxDownstreamBonus: 1.0, // Cap for downstream bonuses
|
|
839
|
+
maxDownstreamPenalty: 0.5 // Cap for downstream penalties
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Decision chain tracking - stores parent-child relationships
|
|
844
|
+
* Maps decisionId -> { parent: parentId, children: [childIds] }
|
|
845
|
+
*/
|
|
846
|
+
const decisionChainFile = path.join(memoryDir, 'decision-chains.json');
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Impact scores cache file for performance
|
|
850
|
+
*/
|
|
851
|
+
const impactScoresFile = path.join(memoryDir, 'impact-scores.json');
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* Calculate time decay weight for recency
|
|
855
|
+
* Recent decisions are weighted more heavily
|
|
856
|
+
* @param {string} timestamp - ISO timestamp
|
|
857
|
+
* @returns {number} Decay weight between MIN_WEIGHT and 1.0
|
|
858
|
+
*/
|
|
859
|
+
function calculateTimeDecay(timestamp) {
|
|
860
|
+
if (!timestamp) return TIME_DECAY.MIN_WEIGHT;
|
|
861
|
+
|
|
862
|
+
const decisionDate = new Date(timestamp);
|
|
863
|
+
const now = new Date();
|
|
864
|
+
const ageInDays = (now - decisionDate) / (1000 * 60 * 60 * 24);
|
|
865
|
+
|
|
866
|
+
if (ageInDays > TIME_DECAY.MAX_AGE_DAYS) {
|
|
867
|
+
return TIME_DECAY.MIN_WEIGHT;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Exponential decay: weight = 0.5^(age/halfLife)
|
|
871
|
+
const decayFactor = Math.pow(0.5, ageInDays / TIME_DECAY.HALF_LIFE_DAYS);
|
|
872
|
+
return Math.max(TIME_DECAY.MIN_WEIGHT, decayFactor);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* Ensure decision chains file exists
|
|
877
|
+
*/
|
|
878
|
+
function ensureDecisionChainsFile() {
|
|
879
|
+
if (!fs.existsSync(decisionChainFile)) {
|
|
880
|
+
fs.writeFileSync(decisionChainFile, JSON.stringify({
|
|
881
|
+
version: '1.0.0',
|
|
882
|
+
updated: getTimestamp(),
|
|
883
|
+
chains: {}
|
|
884
|
+
}, null, 2));
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* Load decision chains from file
|
|
890
|
+
* @returns {Object} Decision chains data
|
|
891
|
+
*/
|
|
892
|
+
function loadDecisionChains() {
|
|
893
|
+
ensureDecisionChainsFile();
|
|
894
|
+
try {
|
|
895
|
+
return JSON.parse(fs.readFileSync(decisionChainFile, 'utf-8'));
|
|
896
|
+
} catch {
|
|
897
|
+
return { version: '1.0.0', updated: getTimestamp(), chains: {} };
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* Save decision chains to file
|
|
903
|
+
* @param {Object} chainsData - Chains data to save
|
|
904
|
+
*/
|
|
905
|
+
function saveDecisionChains(chainsData) {
|
|
906
|
+
chainsData.updated = getTimestamp();
|
|
907
|
+
fs.writeFileSync(decisionChainFile, JSON.stringify(chainsData, null, 2));
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* Link a decision to its parent decision (creates decision chain)
|
|
912
|
+
* @param {string} childId - The decision that was influenced
|
|
913
|
+
* @param {string} parentId - The decision that influenced it
|
|
914
|
+
* @param {string} relationship - Type of relationship (e.g., 'caused_by', 'follows', 'extends')
|
|
915
|
+
*/
|
|
916
|
+
function linkDecisions(childId, parentId, relationship = 'influenced_by') {
|
|
917
|
+
const chainsData = loadDecisionChains();
|
|
918
|
+
|
|
919
|
+
// Initialize entries if needed
|
|
920
|
+
if (!chainsData.chains[childId]) {
|
|
921
|
+
chainsData.chains[childId] = { parents: [], children: [] };
|
|
922
|
+
}
|
|
923
|
+
if (!chainsData.chains[parentId]) {
|
|
924
|
+
chainsData.chains[parentId] = { parents: [], children: [] };
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Add relationship
|
|
928
|
+
const existingParent = chainsData.chains[childId].parents.find(p => p.id === parentId);
|
|
929
|
+
if (!existingParent) {
|
|
930
|
+
chainsData.chains[childId].parents.push({
|
|
931
|
+
id: parentId,
|
|
932
|
+
relationship,
|
|
933
|
+
linked_at: getTimestamp()
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const existingChild = chainsData.chains[parentId].children.find(c => c.id === childId);
|
|
938
|
+
if (!existingChild) {
|
|
939
|
+
chainsData.chains[parentId].children.push({
|
|
940
|
+
id: childId,
|
|
941
|
+
relationship,
|
|
942
|
+
linked_at: getTimestamp()
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
saveDecisionChains(chainsData);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* Get all downstream decisions in the chain (recursive)
|
|
951
|
+
* @param {string} decisionId - Starting decision ID
|
|
952
|
+
* @param {Set} visited - Already visited IDs (for cycle detection)
|
|
953
|
+
* @returns {Array} Array of downstream decision info objects
|
|
954
|
+
*/
|
|
955
|
+
function getDownstreamDecisions(decisionId, visited = new Set()) {
|
|
956
|
+
if (visited.has(decisionId)) return [];
|
|
957
|
+
visited.add(decisionId);
|
|
958
|
+
|
|
959
|
+
const chainsData = loadDecisionChains();
|
|
960
|
+
const chainEntry = chainsData.chains[decisionId];
|
|
961
|
+
|
|
962
|
+
if (!chainEntry || !chainEntry.children.length) return [];
|
|
963
|
+
|
|
964
|
+
const downstream = [];
|
|
965
|
+
for (const child of chainEntry.children) {
|
|
966
|
+
const childDecision = getDecisionById(child.id);
|
|
967
|
+
if (childDecision) {
|
|
968
|
+
downstream.push({
|
|
969
|
+
id: child.id,
|
|
970
|
+
relationship: child.relationship,
|
|
971
|
+
decision: childDecision,
|
|
972
|
+
depth: 1
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
// Recursively get children of children
|
|
976
|
+
const grandchildren = getDownstreamDecisions(child.id, visited);
|
|
977
|
+
grandchildren.forEach(gc => {
|
|
978
|
+
gc.depth++;
|
|
979
|
+
downstream.push(gc);
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
return downstream;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* Get all upstream decisions in the chain (recursive)
|
|
989
|
+
* @param {string} decisionId - Starting decision ID
|
|
990
|
+
* @param {Set} visited - Already visited IDs (for cycle detection)
|
|
991
|
+
* @returns {Array} Array of upstream decision info objects
|
|
992
|
+
*/
|
|
993
|
+
function getUpstreamDecisions(decisionId, visited = new Set()) {
|
|
994
|
+
if (visited.has(decisionId)) return [];
|
|
995
|
+
visited.add(decisionId);
|
|
996
|
+
|
|
997
|
+
const chainsData = loadDecisionChains();
|
|
998
|
+
const chainEntry = chainsData.chains[decisionId];
|
|
999
|
+
|
|
1000
|
+
if (!chainEntry || !chainEntry.parents.length) return [];
|
|
1001
|
+
|
|
1002
|
+
const upstream = [];
|
|
1003
|
+
for (const parent of chainEntry.parents) {
|
|
1004
|
+
const parentDecision = getDecisionById(parent.id);
|
|
1005
|
+
if (parentDecision) {
|
|
1006
|
+
upstream.push({
|
|
1007
|
+
id: parent.id,
|
|
1008
|
+
relationship: parent.relationship,
|
|
1009
|
+
decision: parentDecision,
|
|
1010
|
+
depth: 1
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
// Recursively get parents of parents
|
|
1014
|
+
const grandparents = getUpstreamDecisions(parent.id, visited);
|
|
1015
|
+
grandparents.forEach(gp => {
|
|
1016
|
+
gp.depth++;
|
|
1017
|
+
upstream.push(gp);
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
return upstream;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* Calculate downstream impact based on outcome propagation
|
|
1027
|
+
* @param {string} decisionId - Decision ID
|
|
1028
|
+
* @returns {Object} Downstream impact details
|
|
1029
|
+
*/
|
|
1030
|
+
function calculateDownstreamImpact(decisionId) {
|
|
1031
|
+
const downstream = getDownstreamDecisions(decisionId);
|
|
1032
|
+
|
|
1033
|
+
if (downstream.length === 0) {
|
|
1034
|
+
// Fallback to temporal-based counting for decisions without explicit links
|
|
1035
|
+
return calculateTemporalDownstreamImpact(decisionId);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
let successCount = 0;
|
|
1039
|
+
let failureCount = 0;
|
|
1040
|
+
let totalWeight = 0;
|
|
1041
|
+
|
|
1042
|
+
for (const d of downstream) {
|
|
1043
|
+
const depthWeight = 1 / d.depth; // Closer decisions have more weight
|
|
1044
|
+
const timeWeight = calculateTimeDecay(d.decision.timestamp);
|
|
1045
|
+
const weight = depthWeight * timeWeight;
|
|
1046
|
+
|
|
1047
|
+
if (d.decision.outcome?.status === OUTCOME_STATUS.SUCCESS) {
|
|
1048
|
+
successCount += weight;
|
|
1049
|
+
} else if (d.decision.outcome?.status === OUTCOME_STATUS.FAILURE ||
|
|
1050
|
+
d.decision.outcome?.status === OUTCOME_STATUS.REVERTED) {
|
|
1051
|
+
failureCount += weight;
|
|
1052
|
+
}
|
|
1053
|
+
totalWeight += weight;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
const successBonus = Math.min(
|
|
1057
|
+
successCount * IMPACT_FACTORS.downstreamSuccessBonus,
|
|
1058
|
+
IMPACT_FACTORS.maxDownstreamBonus
|
|
1059
|
+
);
|
|
1060
|
+
|
|
1061
|
+
const failurePenalty = Math.min(
|
|
1062
|
+
failureCount * IMPACT_FACTORS.downstreamFailurePenalty,
|
|
1063
|
+
IMPACT_FACTORS.maxDownstreamPenalty
|
|
1064
|
+
);
|
|
1065
|
+
|
|
1066
|
+
return {
|
|
1067
|
+
downstreamCount: downstream.length,
|
|
1068
|
+
weightedSuccesses: Math.round(successCount * 100) / 100,
|
|
1069
|
+
weightedFailures: Math.round(failureCount * 100) / 100,
|
|
1070
|
+
successBonus: Math.round(successBonus * 100) / 100,
|
|
1071
|
+
failurePenalty: Math.round(failurePenalty * 100) / 100,
|
|
1072
|
+
netImpact: Math.round((successBonus - failurePenalty) * 100) / 100,
|
|
1073
|
+
hasExplicitChain: true
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
/**
|
|
1078
|
+
* Calculate downstream impact based on temporal proximity (fallback)
|
|
1079
|
+
* @param {string} decisionId - Decision ID
|
|
1080
|
+
* @returns {Object} Downstream impact details
|
|
1081
|
+
*/
|
|
1082
|
+
function calculateTemporalDownstreamImpact(decisionId) {
|
|
1083
|
+
const decisions = readAllDecisions();
|
|
1084
|
+
const decision = decisions.find(d => d.id === decisionId);
|
|
1085
|
+
|
|
1086
|
+
if (!decision) {
|
|
1087
|
+
return {
|
|
1088
|
+
downstreamCount: 0,
|
|
1089
|
+
weightedSuccesses: 0,
|
|
1090
|
+
weightedFailures: 0,
|
|
1091
|
+
successBonus: 0,
|
|
1092
|
+
failurePenalty: 0,
|
|
1093
|
+
netImpact: 0,
|
|
1094
|
+
hasExplicitChain: false
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
const timestamp = new Date(decision.timestamp);
|
|
1099
|
+
const relatedTypes = getRelatedTypes(decision.type);
|
|
1100
|
+
|
|
1101
|
+
const downstream = decisions.filter(d => {
|
|
1102
|
+
const dTime = new Date(d.timestamp);
|
|
1103
|
+
return dTime > timestamp && relatedTypes.includes(d.type);
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
let successCount = 0;
|
|
1107
|
+
let failureCount = 0;
|
|
1108
|
+
|
|
1109
|
+
for (const d of downstream) {
|
|
1110
|
+
const timeWeight = calculateTimeDecay(d.timestamp);
|
|
1111
|
+
|
|
1112
|
+
if (d.outcome?.status === OUTCOME_STATUS.SUCCESS) {
|
|
1113
|
+
successCount += timeWeight;
|
|
1114
|
+
} else if (d.outcome?.status === OUTCOME_STATUS.FAILURE ||
|
|
1115
|
+
d.outcome?.status === OUTCOME_STATUS.REVERTED) {
|
|
1116
|
+
failureCount += timeWeight;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
const successBonus = Math.min(
|
|
1121
|
+
successCount * IMPACT_FACTORS.downstreamSuccessBonus,
|
|
1122
|
+
IMPACT_FACTORS.maxDownstreamBonus
|
|
1123
|
+
);
|
|
1124
|
+
|
|
1125
|
+
const failurePenalty = Math.min(
|
|
1126
|
+
failureCount * IMPACT_FACTORS.downstreamFailurePenalty,
|
|
1127
|
+
IMPACT_FACTORS.maxDownstreamPenalty
|
|
1128
|
+
);
|
|
1129
|
+
|
|
1130
|
+
return {
|
|
1131
|
+
downstreamCount: downstream.length,
|
|
1132
|
+
weightedSuccesses: Math.round(successCount * 100) / 100,
|
|
1133
|
+
weightedFailures: Math.round(failureCount * 100) / 100,
|
|
1134
|
+
successBonus: Math.round(successBonus * 100) / 100,
|
|
1135
|
+
failurePenalty: Math.round(failurePenalty * 100) / 100,
|
|
1136
|
+
netImpact: Math.round((successBonus - failurePenalty) * 100) / 100,
|
|
1137
|
+
hasExplicitChain: false
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
/**
|
|
1142
|
+
* Calculate impact score for a decision
|
|
1143
|
+
* Enhanced with time decay, downstream tracking, and outcome propagation
|
|
1144
|
+
* @param {object} decision - Decision object
|
|
1145
|
+
* @param {object} options - Scoring options
|
|
1146
|
+
* @param {boolean} options.includeDownstream - Include downstream impact (default: true)
|
|
1147
|
+
* @param {boolean} options.applyTimeDecay - Apply time decay weighting (default: true)
|
|
1148
|
+
* @returns {object} Impact score details
|
|
1149
|
+
*/
|
|
1150
|
+
function calculateImpactScore(decision, options = {}) {
|
|
1151
|
+
const { includeDownstream = true, applyTimeDecay = true } = options;
|
|
1152
|
+
|
|
1153
|
+
if (!decision) return { score: 0, level: 'UNKNOWN', factors: {} };
|
|
1154
|
+
|
|
1155
|
+
// Base score from type
|
|
1156
|
+
const typeWeight = IMPACT_FACTORS.typeWeights[decision.type] || 1.0;
|
|
1157
|
+
|
|
1158
|
+
// Confidence contribution
|
|
1159
|
+
const confidenceScore = decision.confidence || 0.5;
|
|
1160
|
+
|
|
1161
|
+
// Outcome multiplier
|
|
1162
|
+
const outcomeStatus = decision.outcome?.status || OUTCOME_STATUS.PENDING;
|
|
1163
|
+
const outcomeMultiplier = IMPACT_FACTORS.outcomeMultipliers[outcomeStatus] || 0.8;
|
|
1164
|
+
|
|
1165
|
+
// Time decay for recency weighting
|
|
1166
|
+
const timeDecayWeight = applyTimeDecay ? calculateTimeDecay(decision.timestamp) : 1.0;
|
|
1167
|
+
|
|
1168
|
+
// Calculate downstream impact
|
|
1169
|
+
let downstreamImpact = { netImpact: 0, downstreamCount: 0 };
|
|
1170
|
+
if (includeDownstream) {
|
|
1171
|
+
downstreamImpact = calculateDownstreamImpact(decision.id);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// Calculate raw score with all factors
|
|
1175
|
+
const baseScore = typeWeight * confidenceScore * outcomeMultiplier;
|
|
1176
|
+
const weightedScore = baseScore * timeDecayWeight;
|
|
1177
|
+
const finalScore = weightedScore + downstreamImpact.netImpact;
|
|
1178
|
+
|
|
1179
|
+
// Normalize to 1-5 scale
|
|
1180
|
+
const normalizedScore = Math.min(Math.max(finalScore * 2.5, 1), 5);
|
|
1181
|
+
|
|
1182
|
+
// Determine level
|
|
1183
|
+
let level = 'MINIMAL';
|
|
1184
|
+
if (normalizedScore >= 4.5) level = 'CRITICAL';
|
|
1185
|
+
else if (normalizedScore >= 3.5) level = 'HIGH';
|
|
1186
|
+
else if (normalizedScore >= 2.5) level = 'MEDIUM';
|
|
1187
|
+
else if (normalizedScore >= 1.5) level = 'LOW';
|
|
1188
|
+
|
|
1189
|
+
return {
|
|
1190
|
+
score: Math.round(normalizedScore * 100) / 100,
|
|
1191
|
+
rawScore: Math.round(finalScore * 100) / 100,
|
|
1192
|
+
level,
|
|
1193
|
+
factors: {
|
|
1194
|
+
typeWeight,
|
|
1195
|
+
confidenceScore,
|
|
1196
|
+
outcomeMultiplier,
|
|
1197
|
+
timeDecayWeight: Math.round(timeDecayWeight * 100) / 100,
|
|
1198
|
+
downstreamImpact: downstreamImpact.netImpact,
|
|
1199
|
+
downstreamCount: downstreamImpact.downstreamCount
|
|
1200
|
+
},
|
|
1201
|
+
downstream: includeDownstream ? downstreamImpact : null
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/**
|
|
1206
|
+
* Count downstream decisions affected by this decision (legacy support)
|
|
1207
|
+
* @param {string} decisionId - Decision ID
|
|
1208
|
+
* @returns {number} Count of downstream decisions
|
|
1209
|
+
*/
|
|
1210
|
+
function countDownstreamDecisions(decisionId) {
|
|
1211
|
+
const impact = calculateDownstreamImpact(decisionId);
|
|
1212
|
+
return impact.downstreamCount;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
/**
|
|
1216
|
+
* Get related decision types
|
|
1217
|
+
* @param {string} type - Decision type
|
|
1218
|
+
* @returns {Array} Related types
|
|
1219
|
+
*/
|
|
1220
|
+
function getRelatedTypes(type) {
|
|
1221
|
+
const relationships = {
|
|
1222
|
+
[DECISION_TYPES.ARCHITECTURE]: [DECISION_TYPES.DATABASE, DECISION_TYPES.API, DECISION_TYPES.DEPLOYMENT],
|
|
1223
|
+
[DECISION_TYPES.DATABASE]: [DECISION_TYPES.API, DECISION_TYPES.PERFORMANCE],
|
|
1224
|
+
[DECISION_TYPES.API]: [DECISION_TYPES.UI, DECISION_TYPES.TESTING],
|
|
1225
|
+
[DECISION_TYPES.SECURITY]: [DECISION_TYPES.DATABASE, DECISION_TYPES.API, DECISION_TYPES.DEPLOYMENT],
|
|
1226
|
+
[DECISION_TYPES.UI]: [DECISION_TYPES.TESTING, DECISION_TYPES.PERFORMANCE],
|
|
1227
|
+
[DECISION_TYPES.TESTING]: [],
|
|
1228
|
+
[DECISION_TYPES.DEPLOYMENT]: [DECISION_TYPES.PERFORMANCE],
|
|
1229
|
+
[DECISION_TYPES.PERFORMANCE]: [],
|
|
1230
|
+
[DECISION_TYPES.AGENT_SELECTION]: [DECISION_TYPES.WORKFLOW_CHOICE, DECISION_TYPES.SKILL_USAGE],
|
|
1231
|
+
[DECISION_TYPES.WORKFLOW_CHOICE]: [DECISION_TYPES.SKILL_USAGE],
|
|
1232
|
+
[DECISION_TYPES.SKILL_USAGE]: [],
|
|
1233
|
+
[DECISION_TYPES.REFACTOR]: [DECISION_TYPES.TESTING, DECISION_TYPES.PERFORMANCE],
|
|
1234
|
+
[DECISION_TYPES.BUG_FIX]: [DECISION_TYPES.TESTING]
|
|
1235
|
+
};
|
|
1236
|
+
|
|
1237
|
+
return [type, ...(relationships[type] || [])];
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/**
|
|
1241
|
+
* Track successful outcomes and their decision chains
|
|
1242
|
+
* @param {string} decisionId - Decision ID that succeeded
|
|
1243
|
+
* @returns {Object} Success chain analysis
|
|
1244
|
+
*/
|
|
1245
|
+
function trackSuccessChain(decisionId) {
|
|
1246
|
+
const decision = getDecisionById(decisionId);
|
|
1247
|
+
if (!decision || decision.outcome?.status !== OUTCOME_STATUS.SUCCESS) {
|
|
1248
|
+
return null;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
const upstream = getUpstreamDecisions(decisionId);
|
|
1252
|
+
const downstream = getDownstreamDecisions(decisionId);
|
|
1253
|
+
|
|
1254
|
+
// Find all decisions in the chain that also succeeded
|
|
1255
|
+
const successfulUpstream = upstream.filter(
|
|
1256
|
+
u => u.decision.outcome?.status === OUTCOME_STATUS.SUCCESS
|
|
1257
|
+
);
|
|
1258
|
+
const successfulDownstream = downstream.filter(
|
|
1259
|
+
d => d.decision.outcome?.status === OUTCOME_STATUS.SUCCESS
|
|
1260
|
+
);
|
|
1261
|
+
|
|
1262
|
+
// Calculate chain success rate
|
|
1263
|
+
const totalInChain = upstream.length + downstream.length + 1;
|
|
1264
|
+
const successfulInChain = successfulUpstream.length + successfulDownstream.length + 1;
|
|
1265
|
+
const chainSuccessRate = totalInChain > 0 ? successfulInChain / totalInChain : 1;
|
|
1266
|
+
|
|
1267
|
+
return {
|
|
1268
|
+
decisionId,
|
|
1269
|
+
decision: decision.decision,
|
|
1270
|
+
type: decision.type,
|
|
1271
|
+
chainLength: totalInChain,
|
|
1272
|
+
chainSuccessRate: Math.round(chainSuccessRate * 100) / 100,
|
|
1273
|
+
successfulUpstream: successfulUpstream.map(u => ({
|
|
1274
|
+
id: u.id,
|
|
1275
|
+
decision: u.decision.decision,
|
|
1276
|
+
type: u.decision.type
|
|
1277
|
+
})),
|
|
1278
|
+
successfulDownstream: successfulDownstream.map(d => ({
|
|
1279
|
+
id: d.id,
|
|
1280
|
+
decision: d.decision.decision,
|
|
1281
|
+
type: d.decision.type
|
|
1282
|
+
})),
|
|
1283
|
+
timestamp: decision.timestamp
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* Get decisions that led to successful outcomes
|
|
1289
|
+
* Identifies patterns and decision chains that correlate with success
|
|
1290
|
+
* @param {Object} options - Filter options
|
|
1291
|
+
* @param {number} options.minSuccessRate - Minimum chain success rate (0-1)
|
|
1292
|
+
* @param {number} options.minChainLength - Minimum chain length
|
|
1293
|
+
* @param {number} options.limit - Maximum results to return
|
|
1294
|
+
* @returns {Array} Successful decision patterns
|
|
1295
|
+
*/
|
|
1296
|
+
function getSuccessfulDecisionPatterns(options = {}) {
|
|
1297
|
+
const {
|
|
1298
|
+
minSuccessRate = 0.7,
|
|
1299
|
+
minChainLength = 2,
|
|
1300
|
+
limit = 20
|
|
1301
|
+
} = options;
|
|
1302
|
+
|
|
1303
|
+
const decisions = readAllDecisions();
|
|
1304
|
+
const successfulDecisions = decisions.filter(
|
|
1305
|
+
d => d.outcome?.status === OUTCOME_STATUS.SUCCESS
|
|
1306
|
+
);
|
|
1307
|
+
|
|
1308
|
+
const patterns = [];
|
|
1309
|
+
|
|
1310
|
+
for (const decision of successfulDecisions) {
|
|
1311
|
+
const chain = trackSuccessChain(decision.id);
|
|
1312
|
+
if (chain &&
|
|
1313
|
+
chain.chainSuccessRate >= minSuccessRate &&
|
|
1314
|
+
chain.chainLength >= minChainLength) {
|
|
1315
|
+
patterns.push({
|
|
1316
|
+
...chain,
|
|
1317
|
+
impact: calculateImpactScore(decision)
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// Sort by impact score and chain success rate
|
|
1323
|
+
return patterns
|
|
1324
|
+
.sort((a, b) => {
|
|
1325
|
+
const scoreA = a.impact.score * a.chainSuccessRate;
|
|
1326
|
+
const scoreB = b.impact.score * b.chainSuccessRate;
|
|
1327
|
+
return scoreB - scoreA;
|
|
1328
|
+
})
|
|
1329
|
+
.slice(0, limit);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
/**
|
|
1333
|
+
* Get highest impact decisions with enhanced analysis
|
|
1334
|
+
* @param {number} limit - Number of decisions to return
|
|
1335
|
+
* @param {Object} options - Filter options
|
|
1336
|
+
* @param {boolean} options.onlySuccessful - Only include successful decisions
|
|
1337
|
+
* @param {string} options.type - Filter by decision type
|
|
1338
|
+
* @param {number} options.minScore - Minimum impact score
|
|
1339
|
+
* @returns {Array} Decisions sorted by impact score
|
|
1340
|
+
*/
|
|
1341
|
+
function getHighImpactDecisions(limit = 10, options = {}) {
|
|
1342
|
+
const {
|
|
1343
|
+
onlySuccessful = false,
|
|
1344
|
+
type = null,
|
|
1345
|
+
minScore = 0
|
|
1346
|
+
} = options;
|
|
1347
|
+
|
|
1348
|
+
let decisions = readAllDecisions();
|
|
1349
|
+
|
|
1350
|
+
// Apply filters
|
|
1351
|
+
if (onlySuccessful) {
|
|
1352
|
+
decisions = decisions.filter(d => d.outcome?.status === OUTCOME_STATUS.SUCCESS);
|
|
1353
|
+
}
|
|
1354
|
+
if (type) {
|
|
1355
|
+
decisions = decisions.filter(d => d.type === type);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
const withImpact = decisions
|
|
1359
|
+
.map(d => ({
|
|
1360
|
+
...d,
|
|
1361
|
+
impact: calculateImpactScore(d)
|
|
1362
|
+
}))
|
|
1363
|
+
.filter(d => d.impact.score >= minScore);
|
|
1364
|
+
|
|
1365
|
+
return withImpact
|
|
1366
|
+
.sort((a, b) => b.impact.score - a.impact.score)
|
|
1367
|
+
.slice(0, limit);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
/**
|
|
1371
|
+
* Generate comprehensive impact report for decision analysis
|
|
1372
|
+
* @param {Object} options - Report options
|
|
1373
|
+
* @param {number} options.days - Number of days to analyze
|
|
1374
|
+
* @param {boolean} options.includeChains - Include decision chain analysis
|
|
1375
|
+
* @param {boolean} options.includePatterns - Include pattern analysis
|
|
1376
|
+
* @returns {Object} Comprehensive impact report
|
|
1377
|
+
*/
|
|
1378
|
+
function generateImpactReport(options = {}) {
|
|
1379
|
+
const {
|
|
1380
|
+
days = 30,
|
|
1381
|
+
includeChains = true,
|
|
1382
|
+
includePatterns = true
|
|
1383
|
+
} = options;
|
|
1384
|
+
|
|
1385
|
+
const cutoffDate = new Date();
|
|
1386
|
+
cutoffDate.setDate(cutoffDate.getDate() - days);
|
|
1387
|
+
|
|
1388
|
+
const decisions = readAllDecisions();
|
|
1389
|
+
const recentDecisions = decisions.filter(
|
|
1390
|
+
d => new Date(d.timestamp) >= cutoffDate
|
|
1391
|
+
);
|
|
1392
|
+
|
|
1393
|
+
// Calculate impact for all recent decisions
|
|
1394
|
+
const withImpact = recentDecisions.map(d => ({
|
|
1395
|
+
...d,
|
|
1396
|
+
impact: calculateImpactScore(d)
|
|
1397
|
+
}));
|
|
1398
|
+
|
|
1399
|
+
// Group by impact level
|
|
1400
|
+
const byLevel = {};
|
|
1401
|
+
for (const level of Object.keys(IMPACT_LEVELS)) {
|
|
1402
|
+
byLevel[level] = withImpact.filter(d => d.impact.level === level);
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// Group by type
|
|
1406
|
+
const byType = {};
|
|
1407
|
+
for (const d of withImpact) {
|
|
1408
|
+
if (!byType[d.type]) {
|
|
1409
|
+
byType[d.type] = { decisions: [], totalScore: 0, successCount: 0 };
|
|
1410
|
+
}
|
|
1411
|
+
byType[d.type].decisions.push(d);
|
|
1412
|
+
byType[d.type].totalScore += d.impact.score;
|
|
1413
|
+
if (d.outcome?.status === OUTCOME_STATUS.SUCCESS) {
|
|
1414
|
+
byType[d.type].successCount++;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// Calculate type stats
|
|
1419
|
+
const typeStats = Object.entries(byType).map(([type, data]) => ({
|
|
1420
|
+
type,
|
|
1421
|
+
count: data.decisions.length,
|
|
1422
|
+
averageScore: Math.round((data.totalScore / data.decisions.length) * 100) / 100,
|
|
1423
|
+
successRate: Math.round((data.successCount / data.decisions.length) * 100) / 100,
|
|
1424
|
+
highestImpact: data.decisions.sort((a, b) => b.impact.score - a.impact.score)[0]
|
|
1425
|
+
})).sort((a, b) => b.averageScore - a.averageScore);
|
|
1426
|
+
|
|
1427
|
+
// Overall stats
|
|
1428
|
+
const totalScore = withImpact.reduce((sum, d) => sum + d.impact.score, 0);
|
|
1429
|
+
const successfulDecisions = withImpact.filter(
|
|
1430
|
+
d => d.outcome?.status === OUTCOME_STATUS.SUCCESS
|
|
1431
|
+
);
|
|
1432
|
+
|
|
1433
|
+
// Time trend analysis
|
|
1434
|
+
const weeklyTrends = calculateWeeklyTrends(withImpact, days);
|
|
1435
|
+
|
|
1436
|
+
// Build report
|
|
1437
|
+
const report = {
|
|
1438
|
+
generated: getTimestamp(),
|
|
1439
|
+
period: {
|
|
1440
|
+
days,
|
|
1441
|
+
from: cutoffDate.toISOString(),
|
|
1442
|
+
to: new Date().toISOString()
|
|
1443
|
+
},
|
|
1444
|
+
summary: {
|
|
1445
|
+
totalDecisions: withImpact.length,
|
|
1446
|
+
averageImpactScore: withImpact.length > 0
|
|
1447
|
+
? Math.round((totalScore / withImpact.length) * 100) / 100
|
|
1448
|
+
: 0,
|
|
1449
|
+
successfulDecisions: successfulDecisions.length,
|
|
1450
|
+
successRate: withImpact.length > 0
|
|
1451
|
+
? Math.round((successfulDecisions.length / withImpact.length) * 100) / 100
|
|
1452
|
+
: 0,
|
|
1453
|
+
highImpactCount: byLevel.HIGH?.length || 0 + (byLevel.CRITICAL?.length || 0)
|
|
1454
|
+
},
|
|
1455
|
+
byLevel: Object.entries(byLevel).map(([level, decisions]) => ({
|
|
1456
|
+
level,
|
|
1457
|
+
count: decisions.length,
|
|
1458
|
+
percentage: withImpact.length > 0
|
|
1459
|
+
? Math.round((decisions.length / withImpact.length) * 100)
|
|
1460
|
+
: 0
|
|
1461
|
+
})),
|
|
1462
|
+
byType: typeStats,
|
|
1463
|
+
trends: weeklyTrends,
|
|
1464
|
+
topDecisions: withImpact
|
|
1465
|
+
.sort((a, b) => b.impact.score - a.impact.score)
|
|
1466
|
+
.slice(0, 10)
|
|
1467
|
+
.map(d => ({
|
|
1468
|
+
id: d.id,
|
|
1469
|
+
decision: d.decision,
|
|
1470
|
+
type: d.type,
|
|
1471
|
+
score: d.impact.score,
|
|
1472
|
+
level: d.impact.level,
|
|
1473
|
+
outcome: d.outcome?.status,
|
|
1474
|
+
timestamp: d.timestamp
|
|
1475
|
+
}))
|
|
1476
|
+
};
|
|
1477
|
+
|
|
1478
|
+
// Add chain analysis if requested
|
|
1479
|
+
if (includeChains) {
|
|
1480
|
+
const chainsData = loadDecisionChains();
|
|
1481
|
+
const chainedDecisionIds = Object.keys(chainsData.chains);
|
|
1482
|
+
const chainedCount = chainedDecisionIds.length;
|
|
1483
|
+
|
|
1484
|
+
report.chainAnalysis = {
|
|
1485
|
+
totalChainedDecisions: chainedCount,
|
|
1486
|
+
percentageChained: withImpact.length > 0
|
|
1487
|
+
? Math.round((chainedCount / decisions.length) * 100)
|
|
1488
|
+
: 0,
|
|
1489
|
+
longestChains: findLongestChains(5)
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
// Add pattern analysis if requested
|
|
1494
|
+
if (includePatterns) {
|
|
1495
|
+
report.successPatterns = getSuccessfulDecisionPatterns({
|
|
1496
|
+
minSuccessRate: 0.7,
|
|
1497
|
+
minChainLength: 2,
|
|
1498
|
+
limit: 10
|
|
1499
|
+
});
|
|
1500
|
+
|
|
1501
|
+
report.highImpactPatterns = identifyHighImpactPatterns(withImpact);
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
return report;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
/**
|
|
1508
|
+
* Calculate weekly trends for impact scores
|
|
1509
|
+
* @param {Array} decisions - Decisions with impact scores
|
|
1510
|
+
* @param {number} days - Number of days to analyze
|
|
1511
|
+
* @returns {Array} Weekly trend data
|
|
1512
|
+
*/
|
|
1513
|
+
function calculateWeeklyTrends(decisions, days) {
|
|
1514
|
+
const weeks = Math.ceil(days / 7);
|
|
1515
|
+
const trends = [];
|
|
1516
|
+
const now = new Date();
|
|
1517
|
+
|
|
1518
|
+
for (let i = 0; i < weeks; i++) {
|
|
1519
|
+
const weekEnd = new Date(now);
|
|
1520
|
+
weekEnd.setDate(weekEnd.getDate() - (i * 7));
|
|
1521
|
+
const weekStart = new Date(weekEnd);
|
|
1522
|
+
weekStart.setDate(weekStart.getDate() - 7);
|
|
1523
|
+
|
|
1524
|
+
const weekDecisions = decisions.filter(d => {
|
|
1525
|
+
const dDate = new Date(d.timestamp);
|
|
1526
|
+
return dDate >= weekStart && dDate < weekEnd;
|
|
1527
|
+
});
|
|
1528
|
+
|
|
1529
|
+
const totalScore = weekDecisions.reduce((sum, d) => sum + d.impact.score, 0);
|
|
1530
|
+
const successCount = weekDecisions.filter(
|
|
1531
|
+
d => d.outcome?.status === OUTCOME_STATUS.SUCCESS
|
|
1532
|
+
).length;
|
|
1533
|
+
|
|
1534
|
+
trends.unshift({
|
|
1535
|
+
week: weeks - i,
|
|
1536
|
+
weekStart: weekStart.toISOString().split('T')[0],
|
|
1537
|
+
weekEnd: weekEnd.toISOString().split('T')[0],
|
|
1538
|
+
decisionCount: weekDecisions.length,
|
|
1539
|
+
averageScore: weekDecisions.length > 0
|
|
1540
|
+
? Math.round((totalScore / weekDecisions.length) * 100) / 100
|
|
1541
|
+
: 0,
|
|
1542
|
+
successRate: weekDecisions.length > 0
|
|
1543
|
+
? Math.round((successCount / weekDecisions.length) * 100) / 100
|
|
1544
|
+
: 0
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
return trends;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
/**
|
|
1552
|
+
* Find the longest decision chains
|
|
1553
|
+
* @param {number} limit - Number of chains to return
|
|
1554
|
+
* @returns {Array} Longest chains
|
|
1555
|
+
*/
|
|
1556
|
+
function findLongestChains(limit = 5) {
|
|
1557
|
+
const chainsData = loadDecisionChains();
|
|
1558
|
+
const chainLengths = [];
|
|
1559
|
+
|
|
1560
|
+
for (const decisionId of Object.keys(chainsData.chains)) {
|
|
1561
|
+
const upstream = getUpstreamDecisions(decisionId);
|
|
1562
|
+
const downstream = getDownstreamDecisions(decisionId);
|
|
1563
|
+
const totalLength = upstream.length + downstream.length + 1;
|
|
1564
|
+
|
|
1565
|
+
chainLengths.push({
|
|
1566
|
+
decisionId,
|
|
1567
|
+
totalLength,
|
|
1568
|
+
upstreamCount: upstream.length,
|
|
1569
|
+
downstreamCount: downstream.length
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
return chainLengths
|
|
1574
|
+
.sort((a, b) => b.totalLength - a.totalLength)
|
|
1575
|
+
.slice(0, limit);
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
/**
|
|
1579
|
+
* Identify high-impact patterns from decisions
|
|
1580
|
+
* @param {Array} decisions - Decisions with impact scores
|
|
1581
|
+
* @returns {Object} Pattern analysis
|
|
1582
|
+
*/
|
|
1583
|
+
function identifyHighImpactPatterns(decisions) {
|
|
1584
|
+
const highImpact = decisions.filter(d =>
|
|
1585
|
+
d.impact.level === 'HIGH' || d.impact.level === 'CRITICAL'
|
|
1586
|
+
);
|
|
1587
|
+
|
|
1588
|
+
// Group by decision text similarity
|
|
1589
|
+
const patternGroups = {};
|
|
1590
|
+
for (const d of highImpact) {
|
|
1591
|
+
const key = `${d.type}:${normalizeDecisionText(d.decision)}`;
|
|
1592
|
+
if (!patternGroups[key]) {
|
|
1593
|
+
patternGroups[key] = {
|
|
1594
|
+
type: d.type,
|
|
1595
|
+
pattern: d.decision,
|
|
1596
|
+
decisions: [],
|
|
1597
|
+
totalScore: 0,
|
|
1598
|
+
successCount: 0
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1601
|
+
patternGroups[key].decisions.push(d);
|
|
1602
|
+
patternGroups[key].totalScore += d.impact.score;
|
|
1603
|
+
if (d.outcome?.status === OUTCOME_STATUS.SUCCESS) {
|
|
1604
|
+
patternGroups[key].successCount++;
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// Convert to array and sort by frequency and score
|
|
1609
|
+
return Object.values(patternGroups)
|
|
1610
|
+
.filter(p => p.decisions.length >= 2)
|
|
1611
|
+
.map(p => ({
|
|
1612
|
+
type: p.type,
|
|
1613
|
+
pattern: p.pattern,
|
|
1614
|
+
occurrences: p.decisions.length,
|
|
1615
|
+
averageScore: Math.round((p.totalScore / p.decisions.length) * 100) / 100,
|
|
1616
|
+
successRate: Math.round((p.successCount / p.decisions.length) * 100) / 100
|
|
1617
|
+
}))
|
|
1618
|
+
.sort((a, b) => (b.occurrences * b.averageScore) - (a.occurrences * a.averageScore))
|
|
1619
|
+
.slice(0, 10);
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
/**
|
|
1623
|
+
* Normalize decision text for pattern matching
|
|
1624
|
+
* @param {string} text - Decision text
|
|
1625
|
+
* @returns {string} Normalized text
|
|
1626
|
+
*/
|
|
1627
|
+
function normalizeDecisionText(text) {
|
|
1628
|
+
return (text || '')
|
|
1629
|
+
.toLowerCase()
|
|
1630
|
+
.replace(/[^a-z0-9\s]/g, '')
|
|
1631
|
+
.replace(/\s+/g, ' ')
|
|
1632
|
+
.trim()
|
|
1633
|
+
.slice(0, 50);
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
/**
|
|
1637
|
+
* Get impact-boosted patterns for recommendations integration
|
|
1638
|
+
* Returns patterns that have proven high-impact and successful
|
|
1639
|
+
* @param {Object} options - Options
|
|
1640
|
+
* @param {number} options.minScore - Minimum impact score
|
|
1641
|
+
* @param {number} options.minSuccessRate - Minimum success rate
|
|
1642
|
+
* @returns {Array} High-impact patterns for boosting recommendations
|
|
1643
|
+
*/
|
|
1644
|
+
function getImpactBoostedPatterns(options = {}) {
|
|
1645
|
+
const {
|
|
1646
|
+
minScore = 3.0,
|
|
1647
|
+
minSuccessRate = 0.6
|
|
1648
|
+
} = options;
|
|
1649
|
+
|
|
1650
|
+
const decisions = readAllDecisions();
|
|
1651
|
+
const withImpact = decisions.map(d => ({
|
|
1652
|
+
...d,
|
|
1653
|
+
impact: calculateImpactScore(d)
|
|
1654
|
+
}));
|
|
1655
|
+
|
|
1656
|
+
// Group by type and decision pattern
|
|
1657
|
+
const patterns = {};
|
|
1658
|
+
for (const d of withImpact) {
|
|
1659
|
+
if (d.impact.score < minScore) continue;
|
|
1660
|
+
|
|
1661
|
+
const key = `${d.type}:${normalizeDecisionText(d.decision)}`;
|
|
1662
|
+
if (!patterns[key]) {
|
|
1663
|
+
patterns[key] = {
|
|
1664
|
+
type: d.type,
|
|
1665
|
+
decision: d.decision,
|
|
1666
|
+
decisions: [],
|
|
1667
|
+
totalScore: 0,
|
|
1668
|
+
successCount: 0,
|
|
1669
|
+
totalTimeDecayWeight: 0
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
patterns[key].decisions.push(d);
|
|
1673
|
+
patterns[key].totalScore += d.impact.score;
|
|
1674
|
+
patterns[key].totalTimeDecayWeight += d.impact.factors.timeDecayWeight;
|
|
1675
|
+
if (d.outcome?.status === OUTCOME_STATUS.SUCCESS) {
|
|
1676
|
+
patterns[key].successCount++;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
// Filter and format for recommendations
|
|
1681
|
+
return Object.values(patterns)
|
|
1682
|
+
.filter(p => {
|
|
1683
|
+
const successRate = p.successCount / p.decisions.length;
|
|
1684
|
+
return p.decisions.length >= 2 && successRate >= minSuccessRate;
|
|
1685
|
+
})
|
|
1686
|
+
.map(p => {
|
|
1687
|
+
const successRate = p.successCount / p.decisions.length;
|
|
1688
|
+
const avgScore = p.totalScore / p.decisions.length;
|
|
1689
|
+
const avgRecency = p.totalTimeDecayWeight / p.decisions.length;
|
|
1690
|
+
|
|
1691
|
+
// Calculate boost factor for recommendations
|
|
1692
|
+
// Higher for recent, successful, high-impact patterns
|
|
1693
|
+
const boostFactor = avgScore * successRate * (0.5 + avgRecency * 0.5);
|
|
1694
|
+
|
|
1695
|
+
return {
|
|
1696
|
+
type: p.type,
|
|
1697
|
+
decision: p.decision,
|
|
1698
|
+
occurrences: p.decisions.length,
|
|
1699
|
+
averageImpactScore: Math.round(avgScore * 100) / 100,
|
|
1700
|
+
successRate: Math.round(successRate * 100) / 100,
|
|
1701
|
+
recencyWeight: Math.round(avgRecency * 100) / 100,
|
|
1702
|
+
boostFactor: Math.round(boostFactor * 100) / 100,
|
|
1703
|
+
recommendation: successRate >= 0.8 ? 'STRONGLY_RECOMMENDED' :
|
|
1704
|
+
successRate >= 0.6 ? 'RECOMMENDED' : 'NEUTRAL'
|
|
1705
|
+
};
|
|
1706
|
+
})
|
|
1707
|
+
.sort((a, b) => b.boostFactor - a.boostFactor);
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
/**
|
|
1711
|
+
* Get impact statistics with enhanced analysis
|
|
1712
|
+
* @returns {object} Impact statistics
|
|
1713
|
+
*/
|
|
1714
|
+
function getImpactStats() {
|
|
1715
|
+
const decisions = readAllDecisions();
|
|
1716
|
+
|
|
1717
|
+
if (decisions.length === 0) {
|
|
1718
|
+
return {
|
|
1719
|
+
totalDecisions: 0,
|
|
1720
|
+
byLevel: {},
|
|
1721
|
+
averageScore: 0,
|
|
1722
|
+
highestImpact: null,
|
|
1723
|
+
recentTrend: null,
|
|
1724
|
+
impactBoostedPatterns: []
|
|
1725
|
+
};
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
const withImpact = decisions.map(d => ({
|
|
1729
|
+
...d,
|
|
1730
|
+
impact: calculateImpactScore(d)
|
|
1731
|
+
}));
|
|
1732
|
+
|
|
1733
|
+
const byLevel = {};
|
|
1734
|
+
let totalScore = 0;
|
|
1735
|
+
let highestImpact = null;
|
|
1736
|
+
|
|
1737
|
+
for (const d of withImpact) {
|
|
1738
|
+
const level = d.impact.level;
|
|
1739
|
+
byLevel[level] = (byLevel[level] || 0) + 1;
|
|
1740
|
+
totalScore += d.impact.score;
|
|
1741
|
+
|
|
1742
|
+
if (!highestImpact || d.impact.score > highestImpact.impact.score) {
|
|
1743
|
+
highestImpact = d;
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// Calculate recent trend (last 7 days vs previous 7 days)
|
|
1748
|
+
const now = new Date();
|
|
1749
|
+
const oneWeekAgo = new Date(now);
|
|
1750
|
+
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
|
|
1751
|
+
const twoWeeksAgo = new Date(now);
|
|
1752
|
+
twoWeeksAgo.setDate(twoWeeksAgo.getDate() - 14);
|
|
1753
|
+
|
|
1754
|
+
const recentDecisions = withImpact.filter(d => new Date(d.timestamp) >= oneWeekAgo);
|
|
1755
|
+
const previousDecisions = withImpact.filter(d => {
|
|
1756
|
+
const dt = new Date(d.timestamp);
|
|
1757
|
+
return dt >= twoWeeksAgo && dt < oneWeekAgo;
|
|
1758
|
+
});
|
|
1759
|
+
|
|
1760
|
+
const recentAvg = recentDecisions.length > 0
|
|
1761
|
+
? recentDecisions.reduce((sum, d) => sum + d.impact.score, 0) / recentDecisions.length
|
|
1762
|
+
: 0;
|
|
1763
|
+
const previousAvg = previousDecisions.length > 0
|
|
1764
|
+
? previousDecisions.reduce((sum, d) => sum + d.impact.score, 0) / previousDecisions.length
|
|
1765
|
+
: 0;
|
|
1766
|
+
|
|
1767
|
+
const trendDirection = recentAvg > previousAvg ? 'improving' :
|
|
1768
|
+
recentAvg < previousAvg ? 'declining' : 'stable';
|
|
1769
|
+
|
|
1770
|
+
return {
|
|
1771
|
+
totalDecisions: decisions.length,
|
|
1772
|
+
byLevel,
|
|
1773
|
+
averageScore: Math.round((totalScore / decisions.length) * 100) / 100,
|
|
1774
|
+
highestImpact: highestImpact ? {
|
|
1775
|
+
id: highestImpact.id,
|
|
1776
|
+
type: highestImpact.type,
|
|
1777
|
+
decision: highestImpact.decision,
|
|
1778
|
+
score: highestImpact.impact.score,
|
|
1779
|
+
level: highestImpact.impact.level
|
|
1780
|
+
} : null,
|
|
1781
|
+
recentTrend: {
|
|
1782
|
+
direction: trendDirection,
|
|
1783
|
+
recentAverage: Math.round(recentAvg * 100) / 100,
|
|
1784
|
+
previousAverage: Math.round(previousAvg * 100) / 100,
|
|
1785
|
+
recentCount: recentDecisions.length,
|
|
1786
|
+
previousCount: previousDecisions.length
|
|
1787
|
+
},
|
|
1788
|
+
impactBoostedPatterns: getImpactBoostedPatterns({ minScore: 3.0, minSuccessRate: 0.6 }).slice(0, 5)
|
|
1789
|
+
};
|
|
423
1790
|
}
|
|
424
1791
|
|
|
425
1792
|
module.exports = {
|
|
1793
|
+
// Constants
|
|
426
1794
|
DECISION_TYPES,
|
|
427
1795
|
OUTCOME_STATUS,
|
|
1796
|
+
IMPACT_LEVELS,
|
|
1797
|
+
TIME_DECAY,
|
|
1798
|
+
IMPACT_FACTORS,
|
|
1799
|
+
|
|
1800
|
+
// Core decision tracking
|
|
428
1801
|
logDecision,
|
|
429
1802
|
recordOutcome,
|
|
430
1803
|
readAllDecisions,
|
|
1804
|
+
getDecisionById,
|
|
431
1805
|
getDecisionsByType,
|
|
432
1806
|
getDecisionsByOutcome,
|
|
433
1807
|
getRecentDecisions,
|
|
@@ -435,5 +1809,31 @@ module.exports = {
|
|
|
435
1809
|
getRecommendation,
|
|
436
1810
|
getStats,
|
|
437
1811
|
getPatterns,
|
|
438
|
-
clearHistory
|
|
1812
|
+
clearHistory,
|
|
1813
|
+
compact,
|
|
1814
|
+
shouldCompact,
|
|
1815
|
+
flushIndex,
|
|
1816
|
+
rebuildIndex,
|
|
1817
|
+
|
|
1818
|
+
// Impact scoring (enhanced)
|
|
1819
|
+
calculateImpactScore,
|
|
1820
|
+
calculateTimeDecay,
|
|
1821
|
+
calculateDownstreamImpact,
|
|
1822
|
+
getHighImpactDecisions,
|
|
1823
|
+
getImpactStats,
|
|
1824
|
+
|
|
1825
|
+
// Decision chains
|
|
1826
|
+
linkDecisions,
|
|
1827
|
+
getDownstreamDecisions,
|
|
1828
|
+
getUpstreamDecisions,
|
|
1829
|
+
trackSuccessChain,
|
|
1830
|
+
getSuccessfulDecisionPatterns,
|
|
1831
|
+
|
|
1832
|
+
// Impact reports and analysis
|
|
1833
|
+
generateImpactReport,
|
|
1834
|
+
getImpactBoostedPatterns,
|
|
1835
|
+
|
|
1836
|
+
// Utility functions
|
|
1837
|
+
getRelatedTypes,
|
|
1838
|
+
countDownstreamDecisions
|
|
439
1839
|
};
|