scene-capability-engine 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2513 -0
- package/LICENSE +21 -0
- package/README.md +765 -0
- package/README.zh.md +630 -0
- package/bin/kiro-spec-engine.js +796 -0
- package/bin/kse.js +3 -0
- package/bin/sce.js +3 -0
- package/bin/sco.js +3 -0
- package/docs/331-poc-adaptation-roadmap.md +156 -0
- package/docs/331-poc-dual-track-integration-guide.md +120 -0
- package/docs/331-poc-weekly-delivery-checklist.md +52 -0
- package/docs/OFFLINE_INSTALL.md +96 -0
- package/docs/README.md +279 -0
- package/docs/adopt-migration-guide.md +599 -0
- package/docs/adoption-guide.md +616 -0
- package/docs/agent-hooks-analysis.md +815 -0
- package/docs/architecture.md +733 -0
- package/docs/articles/ai-driven-development-philosophy-and-practice-review.md +208 -0
- package/docs/articles/ai-driven-development-philosophy-and-practice.en.md +459 -0
- package/docs/articles/ai-driven-development-philosophy-and-practice.md +492 -0
- package/docs/autonomous-control-guide.md +851 -0
- package/docs/command-reference.md +1368 -0
- package/docs/community.md +115 -0
- package/docs/cross-tool-guide.md +555 -0
- package/docs/developer-guide.md +619 -0
- package/docs/document-governance.md +865 -0
- package/docs/environment-management-guide.md +526 -0
- package/docs/examples/add-export-command/design.md +194 -0
- package/docs/examples/add-export-command/requirements.md +110 -0
- package/docs/examples/add-export-command/tasks.md +88 -0
- package/docs/examples/add-rest-api/design.md +855 -0
- package/docs/examples/add-rest-api/requirements.md +323 -0
- package/docs/examples/add-rest-api/tasks.md +355 -0
- package/docs/examples/add-user-dashboard/design.md +192 -0
- package/docs/examples/add-user-dashboard/requirements.md +143 -0
- package/docs/examples/add-user-dashboard/tasks.md +91 -0
- package/docs/faq.md +697 -0
- package/docs/handoffs/evidence/ontology/moqui-template-baseline-2026-02-17-232922.json +156 -0
- package/docs/handoffs/evidence/ontology/moqui-template-baseline-2026-02-17-232922.md +24 -0
- package/docs/images/wechat-qr.png +0 -0
- package/docs/integration-modes.md +529 -0
- package/docs/integration-philosophy.md +313 -0
- package/docs/knowledge-management-guide.md +263 -0
- package/docs/manual-workflows-guide.md +418 -0
- package/docs/moqui-capability-matrix.md +73 -0
- package/docs/moqui-template-core-library-playbook.md +109 -0
- package/docs/multi-agent-coordination-guide.md +553 -0
- package/docs/multi-repo-management-guide.md +1344 -0
- package/docs/quick-start-with-ai-tools.md +375 -0
- package/docs/quick-start.md +146 -0
- package/docs/release-checklist.md +121 -0
- package/docs/releases/README.md +13 -0
- package/docs/releases/v1.46.2-validation.md +45 -0
- package/docs/releases/v1.46.2.md +50 -0
- package/docs/scene-runtime-guide.md +347 -0
- package/docs/spec-collaboration-guide.md +369 -0
- package/docs/spec-locking-guide.md +225 -0
- package/docs/spec-numbering-guide.md +348 -0
- package/docs/spec-workflow.md +519 -0
- package/docs/steering-strategy-guide.md +196 -0
- package/docs/team-collaboration-guide.md +465 -0
- package/docs/testing-strategy.md +272 -0
- package/docs/tools/claude-guide.md +654 -0
- package/docs/tools/cursor-guide.md +706 -0
- package/docs/tools/generic-guide.md +446 -0
- package/docs/tools/kiro-guide.md +308 -0
- package/docs/tools/vscode-guide.md +445 -0
- package/docs/tools/windsurf-guide.md +391 -0
- package/docs/troubleshooting.md +1135 -0
- package/docs/upgrade-guide.md +639 -0
- package/docs/value-observability-guide.md +127 -0
- package/docs/zh/README.md +341 -0
- package/docs/zh/quick-start.md +764 -0
- package/docs/zh/release-checklist.md +121 -0
- package/docs/zh/releases/README.md +13 -0
- package/docs/zh/releases/v1.46.2-validation.md +45 -0
- package/docs/zh/releases/v1.46.2.md +50 -0
- package/docs/zh/spec-numbering-guide.md +348 -0
- package/docs/zh/tools/claude-guide.md +349 -0
- package/docs/zh/tools/cursor-guide.md +281 -0
- package/docs/zh/tools/generic-guide.md +499 -0
- package/docs/zh/tools/kiro-guide.md +342 -0
- package/docs/zh/tools/vscode-guide.md +449 -0
- package/docs/zh/tools/windsurf-guide.md +378 -0
- package/docs/zh/value-observability-guide.md +127 -0
- package/docs//344/272/244/344/273/230/346/270/205/345/215/225.md +75 -0
- package/lib/adoption/adoption-logger.js +487 -0
- package/lib/adoption/adoption-strategy.js +538 -0
- package/lib/adoption/backup-manager.js +420 -0
- package/lib/adoption/conflict-resolver.js +410 -0
- package/lib/adoption/detection-engine.js +275 -0
- package/lib/adoption/diff-viewer.js +226 -0
- package/lib/adoption/error-formatter.js +509 -0
- package/lib/adoption/file-classifier.js +385 -0
- package/lib/adoption/progress-reporter.js +534 -0
- package/lib/adoption/smart-orchestrator.js +470 -0
- package/lib/adoption/strategy-selector.js +218 -0
- package/lib/adoption/summary-generator.js +493 -0
- package/lib/adoption/template-sync.js +605 -0
- package/lib/auto/autonomous-engine.js +485 -0
- package/lib/auto/checkpoint-manager.js +300 -0
- package/lib/auto/close-loop-runner.js +2476 -0
- package/lib/auto/config-schema.js +176 -0
- package/lib/auto/decision-engine.js +344 -0
- package/lib/auto/error-recovery-manager.js +580 -0
- package/lib/auto/goal-decomposer.js +278 -0
- package/lib/auto/progress-tracker.js +502 -0
- package/lib/auto/safety-manager.js +186 -0
- package/lib/auto/semantic-decomposer.js +137 -0
- package/lib/auto/state-manager.js +126 -0
- package/lib/auto/task-queue-manager.js +340 -0
- package/lib/backup/backup-system.js +372 -0
- package/lib/backup/selective-backup.js +207 -0
- package/lib/collab/agent-registry.js +240 -0
- package/lib/collab/collab-manager.js +285 -0
- package/lib/collab/contract-manager.js +320 -0
- package/lib/collab/coordinator.js +370 -0
- package/lib/collab/dependency-manager.js +280 -0
- package/lib/collab/index.js +20 -0
- package/lib/collab/integration-manager.js +202 -0
- package/lib/collab/merge-coordinator.js +252 -0
- package/lib/collab/metadata-manager.js +233 -0
- package/lib/collab/multi-agent-config.js +120 -0
- package/lib/collab/spec-lifecycle-manager.js +304 -0
- package/lib/collab/sync-barrier.js +88 -0
- package/lib/collab/visualizer.js +208 -0
- package/lib/commands/adopt.js +749 -0
- package/lib/commands/auto.js +19559 -0
- package/lib/commands/collab.js +275 -0
- package/lib/commands/context.js +99 -0
- package/lib/commands/docs.js +808 -0
- package/lib/commands/doctor.js +273 -0
- package/lib/commands/env.js +420 -0
- package/lib/commands/knowledge.js +309 -0
- package/lib/commands/lock.js +235 -0
- package/lib/commands/ops.js +409 -0
- package/lib/commands/orchestrate.js +446 -0
- package/lib/commands/prompt.js +105 -0
- package/lib/commands/repo.js +118 -0
- package/lib/commands/rollback.js +219 -0
- package/lib/commands/scene.js +15549 -0
- package/lib/commands/spec-bootstrap.js +147 -0
- package/lib/commands/spec-gate.js +157 -0
- package/lib/commands/spec-pipeline.js +205 -0
- package/lib/commands/status.js +321 -0
- package/lib/commands/task.js +199 -0
- package/lib/commands/templates.js +654 -0
- package/lib/commands/upgrade.js +231 -0
- package/lib/commands/value.js +569 -0
- package/lib/commands/watch.js +684 -0
- package/lib/commands/workflows.js +240 -0
- package/lib/commands/workspace-multi.js +325 -0
- package/lib/commands/workspace.js +189 -0
- package/lib/context/context-exporter.js +378 -0
- package/lib/context/prompt-generator.js +482 -0
- package/lib/data/moqui-capability-lexicon.json +45 -0
- package/lib/environment/backup-system.js +189 -0
- package/lib/environment/environment-manager.js +379 -0
- package/lib/environment/environment-registry.js +168 -0
- package/lib/gitignore/gitignore-backup.js +229 -0
- package/lib/gitignore/gitignore-detector.js +239 -0
- package/lib/gitignore/gitignore-integration.js +267 -0
- package/lib/gitignore/gitignore-transformer.js +193 -0
- package/lib/gitignore/layered-rules-template.js +42 -0
- package/lib/governance/archive-tool.js +284 -0
- package/lib/governance/cleanup-tool.js +237 -0
- package/lib/governance/config-manager.js +186 -0
- package/lib/governance/diagnostic-engine.js +271 -0
- package/lib/governance/doc-reference-checker.js +200 -0
- package/lib/governance/execution-logger.js +243 -0
- package/lib/governance/file-scanner.js +285 -0
- package/lib/governance/hooks-manager.js +333 -0
- package/lib/governance/reporter.js +337 -0
- package/lib/governance/validation-engine.js +181 -0
- package/lib/i18n.js +79 -0
- package/lib/knowledge/entry-manager.js +208 -0
- package/lib/knowledge/index-manager.js +261 -0
- package/lib/knowledge/knowledge-manager.js +273 -0
- package/lib/knowledge/template-manager.js +191 -0
- package/lib/lock/index.js +21 -0
- package/lib/lock/lock-file.js +192 -0
- package/lib/lock/lock-manager.js +321 -0
- package/lib/lock/machine-identifier.js +135 -0
- package/lib/lock/steering-file-lock.js +207 -0
- package/lib/lock/task-lock-manager.js +345 -0
- package/lib/operations/audit-logger.js +293 -0
- package/lib/operations/feedback-manager.js +1147 -0
- package/lib/operations/index.js +23 -0
- package/lib/operations/models/index.js +170 -0
- package/lib/operations/operations-manager.js +151 -0
- package/lib/operations/operations-validator.js +280 -0
- package/lib/operations/permission-manager.js +354 -0
- package/lib/operations/template-loader.js +143 -0
- package/lib/orchestrator/agent-spawner.js +629 -0
- package/lib/orchestrator/bootstrap-prompt-builder.js +236 -0
- package/lib/orchestrator/index.js +19 -0
- package/lib/orchestrator/orchestration-engine.js +1270 -0
- package/lib/orchestrator/orchestrator-config.js +173 -0
- package/lib/orchestrator/status-monitor.js +591 -0
- package/lib/python-checker.js +209 -0
- package/lib/repo/config-manager.js +580 -0
- package/lib/repo/errors/config-error.js +13 -0
- package/lib/repo/errors/git-error.js +15 -0
- package/lib/repo/errors/repo-error.js +14 -0
- package/lib/repo/git-operations.js +181 -0
- package/lib/repo/handlers/.gitkeep +1 -0
- package/lib/repo/handlers/exec-handler.js +155 -0
- package/lib/repo/handlers/health-handler.js +169 -0
- package/lib/repo/handlers/init-handler.js +197 -0
- package/lib/repo/handlers/status-handler.js +176 -0
- package/lib/repo/output-formatter.js +184 -0
- package/lib/repo/path-resolver.js +178 -0
- package/lib/repo/repo-manager.js +514 -0
- package/lib/scene-runtime/audit-emitter.js +59 -0
- package/lib/scene-runtime/binding-plugin-loader.js +351 -0
- package/lib/scene-runtime/binding-registry.js +349 -0
- package/lib/scene-runtime/eval-bridge.js +44 -0
- package/lib/scene-runtime/index.js +19 -0
- package/lib/scene-runtime/moqui-adapter.js +620 -0
- package/lib/scene-runtime/moqui-client.js +606 -0
- package/lib/scene-runtime/moqui-extractor.js +2029 -0
- package/lib/scene-runtime/plan-compiler.js +208 -0
- package/lib/scene-runtime/policy-gate.js +58 -0
- package/lib/scene-runtime/runtime-executor.js +358 -0
- package/lib/scene-runtime/scene-loader.js +96 -0
- package/lib/scene-runtime/scene-ontology.js +959 -0
- package/lib/scene-runtime/scene-template-linter.js +852 -0
- package/lib/scene-runtime/templates/scene-template-erp-query-v0.1.yaml +28 -0
- package/lib/scene-runtime/templates/scene-template-hybrid-shadow-v0.1.yaml +34 -0
- package/lib/spec/bootstrap/context-collector.js +48 -0
- package/lib/spec/bootstrap/draft-generator.js +158 -0
- package/lib/spec/bootstrap/questionnaire-engine.js +70 -0
- package/lib/spec/bootstrap/trace-emitter.js +59 -0
- package/lib/spec/multi-spec-orchestrate.js +93 -0
- package/lib/spec/pipeline/constants.js +6 -0
- package/lib/spec/pipeline/stage-adapters.js +118 -0
- package/lib/spec/pipeline/stage-runner.js +146 -0
- package/lib/spec/pipeline/state-store.js +119 -0
- package/lib/spec-gate/engine/gate-engine.js +165 -0
- package/lib/spec-gate/policy/default-policy.js +22 -0
- package/lib/spec-gate/policy/policy-loader.js +103 -0
- package/lib/spec-gate/result-emitter.js +81 -0
- package/lib/spec-gate/rules/default-rules.js +156 -0
- package/lib/spec-gate/rules/rule-registry.js +51 -0
- package/lib/steering/adoption-config.js +164 -0
- package/lib/steering/compliance-auto-fixer.js +204 -0
- package/lib/steering/compliance-cache.js +99 -0
- package/lib/steering/compliance-error-reporter.js +70 -0
- package/lib/steering/context-sync-manager.js +273 -0
- package/lib/steering/index.js +92 -0
- package/lib/steering/spec-steering.js +230 -0
- package/lib/steering/steering-compliance-checker.js +73 -0
- package/lib/steering/steering-loader.js +144 -0
- package/lib/steering/steering-manager.js +289 -0
- package/lib/task/index.js +12 -0
- package/lib/task/task-claimer.js +489 -0
- package/lib/task/task-status-store.js +418 -0
- package/lib/templates/cache-manager.js +440 -0
- package/lib/templates/content-generalizer.js +247 -0
- package/lib/templates/frontmatter-generator.js +128 -0
- package/lib/templates/git-handler.js +471 -0
- package/lib/templates/metadata-collector.js +328 -0
- package/lib/templates/path-utils.js +144 -0
- package/lib/templates/registry-parser.js +505 -0
- package/lib/templates/spec-reader.js +216 -0
- package/lib/templates/template-applicator.js +249 -0
- package/lib/templates/template-creator.js +256 -0
- package/lib/templates/template-error.js +143 -0
- package/lib/templates/template-exporter.js +502 -0
- package/lib/templates/template-manager.js +782 -0
- package/lib/templates/template-validator.js +361 -0
- package/lib/upgrade/migration-engine.js +382 -0
- package/lib/upgrade/migrations/.gitkeep +52 -0
- package/lib/upgrade/migrations/1.0.0-to-1.1.0.js +78 -0
- package/lib/utils/file-diff.js +177 -0
- package/lib/utils/fs-utils.js +274 -0
- package/lib/utils/tool-detector.js +383 -0
- package/lib/utils/validation.js +324 -0
- package/lib/value/gate-summary-emitter.js +99 -0
- package/lib/value/metric-contract-loader.js +210 -0
- package/lib/value/risk-evaluator.js +117 -0
- package/lib/value/weekly-snapshot-builder.js +61 -0
- package/lib/version/version-checker.js +156 -0
- package/lib/version/version-manager.js +327 -0
- package/lib/watch/action-executor.js +458 -0
- package/lib/watch/event-debouncer.js +323 -0
- package/lib/watch/execution-logger.js +550 -0
- package/lib/watch/file-watcher.js +499 -0
- package/lib/watch/presets.js +266 -0
- package/lib/watch/watch-manager.js +533 -0
- package/lib/workspace/multi/global-config.js +150 -0
- package/lib/workspace/multi/index.js +22 -0
- package/lib/workspace/multi/path-utils.js +173 -0
- package/lib/workspace/multi/workspace-context-resolver.js +244 -0
- package/lib/workspace/multi/workspace-registry.js +196 -0
- package/lib/workspace/multi/workspace-state-manager.js +537 -0
- package/lib/workspace/multi/workspace.js +90 -0
- package/lib/workspace/workspace-manager.js +370 -0
- package/lib/workspace/workspace-sync.js +356 -0
- package/locales/en.json +114 -0
- package/locales/zh.json +114 -0
- package/package.json +102 -0
- package/template/.kiro/README.md +247 -0
- package/template/.kiro/hooks/check-spec-on-create.kiro.hook +17 -0
- package/template/.kiro/hooks/run-tests-on-save.kiro.hook +13 -0
- package/template/.kiro/hooks/sync-tasks-on-edit.kiro.hook +16 -0
- package/template/.kiro/specs/SPEC_WORKFLOW_GUIDE.md +134 -0
- package/template/.kiro/steering/CORE_PRINCIPLES.md +133 -0
- package/template/.kiro/steering/CURRENT_CONTEXT.md +30 -0
- package/template/.kiro/steering/ENVIRONMENT.md +35 -0
- package/template/.kiro/steering/RULES_GUIDE.md +46 -0
- package/template/.kiro/templates/operations/default/change-impact.md +112 -0
- package/template/.kiro/templates/operations/default/deployment.md +91 -0
- package/template/.kiro/templates/operations/default/feedback-response.md +269 -0
- package/template/.kiro/templates/operations/default/migration-plan.md +172 -0
- package/template/.kiro/templates/operations/default/monitoring.md +135 -0
- package/template/.kiro/templates/operations/default/operations.md +135 -0
- package/template/.kiro/templates/operations/default/rollback.md +143 -0
- package/template/.kiro/templates/operations/default/tools.yaml +364 -0
- package/template/.kiro/templates/operations/default/troubleshooting.md +123 -0
- package/template/.kiro/tools/backup_manager.py +295 -0
- package/template/.kiro/tools/configuration_manager.py +218 -0
- package/template/.kiro/tools/document_evaluator.py +550 -0
- package/template/.kiro/tools/enhancement_logger.py +168 -0
- package/template/.kiro/tools/error_handler.py +335 -0
- package/template/.kiro/tools/improvement_identifier.py +444 -0
- package/template/.kiro/tools/modification_applicator.py +737 -0
- package/template/.kiro/tools/quality_gate_enforcer.py +207 -0
- package/template/.kiro/tools/quality_scorer.py +305 -0
- package/template/.kiro/tools/report_generator.py +154 -0
- package/template/.kiro/tools/ultrawork_enhancer.py +676 -0
- package/template/.kiro/tools/ultrawork_enhancer_refactored.py +0 -0
- package/template/.kiro/tools/ultrawork_enhancer_v2.py +463 -0
- package/template/.kiro/tools/ultrawork_enhancer_v3.py +606 -0
- package/template/.kiro/tools/workflow_quality_gate.py +100 -0
- package/template/README.md +111 -0
|
@@ -0,0 +1,1147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feedback Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages user feedback collection, classification, routing, and resolution tracking.
|
|
5
|
+
* Integrates with operations specs to improve operational procedures based on feedback.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const crypto = require('crypto');
|
|
10
|
+
const {
|
|
11
|
+
FeedbackChannel,
|
|
12
|
+
FeedbackType,
|
|
13
|
+
FeedbackSeverity,
|
|
14
|
+
FeedbackStatus
|
|
15
|
+
} = require('./models');
|
|
16
|
+
const {
|
|
17
|
+
pathExists,
|
|
18
|
+
ensureDirectory,
|
|
19
|
+
readJSON,
|
|
20
|
+
writeJSON
|
|
21
|
+
} = require('../utils/fs-utils');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Generate UUID v4
|
|
25
|
+
* @returns {string} UUID
|
|
26
|
+
*/
|
|
27
|
+
function generateUUID() {
|
|
28
|
+
return crypto.randomUUID();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* FeedbackManager class
|
|
33
|
+
*/
|
|
34
|
+
class FeedbackManager {
|
|
35
|
+
constructor(projectPath) {
|
|
36
|
+
this.projectPath = projectPath;
|
|
37
|
+
this.feedbackDir = path.join(projectPath, '.kiro/feedback');
|
|
38
|
+
this.feedbackFile = path.join(this.feedbackDir, 'feedback.json');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Receive feedback from a channel
|
|
43
|
+
*
|
|
44
|
+
* @param {string} channel - Feedback channel (from FeedbackChannel enum)
|
|
45
|
+
* @param {Object} content - Feedback content
|
|
46
|
+
* @param {string} content.title - Feedback title
|
|
47
|
+
* @param {string} content.description - Detailed description
|
|
48
|
+
* @param {string} content.project - Project name
|
|
49
|
+
* @param {string} content.version - Project version
|
|
50
|
+
* @param {Object} content.metadata - Additional metadata
|
|
51
|
+
* @returns {Promise<Object>} Created feedback object
|
|
52
|
+
*/
|
|
53
|
+
async receiveFeedback(channel, content) {
|
|
54
|
+
// Validate channel
|
|
55
|
+
if (!Object.values(FeedbackChannel).includes(channel)) {
|
|
56
|
+
throw new Error(`Invalid feedback channel: ${channel}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Validate required content fields
|
|
60
|
+
if (!content.title || !content.description) {
|
|
61
|
+
throw new Error('Feedback must include title and description');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!content.project) {
|
|
65
|
+
throw new Error('Feedback must include project name');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Create feedback object
|
|
69
|
+
const feedback = {
|
|
70
|
+
id: generateUUID(),
|
|
71
|
+
channel,
|
|
72
|
+
type: null, // Will be classified
|
|
73
|
+
severity: null, // Will be classified
|
|
74
|
+
status: FeedbackStatus.ACKNOWLEDGED,
|
|
75
|
+
project: content.project,
|
|
76
|
+
version: content.version || 'unknown',
|
|
77
|
+
content: {
|
|
78
|
+
title: content.title,
|
|
79
|
+
description: content.description,
|
|
80
|
+
metadata: content.metadata || {}
|
|
81
|
+
},
|
|
82
|
+
classification: null, // Will be set by classifier
|
|
83
|
+
resolution: null,
|
|
84
|
+
createdAt: new Date().toISOString(),
|
|
85
|
+
updatedAt: new Date().toISOString()
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Classify feedback
|
|
89
|
+
const classification = this.classifyFeedback(feedback);
|
|
90
|
+
feedback.type = classification.type;
|
|
91
|
+
feedback.severity = classification.severity;
|
|
92
|
+
feedback.classification = classification;
|
|
93
|
+
|
|
94
|
+
// Save feedback
|
|
95
|
+
await this.saveFeedback(feedback);
|
|
96
|
+
|
|
97
|
+
// Route feedback based on severity
|
|
98
|
+
await this.routeFeedback(feedback);
|
|
99
|
+
|
|
100
|
+
return feedback;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Classify feedback by type and severity
|
|
105
|
+
*
|
|
106
|
+
* @param {Object} feedback - Feedback object
|
|
107
|
+
* @returns {Object} Classification result
|
|
108
|
+
*/
|
|
109
|
+
classifyFeedback(feedback) {
|
|
110
|
+
const content = feedback.content;
|
|
111
|
+
const text = `${content.title} ${content.description}`.toLowerCase();
|
|
112
|
+
|
|
113
|
+
// Classify type based on keywords
|
|
114
|
+
let type = FeedbackType.OPERATIONAL_CONCERN; // Default
|
|
115
|
+
let confidence = 0.5;
|
|
116
|
+
|
|
117
|
+
if (text.includes('bug') || text.includes('error') || text.includes('crash') || text.includes('fail')) {
|
|
118
|
+
type = FeedbackType.BUG_REPORT;
|
|
119
|
+
confidence = 0.9;
|
|
120
|
+
} else if (text.includes('slow') || text.includes('performance') || text.includes('timeout') || text.includes('latency')) {
|
|
121
|
+
type = FeedbackType.PERFORMANCE_ISSUE;
|
|
122
|
+
confidence = 0.85;
|
|
123
|
+
} else if (text.includes('feature') || text.includes('enhancement') || text.includes('request') || text.includes('add')) {
|
|
124
|
+
type = FeedbackType.FEATURE_REQUEST;
|
|
125
|
+
confidence = 0.8;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Classify severity based on keywords and channel
|
|
129
|
+
let severity = FeedbackSeverity.MEDIUM; // Default
|
|
130
|
+
|
|
131
|
+
if (text.includes('down') || text.includes('critical') || text.includes('urgent') || text.includes('production')) {
|
|
132
|
+
severity = FeedbackSeverity.CRITICAL;
|
|
133
|
+
} else if (text.includes('high priority') || text.includes('important') || text.includes('degraded')) {
|
|
134
|
+
severity = FeedbackSeverity.HIGH;
|
|
135
|
+
} else if (text.includes('minor') || text.includes('low priority') || text.includes('enhancement')) {
|
|
136
|
+
severity = FeedbackSeverity.LOW;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Monitoring alerts are typically high severity
|
|
140
|
+
if (feedback.channel === FeedbackChannel.MONITORING_ALERT) {
|
|
141
|
+
if (severity === FeedbackSeverity.MEDIUM) {
|
|
142
|
+
severity = FeedbackSeverity.HIGH;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
type,
|
|
148
|
+
severity,
|
|
149
|
+
confidence,
|
|
150
|
+
classifiedAt: new Date().toISOString()
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Route feedback to appropriate handler
|
|
156
|
+
*
|
|
157
|
+
* @param {Object} feedback - Feedback object
|
|
158
|
+
* @returns {Promise<void>}
|
|
159
|
+
*/
|
|
160
|
+
async routeFeedback(feedback) {
|
|
161
|
+
// Critical feedback triggers immediate response
|
|
162
|
+
if (feedback.severity === FeedbackSeverity.CRITICAL) {
|
|
163
|
+
await this.triggerCriticalResponse(feedback);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Log routing action
|
|
167
|
+
const routingLog = {
|
|
168
|
+
feedbackId: feedback.id,
|
|
169
|
+
severity: feedback.severity,
|
|
170
|
+
type: feedback.type,
|
|
171
|
+
action: feedback.severity === FeedbackSeverity.CRITICAL ? 'triggered_critical_response' : 'queued_for_review',
|
|
172
|
+
timestamp: new Date().toISOString()
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
await this.logRoutingAction(routingLog);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Trigger critical feedback response
|
|
180
|
+
*
|
|
181
|
+
* @param {Object} feedback - Critical feedback object
|
|
182
|
+
* @returns {Promise<void>}
|
|
183
|
+
*/
|
|
184
|
+
async triggerCriticalResponse(feedback) {
|
|
185
|
+
// Create response record
|
|
186
|
+
const response = {
|
|
187
|
+
feedbackId: feedback.id,
|
|
188
|
+
type: 'critical_response',
|
|
189
|
+
actions: [
|
|
190
|
+
'Escalated to on-call team',
|
|
191
|
+
'Triggered troubleshooting procedure',
|
|
192
|
+
'Notified stakeholders'
|
|
193
|
+
],
|
|
194
|
+
triggeredAt: new Date().toISOString()
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// Save response
|
|
198
|
+
await this.saveCriticalResponse(response);
|
|
199
|
+
|
|
200
|
+
// Update feedback status (only if feedback is persisted)
|
|
201
|
+
try {
|
|
202
|
+
const existingFeedback = await this.getFeedback(feedback.id);
|
|
203
|
+
if (existingFeedback) {
|
|
204
|
+
await this.trackResolution(feedback.id, FeedbackStatus.INVESTIGATING);
|
|
205
|
+
}
|
|
206
|
+
} catch (error) {
|
|
207
|
+
// Feedback not yet persisted, skip status update
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Track feedback resolution
|
|
213
|
+
*
|
|
214
|
+
* @param {string} feedbackId - Feedback ID
|
|
215
|
+
* @param {string} status - New status (from FeedbackStatus enum)
|
|
216
|
+
* @param {string} resolution - Resolution description (optional)
|
|
217
|
+
* @returns {Promise<void>}
|
|
218
|
+
*/
|
|
219
|
+
async trackResolution(feedbackId, status, resolution = null) {
|
|
220
|
+
// Validate status
|
|
221
|
+
if (!Object.values(FeedbackStatus).includes(status)) {
|
|
222
|
+
throw new Error(`Invalid feedback status: ${status}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Load feedback
|
|
226
|
+
const feedbacks = await this.loadFeedbacks();
|
|
227
|
+
const feedback = feedbacks.find(f => f.id === feedbackId);
|
|
228
|
+
|
|
229
|
+
if (!feedback) {
|
|
230
|
+
throw new Error(`Feedback not found: ${feedbackId}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Validate state transition
|
|
234
|
+
this.validateStateTransition(feedback.status, status);
|
|
235
|
+
|
|
236
|
+
// Update feedback
|
|
237
|
+
feedback.status = status;
|
|
238
|
+
feedback.updatedAt = new Date().toISOString();
|
|
239
|
+
|
|
240
|
+
if (resolution) {
|
|
241
|
+
feedback.resolution = {
|
|
242
|
+
description: resolution,
|
|
243
|
+
resolvedAt: new Date().toISOString()
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Save updated feedback
|
|
248
|
+
await this.saveFeedbacks(feedbacks);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Validate feedback state transition
|
|
253
|
+
*
|
|
254
|
+
* @param {string} currentStatus - Current status
|
|
255
|
+
* @param {string} newStatus - New status
|
|
256
|
+
* @throws {Error} If transition is invalid
|
|
257
|
+
*/
|
|
258
|
+
validateStateTransition(currentStatus, newStatus) {
|
|
259
|
+
const validTransitions = {
|
|
260
|
+
[FeedbackStatus.ACKNOWLEDGED]: [FeedbackStatus.INVESTIGATING, FeedbackStatus.RESOLVED],
|
|
261
|
+
[FeedbackStatus.INVESTIGATING]: [FeedbackStatus.RESOLVED, FeedbackStatus.ACKNOWLEDGED],
|
|
262
|
+
[FeedbackStatus.RESOLVED]: [FeedbackStatus.VERIFIED, FeedbackStatus.INVESTIGATING],
|
|
263
|
+
[FeedbackStatus.VERIFIED]: [] // Terminal state
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const allowed = validTransitions[currentStatus] || [];
|
|
267
|
+
|
|
268
|
+
if (!allowed.includes(newStatus) && currentStatus !== newStatus) {
|
|
269
|
+
throw new Error(`Invalid state transition: ${currentStatus} -> ${newStatus}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Get feedback by ID
|
|
275
|
+
*
|
|
276
|
+
* @param {string} feedbackId - Feedback ID
|
|
277
|
+
* @returns {Promise<Object|null>} Feedback object or null
|
|
278
|
+
*/
|
|
279
|
+
async getFeedback(feedbackId) {
|
|
280
|
+
const feedbacks = await this.loadFeedbacks();
|
|
281
|
+
return feedbacks.find(f => f.id === feedbackId) || null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* List feedbacks with optional filters
|
|
286
|
+
*
|
|
287
|
+
* @param {Object} filters - Filter criteria
|
|
288
|
+
* @param {string} filters.project - Filter by project
|
|
289
|
+
* @param {string} filters.severity - Filter by severity
|
|
290
|
+
* @param {string} filters.status - Filter by status
|
|
291
|
+
* @param {string} filters.type - Filter by type
|
|
292
|
+
* @returns {Promise<Array>} Array of feedback objects
|
|
293
|
+
*/
|
|
294
|
+
async listFeedbacks(filters = {}) {
|
|
295
|
+
let feedbacks = await this.loadFeedbacks();
|
|
296
|
+
|
|
297
|
+
// Apply filters
|
|
298
|
+
if (filters.project) {
|
|
299
|
+
feedbacks = feedbacks.filter(f => f.project === filters.project);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (filters.severity) {
|
|
303
|
+
feedbacks = feedbacks.filter(f => f.severity === filters.severity);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (filters.status) {
|
|
307
|
+
feedbacks = feedbacks.filter(f => f.status === filters.status);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (filters.type) {
|
|
311
|
+
feedbacks = feedbacks.filter(f => f.type === filters.type);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return feedbacks;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Link feedback to project version
|
|
319
|
+
*
|
|
320
|
+
* @param {string} feedbackId - Feedback ID
|
|
321
|
+
* @param {string} version - Project version
|
|
322
|
+
* @returns {Promise<void>}
|
|
323
|
+
*/
|
|
324
|
+
async linkToVersion(feedbackId, version) {
|
|
325
|
+
const feedbacks = await this.loadFeedbacks();
|
|
326
|
+
const feedback = feedbacks.find(f => f.id === feedbackId);
|
|
327
|
+
|
|
328
|
+
if (!feedback) {
|
|
329
|
+
throw new Error(`Feedback not found: ${feedbackId}`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
feedback.version = version;
|
|
333
|
+
feedback.updatedAt = new Date().toISOString();
|
|
334
|
+
|
|
335
|
+
await this.saveFeedbacks(feedbacks);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Save feedback to storage
|
|
340
|
+
*
|
|
341
|
+
* @param {Object} feedback - Feedback object
|
|
342
|
+
* @returns {Promise<void>}
|
|
343
|
+
*/
|
|
344
|
+
async saveFeedback(feedback) {
|
|
345
|
+
const feedbacks = await this.loadFeedbacks();
|
|
346
|
+
feedbacks.push(feedback);
|
|
347
|
+
await this.saveFeedbacks(feedbacks);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Load all feedbacks from storage
|
|
352
|
+
*
|
|
353
|
+
* @returns {Promise<Array>} Array of feedback objects
|
|
354
|
+
*/
|
|
355
|
+
async loadFeedbacks() {
|
|
356
|
+
await ensureDirectory(this.feedbackDir);
|
|
357
|
+
|
|
358
|
+
const exists = await pathExists(this.feedbackFile);
|
|
359
|
+
if (!exists) {
|
|
360
|
+
return [];
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
const data = await readJSON(this.feedbackFile);
|
|
365
|
+
return data.feedbacks || [];
|
|
366
|
+
} catch (error) {
|
|
367
|
+
console.warn(`Warning: Could not load feedbacks: ${error.message}`);
|
|
368
|
+
return [];
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Save feedbacks to storage
|
|
374
|
+
*
|
|
375
|
+
* @param {Array} feedbacks - Array of feedback objects
|
|
376
|
+
* @returns {Promise<void>}
|
|
377
|
+
*/
|
|
378
|
+
async saveFeedbacks(feedbacks) {
|
|
379
|
+
await ensureDirectory(this.feedbackDir);
|
|
380
|
+
|
|
381
|
+
const data = {
|
|
382
|
+
feedbacks,
|
|
383
|
+
lastUpdated: new Date().toISOString()
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
await writeJSON(this.feedbackFile, data);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Log routing action
|
|
391
|
+
*
|
|
392
|
+
* @param {Object} routingLog - Routing log entry
|
|
393
|
+
* @returns {Promise<void>}
|
|
394
|
+
*/
|
|
395
|
+
async logRoutingAction(routingLog) {
|
|
396
|
+
const logFile = path.join(this.feedbackDir, 'routing-log.json');
|
|
397
|
+
await ensureDirectory(this.feedbackDir);
|
|
398
|
+
|
|
399
|
+
let logs = [];
|
|
400
|
+
const exists = await pathExists(logFile);
|
|
401
|
+
if (exists) {
|
|
402
|
+
try {
|
|
403
|
+
const data = await readJSON(logFile);
|
|
404
|
+
logs = data.logs || [];
|
|
405
|
+
} catch (error) {
|
|
406
|
+
// Ignore errors, start fresh
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
logs.push(routingLog);
|
|
411
|
+
|
|
412
|
+
await writeJSON(logFile, { logs, lastUpdated: new Date().toISOString() });
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Save critical response
|
|
417
|
+
*
|
|
418
|
+
* @param {Object} response - Critical response object
|
|
419
|
+
* @returns {Promise<void>}
|
|
420
|
+
*/
|
|
421
|
+
async saveCriticalResponse(response) {
|
|
422
|
+
const responseFile = path.join(this.feedbackDir, 'critical-responses.json');
|
|
423
|
+
await ensureDirectory(this.feedbackDir);
|
|
424
|
+
|
|
425
|
+
let responses = [];
|
|
426
|
+
const exists = await pathExists(responseFile);
|
|
427
|
+
if (exists) {
|
|
428
|
+
try {
|
|
429
|
+
const data = await readJSON(responseFile);
|
|
430
|
+
responses = data.responses || [];
|
|
431
|
+
} catch (error) {
|
|
432
|
+
// Ignore errors, start fresh
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
responses.push(response);
|
|
437
|
+
|
|
438
|
+
await writeJSON(responseFile, { responses, lastUpdated: new Date().toISOString() });
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Generate feedback analytics
|
|
443
|
+
*
|
|
444
|
+
* @param {string} project - Project name
|
|
445
|
+
* @param {Object} timeRange - Time range for analytics
|
|
446
|
+
* @param {string} timeRange.from - Start date (ISO)
|
|
447
|
+
* @param {string} timeRange.to - End date (ISO)
|
|
448
|
+
* @returns {Promise<Object>} FeedbackAnalytics object
|
|
449
|
+
*/
|
|
450
|
+
async generateAnalytics(project, timeRange) {
|
|
451
|
+
// Load all feedbacks for the project
|
|
452
|
+
const allFeedbacks = await this.listFeedbacks({ project });
|
|
453
|
+
|
|
454
|
+
// Filter by time range
|
|
455
|
+
const fromDate = new Date(timeRange.from);
|
|
456
|
+
const toDate = new Date(timeRange.to);
|
|
457
|
+
|
|
458
|
+
const feedbacks = allFeedbacks.filter(f => {
|
|
459
|
+
const createdAt = new Date(f.createdAt);
|
|
460
|
+
return createdAt >= fromDate && createdAt <= toDate;
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// Generate analytics components
|
|
464
|
+
const commonIssues = this._analyzeCommonIssues(feedbacks);
|
|
465
|
+
const resolutionTimes = this._calculateResolutionTimes(feedbacks);
|
|
466
|
+
const satisfactionTrends = this._trackSatisfactionTrends(feedbacks, fromDate, toDate);
|
|
467
|
+
const versionSpecificIssues = this._identifyVersionIssues(feedbacks);
|
|
468
|
+
|
|
469
|
+
// Calculate status distribution
|
|
470
|
+
const statusDistribution = {};
|
|
471
|
+
Object.values(FeedbackStatus).forEach(status => {
|
|
472
|
+
statusDistribution[status] = feedbacks.filter(f => f.status === status).length;
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
project,
|
|
477
|
+
generatedAt: new Date().toISOString(),
|
|
478
|
+
timeRange: {
|
|
479
|
+
from: timeRange.from,
|
|
480
|
+
to: timeRange.to
|
|
481
|
+
},
|
|
482
|
+
commonIssues,
|
|
483
|
+
resolutionTimes,
|
|
484
|
+
satisfactionTrends,
|
|
485
|
+
versionSpecificIssues,
|
|
486
|
+
totalFeedback: feedbacks.length,
|
|
487
|
+
statusDistribution
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Analyze common issue patterns
|
|
493
|
+
*
|
|
494
|
+
* @param {Array} feedbacks - Array of feedback objects
|
|
495
|
+
* @returns {Array} Array of IssuePattern objects
|
|
496
|
+
* @private
|
|
497
|
+
*/
|
|
498
|
+
_analyzeCommonIssues(feedbacks) {
|
|
499
|
+
// Group feedbacks by type and severity
|
|
500
|
+
const patterns = new Map();
|
|
501
|
+
|
|
502
|
+
feedbacks.forEach(feedback => {
|
|
503
|
+
const key = `${feedback.type}_${feedback.severity}`;
|
|
504
|
+
|
|
505
|
+
if (!patterns.has(key)) {
|
|
506
|
+
patterns.set(key, {
|
|
507
|
+
pattern: `${feedback.type} with ${feedback.severity} severity`,
|
|
508
|
+
occurrences: 0,
|
|
509
|
+
type: feedback.type,
|
|
510
|
+
severity: feedback.severity,
|
|
511
|
+
affectedVersions: new Set(),
|
|
512
|
+
firstSeen: feedback.createdAt,
|
|
513
|
+
lastSeen: feedback.createdAt
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const pattern = patterns.get(key);
|
|
518
|
+
pattern.occurrences++;
|
|
519
|
+
pattern.affectedVersions.add(feedback.version);
|
|
520
|
+
|
|
521
|
+
// Update first/last seen
|
|
522
|
+
if (new Date(feedback.createdAt) < new Date(pattern.firstSeen)) {
|
|
523
|
+
pattern.firstSeen = feedback.createdAt;
|
|
524
|
+
}
|
|
525
|
+
if (new Date(feedback.createdAt) > new Date(pattern.lastSeen)) {
|
|
526
|
+
pattern.lastSeen = feedback.createdAt;
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
// Convert to array and sort by occurrences
|
|
531
|
+
const issuePatterns = Array.from(patterns.values())
|
|
532
|
+
.map(p => ({
|
|
533
|
+
...p,
|
|
534
|
+
affectedVersions: Array.from(p.affectedVersions)
|
|
535
|
+
}))
|
|
536
|
+
.sort((a, b) => b.occurrences - a.occurrences)
|
|
537
|
+
.slice(0, 10); // Top 10 issues
|
|
538
|
+
|
|
539
|
+
return issuePatterns;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Calculate resolution time statistics
|
|
544
|
+
*
|
|
545
|
+
* @param {Array} feedbacks - Array of feedback objects
|
|
546
|
+
* @returns {Object} ResolutionTimeStats object
|
|
547
|
+
* @private
|
|
548
|
+
*/
|
|
549
|
+
_calculateResolutionTimes(feedbacks) {
|
|
550
|
+
// Filter resolved feedbacks
|
|
551
|
+
const resolvedFeedbacks = feedbacks.filter(f =>
|
|
552
|
+
f.resolution && f.resolution.resolvedAt
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
if (resolvedFeedbacks.length === 0) {
|
|
556
|
+
return {
|
|
557
|
+
average: 0,
|
|
558
|
+
median: 0,
|
|
559
|
+
min: 0,
|
|
560
|
+
max: 0,
|
|
561
|
+
bySeverity: {},
|
|
562
|
+
byType: {}
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Calculate resolution times in hours
|
|
567
|
+
const resolutionTimes = resolvedFeedbacks.map(f => {
|
|
568
|
+
const created = new Date(f.createdAt);
|
|
569
|
+
const resolved = new Date(f.resolution.resolvedAt);
|
|
570
|
+
return (resolved - created) / (1000 * 60 * 60); // Convert to hours
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// Calculate statistics
|
|
574
|
+
const sorted = resolutionTimes.sort((a, b) => a - b);
|
|
575
|
+
const average = resolutionTimes.reduce((sum, t) => sum + t, 0) / resolutionTimes.length;
|
|
576
|
+
const median = sorted[Math.floor(sorted.length / 2)];
|
|
577
|
+
const min = sorted[0];
|
|
578
|
+
const max = sorted[sorted.length - 1];
|
|
579
|
+
|
|
580
|
+
// Calculate by severity
|
|
581
|
+
const bySeverity = {};
|
|
582
|
+
Object.values(FeedbackSeverity).forEach(severity => {
|
|
583
|
+
const severityFeedbacks = resolvedFeedbacks.filter(f => f.severity === severity);
|
|
584
|
+
if (severityFeedbacks.length > 0) {
|
|
585
|
+
const times = severityFeedbacks.map(f => {
|
|
586
|
+
const created = new Date(f.createdAt);
|
|
587
|
+
const resolved = new Date(f.resolution.resolvedAt);
|
|
588
|
+
return (resolved - created) / (1000 * 60 * 60);
|
|
589
|
+
});
|
|
590
|
+
bySeverity[severity] = times.reduce((sum, t) => sum + t, 0) / times.length;
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
// Calculate by type
|
|
595
|
+
const byType = {};
|
|
596
|
+
Object.values(FeedbackType).forEach(type => {
|
|
597
|
+
const typeFeedbacks = resolvedFeedbacks.filter(f => f.type === type);
|
|
598
|
+
if (typeFeedbacks.length > 0) {
|
|
599
|
+
const times = typeFeedbacks.map(f => {
|
|
600
|
+
const created = new Date(f.createdAt);
|
|
601
|
+
const resolved = new Date(f.resolution.resolvedAt);
|
|
602
|
+
return (resolved - created) / (1000 * 60 * 60);
|
|
603
|
+
});
|
|
604
|
+
byType[type] = times.reduce((sum, t) => sum + t, 0) / times.length;
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
return {
|
|
609
|
+
average,
|
|
610
|
+
median,
|
|
611
|
+
min,
|
|
612
|
+
max,
|
|
613
|
+
bySeverity,
|
|
614
|
+
byType
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Track satisfaction trends over time
|
|
620
|
+
*
|
|
621
|
+
* @param {Array} feedbacks - Array of feedback objects
|
|
622
|
+
* @param {Date} fromDate - Start date
|
|
623
|
+
* @param {Date} toDate - End date
|
|
624
|
+
* @returns {Array} Array of SatisfactionTrend objects
|
|
625
|
+
* @private
|
|
626
|
+
*/
|
|
627
|
+
_trackSatisfactionTrends(feedbacks, fromDate, toDate) {
|
|
628
|
+
// Group feedbacks by month
|
|
629
|
+
const monthlyData = new Map();
|
|
630
|
+
|
|
631
|
+
feedbacks.forEach(feedback => {
|
|
632
|
+
const date = new Date(feedback.createdAt);
|
|
633
|
+
const period = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
|
634
|
+
|
|
635
|
+
if (!monthlyData.has(period)) {
|
|
636
|
+
monthlyData.set(period, {
|
|
637
|
+
period,
|
|
638
|
+
feedbacks: [],
|
|
639
|
+
severityDistribution: {}
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
monthlyData.get(period).feedbacks.push(feedback);
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
// Calculate trends for each period
|
|
647
|
+
const trends = Array.from(monthlyData.values()).map(data => {
|
|
648
|
+
const totalFeedback = data.feedbacks.length;
|
|
649
|
+
const resolvedCount = data.feedbacks.filter(f =>
|
|
650
|
+
f.status === FeedbackStatus.RESOLVED || f.status === FeedbackStatus.VERIFIED
|
|
651
|
+
).length;
|
|
652
|
+
const resolutionRate = totalFeedback > 0 ? (resolvedCount / totalFeedback) * 100 : 0;
|
|
653
|
+
|
|
654
|
+
// Calculate average resolution time for this period
|
|
655
|
+
const resolvedFeedbacks = data.feedbacks.filter(f =>
|
|
656
|
+
f.resolution && f.resolution.resolvedAt
|
|
657
|
+
);
|
|
658
|
+
let averageResolutionTime = 0;
|
|
659
|
+
if (resolvedFeedbacks.length > 0) {
|
|
660
|
+
const times = resolvedFeedbacks.map(f => {
|
|
661
|
+
const created = new Date(f.createdAt);
|
|
662
|
+
const resolved = new Date(f.resolution.resolvedAt);
|
|
663
|
+
return (resolved - created) / (1000 * 60 * 60);
|
|
664
|
+
});
|
|
665
|
+
averageResolutionTime = times.reduce((sum, t) => sum + t, 0) / times.length;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Calculate severity distribution
|
|
669
|
+
const severityDistribution = {};
|
|
670
|
+
Object.values(FeedbackSeverity).forEach(severity => {
|
|
671
|
+
severityDistribution[severity] = data.feedbacks.filter(f => f.severity === severity).length;
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
return {
|
|
675
|
+
period: data.period,
|
|
676
|
+
totalFeedback,
|
|
677
|
+
resolvedCount,
|
|
678
|
+
resolutionRate,
|
|
679
|
+
averageResolutionTime,
|
|
680
|
+
severityDistribution
|
|
681
|
+
};
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
// Sort by period
|
|
685
|
+
return trends.sort((a, b) => a.period.localeCompare(b.period));
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Identify version-specific issues
|
|
690
|
+
*
|
|
691
|
+
* @param {Array} feedbacks - Array of feedback objects
|
|
692
|
+
* @returns {Array} Array of VersionIssue objects
|
|
693
|
+
* @private
|
|
694
|
+
*/
|
|
695
|
+
_identifyVersionIssues(feedbacks) {
|
|
696
|
+
// Group feedbacks by version
|
|
697
|
+
const versionData = new Map();
|
|
698
|
+
|
|
699
|
+
feedbacks.forEach(feedback => {
|
|
700
|
+
const version = feedback.version || 'unknown';
|
|
701
|
+
|
|
702
|
+
if (!versionData.has(version)) {
|
|
703
|
+
versionData.set(version, {
|
|
704
|
+
version,
|
|
705
|
+
feedbacks: []
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
versionData.get(version).feedbacks.push(feedback);
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
// Analyze each version
|
|
713
|
+
const versionIssues = Array.from(versionData.values()).map(data => {
|
|
714
|
+
const feedbackCount = data.feedbacks.length;
|
|
715
|
+
const criticalCount = data.feedbacks.filter(f => f.severity === FeedbackSeverity.CRITICAL).length;
|
|
716
|
+
|
|
717
|
+
// Get top issues for this version
|
|
718
|
+
const topIssues = this._analyzeCommonIssues(data.feedbacks).slice(0, 5);
|
|
719
|
+
|
|
720
|
+
// Calculate average resolution time
|
|
721
|
+
const resolvedFeedbacks = data.feedbacks.filter(f =>
|
|
722
|
+
f.resolution && f.resolution.resolvedAt
|
|
723
|
+
);
|
|
724
|
+
let averageResolutionTime = 0;
|
|
725
|
+
if (resolvedFeedbacks.length > 0) {
|
|
726
|
+
const times = resolvedFeedbacks.map(f => {
|
|
727
|
+
const created = new Date(f.createdAt);
|
|
728
|
+
const resolved = new Date(f.resolution.resolvedAt);
|
|
729
|
+
return (resolved - created) / (1000 * 60 * 60);
|
|
730
|
+
});
|
|
731
|
+
averageResolutionTime = times.reduce((sum, t) => sum + t, 0) / times.length;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
return {
|
|
735
|
+
version: data.version,
|
|
736
|
+
feedbackCount,
|
|
737
|
+
topIssues,
|
|
738
|
+
criticalCount,
|
|
739
|
+
averageResolutionTime
|
|
740
|
+
};
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
// Sort by feedback count (descending)
|
|
744
|
+
return versionIssues.sort((a, b) => b.feedbackCount - a.feedbackCount);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Generate automated response for feedback
|
|
749
|
+
*
|
|
750
|
+
* @param {string} feedbackId - Feedback ID
|
|
751
|
+
* @param {string} project - Project name
|
|
752
|
+
* @param {string} environment - Security environment
|
|
753
|
+
* @param {Object} permissionManager - PermissionManager instance (optional)
|
|
754
|
+
* @returns {Promise<Object>} Response result
|
|
755
|
+
*/
|
|
756
|
+
async generateAutomatedResponse(feedbackId, project, environment, permissionManager = null) {
|
|
757
|
+
// Load feedback
|
|
758
|
+
const feedback = await this.getFeedback(feedbackId);
|
|
759
|
+
if (!feedback) {
|
|
760
|
+
throw new Error(`Feedback not found: ${feedbackId}`);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Check if automated response is authorized
|
|
764
|
+
if (permissionManager) {
|
|
765
|
+
const takeoverLevel = await permissionManager.getTakeoverLevel(project, environment);
|
|
766
|
+
const authorized = this._isAutomatedResponseAuthorized(feedback, takeoverLevel);
|
|
767
|
+
|
|
768
|
+
if (!authorized) {
|
|
769
|
+
return {
|
|
770
|
+
success: false,
|
|
771
|
+
reason: 'Automated response not authorized for current takeover level',
|
|
772
|
+
takeoverLevel,
|
|
773
|
+
requiresHumanReview: true
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Generate response based on feedback pattern
|
|
779
|
+
const response = this._generateResponseContent(feedback);
|
|
780
|
+
|
|
781
|
+
// Save automated response
|
|
782
|
+
await this.saveAutomatedResponse({
|
|
783
|
+
feedbackId,
|
|
784
|
+
response,
|
|
785
|
+
generatedAt: new Date().toISOString(),
|
|
786
|
+
authorized: true,
|
|
787
|
+
takeoverLevel: permissionManager ? await permissionManager.getTakeoverLevel(project, environment) : 'unknown'
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
return {
|
|
791
|
+
success: true,
|
|
792
|
+
response,
|
|
793
|
+
requiresHumanReview: false
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Check if automated response is authorized
|
|
799
|
+
*
|
|
800
|
+
* @param {Object} feedback - Feedback object
|
|
801
|
+
* @param {string} takeoverLevel - Current takeover level
|
|
802
|
+
* @returns {boolean} True if authorized
|
|
803
|
+
* @private
|
|
804
|
+
*/
|
|
805
|
+
_isAutomatedResponseAuthorized(feedback, takeoverLevel) {
|
|
806
|
+
const { TakeoverLevel } = require('./models');
|
|
807
|
+
|
|
808
|
+
// Critical feedback always requires human review
|
|
809
|
+
if (feedback.severity === FeedbackSeverity.CRITICAL) {
|
|
810
|
+
return false;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// L1 and L2: No automation
|
|
814
|
+
if (takeoverLevel === TakeoverLevel.L1_OBSERVATION ||
|
|
815
|
+
takeoverLevel === TakeoverLevel.L2_SUGGESTION) {
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// L3+: Can automate non-critical feedback
|
|
820
|
+
return true;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Generate response content based on feedback pattern
|
|
825
|
+
*
|
|
826
|
+
* @param {Object} feedback - Feedback object
|
|
827
|
+
* @returns {Object} Response content
|
|
828
|
+
* @private
|
|
829
|
+
*/
|
|
830
|
+
_generateResponseContent(feedback) {
|
|
831
|
+
// Simple pattern-based response generation
|
|
832
|
+
const responses = {
|
|
833
|
+
[FeedbackType.BUG_REPORT]: {
|
|
834
|
+
message: 'Thank you for reporting this issue. We have logged it and will investigate.',
|
|
835
|
+
actions: ['Issue logged', 'Assigned to engineering team']
|
|
836
|
+
},
|
|
837
|
+
[FeedbackType.PERFORMANCE_ISSUE]: {
|
|
838
|
+
message: 'We have received your performance report and are analyzing the metrics.',
|
|
839
|
+
actions: ['Performance metrics collected', 'Analysis in progress']
|
|
840
|
+
},
|
|
841
|
+
[FeedbackType.FEATURE_REQUEST]: {
|
|
842
|
+
message: 'Thank you for your feature suggestion. We have added it to our backlog.',
|
|
843
|
+
actions: ['Feature request logged', 'Added to product backlog']
|
|
844
|
+
},
|
|
845
|
+
[FeedbackType.OPERATIONAL_CONCERN]: {
|
|
846
|
+
message: 'Your operational concern has been noted and forwarded to the operations team.',
|
|
847
|
+
actions: ['Concern logged', 'Operations team notified']
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
const template = responses[feedback.type] || responses[FeedbackType.OPERATIONAL_CONCERN];
|
|
852
|
+
|
|
853
|
+
return {
|
|
854
|
+
message: template.message,
|
|
855
|
+
actions: template.actions,
|
|
856
|
+
feedbackId: feedback.id,
|
|
857
|
+
type: 'automated'
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Generate change proposal from feedback
|
|
863
|
+
*
|
|
864
|
+
* @param {string} feedbackId - Feedback ID
|
|
865
|
+
* @returns {Promise<Object>} Change proposal
|
|
866
|
+
*/
|
|
867
|
+
async generateChangeProposal(feedbackId) {
|
|
868
|
+
const feedback = await this.getFeedback(feedbackId);
|
|
869
|
+
if (!feedback) {
|
|
870
|
+
throw new Error(`Feedback not found: ${feedbackId}`);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// Determine if feedback requires operational changes
|
|
874
|
+
const requiresChange = this._requiresOperationalChange(feedback);
|
|
875
|
+
|
|
876
|
+
if (!requiresChange) {
|
|
877
|
+
return {
|
|
878
|
+
required: false,
|
|
879
|
+
reason: 'Feedback does not indicate need for operational changes'
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// Generate change proposal
|
|
884
|
+
const proposal = {
|
|
885
|
+
feedbackId: feedback.id,
|
|
886
|
+
proposalType: this._determineChangeType(feedback),
|
|
887
|
+
description: `Operational change proposed based on feedback: ${feedback.content.title}`,
|
|
888
|
+
impactAssessment: this._assessChangeImpact(feedback),
|
|
889
|
+
recommendedActions: this._recommendActions(feedback),
|
|
890
|
+
priority: feedback.severity,
|
|
891
|
+
createdAt: new Date().toISOString()
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
// Save proposal
|
|
895
|
+
await this.saveChangeProposal(proposal);
|
|
896
|
+
|
|
897
|
+
return {
|
|
898
|
+
required: true,
|
|
899
|
+
proposal
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Check if feedback requires operational change
|
|
905
|
+
*
|
|
906
|
+
* @param {Object} feedback - Feedback object
|
|
907
|
+
* @returns {boolean} True if change required
|
|
908
|
+
* @private
|
|
909
|
+
*/
|
|
910
|
+
_requiresOperationalChange(feedback) {
|
|
911
|
+
// Recurring issues or high severity issues typically require changes
|
|
912
|
+
return feedback.severity === FeedbackSeverity.CRITICAL ||
|
|
913
|
+
feedback.severity === FeedbackSeverity.HIGH;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Determine type of operational change needed
|
|
918
|
+
*
|
|
919
|
+
* @param {Object} feedback - Feedback object
|
|
920
|
+
* @returns {string} Change type
|
|
921
|
+
* @private
|
|
922
|
+
*/
|
|
923
|
+
_determineChangeType(feedback) {
|
|
924
|
+
if (feedback.type === FeedbackType.PERFORMANCE_ISSUE) {
|
|
925
|
+
return 'performance_optimization';
|
|
926
|
+
} else if (feedback.type === FeedbackType.BUG_REPORT) {
|
|
927
|
+
return 'bug_fix';
|
|
928
|
+
} else {
|
|
929
|
+
return 'operational_improvement';
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* Assess impact of proposed change
|
|
935
|
+
*
|
|
936
|
+
* @param {Object} feedback - Feedback object
|
|
937
|
+
* @returns {Object} Impact assessment
|
|
938
|
+
* @private
|
|
939
|
+
*/
|
|
940
|
+
_assessChangeImpact(feedback) {
|
|
941
|
+
return {
|
|
942
|
+
severity: feedback.severity,
|
|
943
|
+
affectedComponents: ['operations'],
|
|
944
|
+
estimatedEffort: feedback.severity === FeedbackSeverity.CRITICAL ? 'high' : 'medium',
|
|
945
|
+
riskLevel: feedback.severity === FeedbackSeverity.CRITICAL ? 'high' : 'low'
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* Recommend actions for feedback
|
|
951
|
+
*
|
|
952
|
+
* @param {Object} feedback - Feedback object
|
|
953
|
+
* @returns {Array} Recommended actions
|
|
954
|
+
* @private
|
|
955
|
+
*/
|
|
956
|
+
_recommendActions(feedback) {
|
|
957
|
+
const actions = [];
|
|
958
|
+
|
|
959
|
+
if (feedback.type === FeedbackType.PERFORMANCE_ISSUE) {
|
|
960
|
+
actions.push('Review performance metrics');
|
|
961
|
+
actions.push('Optimize slow operations');
|
|
962
|
+
actions.push('Update monitoring thresholds');
|
|
963
|
+
} else if (feedback.type === FeedbackType.BUG_REPORT) {
|
|
964
|
+
actions.push('Investigate root cause');
|
|
965
|
+
actions.push('Implement fix');
|
|
966
|
+
actions.push('Add regression test');
|
|
967
|
+
} else {
|
|
968
|
+
actions.push('Review operational procedures');
|
|
969
|
+
actions.push('Update documentation');
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
return actions;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* Notify stakeholders about feedback
|
|
977
|
+
*
|
|
978
|
+
* @param {string} feedbackId - Feedback ID
|
|
979
|
+
* @param {Array} stakeholders - List of stakeholder emails
|
|
980
|
+
* @returns {Promise<Object>} Notification result
|
|
981
|
+
*/
|
|
982
|
+
async notifyStakeholders(feedbackId, stakeholders = []) {
|
|
983
|
+
const feedback = await this.getFeedback(feedbackId);
|
|
984
|
+
if (!feedback) {
|
|
985
|
+
throw new Error(`Feedback not found: ${feedbackId}`);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// Determine if notification is required
|
|
989
|
+
const requiresNotification = this._requiresStakeholderNotification(feedback);
|
|
990
|
+
|
|
991
|
+
if (!requiresNotification) {
|
|
992
|
+
return {
|
|
993
|
+
sent: false,
|
|
994
|
+
reason: 'Feedback does not require stakeholder notification'
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// Generate notification
|
|
999
|
+
const notification = {
|
|
1000
|
+
feedbackId: feedback.id,
|
|
1001
|
+
subject: `[${feedback.severity.toUpperCase()}] Feedback requires attention: ${feedback.content.title}`,
|
|
1002
|
+
body: this._generateNotificationBody(feedback),
|
|
1003
|
+
recipients: stakeholders.length > 0 ? stakeholders : this._getDefaultStakeholders(feedback),
|
|
1004
|
+
sentAt: new Date().toISOString()
|
|
1005
|
+
};
|
|
1006
|
+
|
|
1007
|
+
// Save notification record
|
|
1008
|
+
await this.saveNotification(notification);
|
|
1009
|
+
|
|
1010
|
+
return {
|
|
1011
|
+
sent: true,
|
|
1012
|
+
notification
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/**
|
|
1017
|
+
* Check if feedback requires stakeholder notification
|
|
1018
|
+
*
|
|
1019
|
+
* @param {Object} feedback - Feedback object
|
|
1020
|
+
* @returns {boolean} True if notification required
|
|
1021
|
+
* @private
|
|
1022
|
+
*/
|
|
1023
|
+
_requiresStakeholderNotification(feedback) {
|
|
1024
|
+
// Critical and high severity feedback requires notification
|
|
1025
|
+
return feedback.severity === FeedbackSeverity.CRITICAL ||
|
|
1026
|
+
feedback.severity === FeedbackSeverity.HIGH;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* Generate notification body
|
|
1031
|
+
*
|
|
1032
|
+
* @param {Object} feedback - Feedback object
|
|
1033
|
+
* @returns {string} Notification body
|
|
1034
|
+
* @private
|
|
1035
|
+
*/
|
|
1036
|
+
_generateNotificationBody(feedback) {
|
|
1037
|
+
return `
|
|
1038
|
+
Feedback ID: ${feedback.id}
|
|
1039
|
+
Severity: ${feedback.severity}
|
|
1040
|
+
Type: ${feedback.type}
|
|
1041
|
+
Project: ${feedback.project}
|
|
1042
|
+
Version: ${feedback.version}
|
|
1043
|
+
|
|
1044
|
+
Title: ${feedback.content.title}
|
|
1045
|
+
Description: ${feedback.content.description}
|
|
1046
|
+
|
|
1047
|
+
Status: ${feedback.status}
|
|
1048
|
+
Created: ${feedback.createdAt}
|
|
1049
|
+
|
|
1050
|
+
This feedback requires human attention. Please review and take appropriate action.
|
|
1051
|
+
`.trim();
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
/**
|
|
1055
|
+
* Get default stakeholders for feedback
|
|
1056
|
+
*
|
|
1057
|
+
* @param {Object} feedback - Feedback object
|
|
1058
|
+
* @returns {Array} List of stakeholder emails
|
|
1059
|
+
* @private
|
|
1060
|
+
*/
|
|
1061
|
+
_getDefaultStakeholders(feedback) {
|
|
1062
|
+
// In a real system, this would query a stakeholder registry
|
|
1063
|
+
// For now, return placeholder
|
|
1064
|
+
return ['operations-team@example.com'];
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* Save automated response
|
|
1069
|
+
*
|
|
1070
|
+
* @param {Object} response - Automated response object
|
|
1071
|
+
* @returns {Promise<void>}
|
|
1072
|
+
*/
|
|
1073
|
+
async saveAutomatedResponse(response) {
|
|
1074
|
+
const responseFile = path.join(this.feedbackDir, 'automated-responses.json');
|
|
1075
|
+
await ensureDirectory(this.feedbackDir);
|
|
1076
|
+
|
|
1077
|
+
let responses = [];
|
|
1078
|
+
const exists = await pathExists(responseFile);
|
|
1079
|
+
if (exists) {
|
|
1080
|
+
try {
|
|
1081
|
+
const data = await readJSON(responseFile);
|
|
1082
|
+
responses = data.responses || [];
|
|
1083
|
+
} catch (error) {
|
|
1084
|
+
// Ignore errors, start fresh
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
responses.push(response);
|
|
1089
|
+
|
|
1090
|
+
await writeJSON(responseFile, { responses, lastUpdated: new Date().toISOString() });
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
/**
|
|
1094
|
+
* Save change proposal
|
|
1095
|
+
*
|
|
1096
|
+
* @param {Object} proposal - Change proposal object
|
|
1097
|
+
* @returns {Promise<void>}
|
|
1098
|
+
*/
|
|
1099
|
+
async saveChangeProposal(proposal) {
|
|
1100
|
+
const proposalFile = path.join(this.feedbackDir, 'change-proposals.json');
|
|
1101
|
+
await ensureDirectory(this.feedbackDir);
|
|
1102
|
+
|
|
1103
|
+
let proposals = [];
|
|
1104
|
+
const exists = await pathExists(proposalFile);
|
|
1105
|
+
if (exists) {
|
|
1106
|
+
try {
|
|
1107
|
+
const data = await readJSON(proposalFile);
|
|
1108
|
+
proposals = data.proposals || [];
|
|
1109
|
+
} catch (error) {
|
|
1110
|
+
// Ignore errors, start fresh
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
proposals.push(proposal);
|
|
1115
|
+
|
|
1116
|
+
await writeJSON(proposalFile, { proposals, lastUpdated: new Date().toISOString() });
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* Save notification record
|
|
1121
|
+
*
|
|
1122
|
+
* @param {Object} notification - Notification object
|
|
1123
|
+
* @returns {Promise<void>}
|
|
1124
|
+
*/
|
|
1125
|
+
async saveNotification(notification) {
|
|
1126
|
+
const notificationFile = path.join(this.feedbackDir, 'notifications.json');
|
|
1127
|
+
await ensureDirectory(this.feedbackDir);
|
|
1128
|
+
|
|
1129
|
+
let notifications = [];
|
|
1130
|
+
const exists = await pathExists(notificationFile);
|
|
1131
|
+
if (exists) {
|
|
1132
|
+
try {
|
|
1133
|
+
const data = await readJSON(notificationFile);
|
|
1134
|
+
notifications = data.notifications || [];
|
|
1135
|
+
} catch (error) {
|
|
1136
|
+
// Ignore errors, start fresh
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
notifications.push(notification);
|
|
1141
|
+
|
|
1142
|
+
await writeJSON(notificationFile, { notifications, lastUpdated: new Date().toISOString() });
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
module.exports = FeedbackManager;
|
|
1147
|
+
|