reflectt-node 0.1.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/LICENSE +178 -0
- package/README.md +188 -0
- package/dist/activationEvents.d.ts +110 -0
- package/dist/activationEvents.d.ts.map +1 -0
- package/dist/activationEvents.js +378 -0
- package/dist/activationEvents.js.map +1 -0
- package/dist/activity-signal.d.ts +30 -0
- package/dist/activity-signal.d.ts.map +1 -0
- package/dist/activity-signal.js +93 -0
- package/dist/activity-signal.js.map +1 -0
- package/dist/alert-integrity.d.ts +100 -0
- package/dist/alert-integrity.d.ts.map +1 -0
- package/dist/alert-integrity.js +333 -0
- package/dist/alert-integrity.js.map +1 -0
- package/dist/alert-preflight.d.ts +40 -0
- package/dist/alert-preflight.d.ts.map +1 -0
- package/dist/alert-preflight.js +235 -0
- package/dist/alert-preflight.js.map +1 -0
- package/dist/analytics.d.ts +131 -0
- package/dist/analytics.d.ts.map +1 -0
- package/dist/analytics.js +371 -0
- package/dist/analytics.js.map +1 -0
- package/dist/artifact-mirror.d.ts +26 -0
- package/dist/artifact-mirror.d.ts.map +1 -0
- package/dist/artifact-mirror.js +170 -0
- package/dist/artifact-mirror.js.map +1 -0
- package/dist/artifact-resolver.d.ts +48 -0
- package/dist/artifact-resolver.d.ts.map +1 -0
- package/dist/artifact-resolver.js +164 -0
- package/dist/artifact-resolver.js.map +1 -0
- package/dist/assignment.d.ts +116 -0
- package/dist/assignment.d.ts.map +1 -0
- package/dist/assignment.js +475 -0
- package/dist/assignment.js.map +1 -0
- package/dist/auditLedger.d.ts +50 -0
- package/dist/auditLedger.d.ts.map +1 -0
- package/dist/auditLedger.js +136 -0
- package/dist/auditLedger.js.map +1 -0
- package/dist/boardHealthWorker.d.ts +134 -0
- package/dist/boardHealthWorker.d.ts.map +1 -0
- package/dist/boardHealthWorker.js +882 -0
- package/dist/boardHealthWorker.js.map +1 -0
- package/dist/bootstrap-team.d.ts +42 -0
- package/dist/bootstrap-team.d.ts.map +1 -0
- package/dist/bootstrap-team.js +111 -0
- package/dist/bootstrap-team.js.map +1 -0
- package/dist/buildInfo.d.ts +17 -0
- package/dist/buildInfo.d.ts.map +1 -0
- package/dist/buildInfo.js +56 -0
- package/dist/buildInfo.js.map +1 -0
- package/dist/calendar-events.d.ts +133 -0
- package/dist/calendar-events.d.ts.map +1 -0
- package/dist/calendar-events.js +615 -0
- package/dist/calendar-events.js.map +1 -0
- package/dist/calendar-ical.d.ts +41 -0
- package/dist/calendar-ical.d.ts.map +1 -0
- package/dist/calendar-ical.js +413 -0
- package/dist/calendar-ical.js.map +1 -0
- package/dist/calendar-reminder-engine.d.ts +10 -0
- package/dist/calendar-reminder-engine.d.ts.map +1 -0
- package/dist/calendar-reminder-engine.js +143 -0
- package/dist/calendar-reminder-engine.js.map +1 -0
- package/dist/calendar.d.ts +75 -0
- package/dist/calendar.d.ts.map +1 -0
- package/dist/calendar.js +391 -0
- package/dist/calendar.js.map +1 -0
- package/dist/canvas-multiplexer.d.ts +44 -0
- package/dist/canvas-multiplexer.d.ts.map +1 -0
- package/dist/canvas-multiplexer.js +150 -0
- package/dist/canvas-multiplexer.js.map +1 -0
- package/dist/canvas-slots.d.ts +83 -0
- package/dist/canvas-slots.d.ts.map +1 -0
- package/dist/canvas-slots.js +144 -0
- package/dist/canvas-slots.js.map +1 -0
- package/dist/canvas-types.d.ts +56 -0
- package/dist/canvas-types.d.ts.map +1 -0
- package/dist/canvas-types.js +54 -0
- package/dist/canvas-types.js.map +1 -0
- package/dist/cf-keepalive.d.ts +40 -0
- package/dist/cf-keepalive.d.ts.map +1 -0
- package/dist/cf-keepalive.js +153 -0
- package/dist/cf-keepalive.js.map +1 -0
- package/dist/changeFeed.d.ts +38 -0
- package/dist/changeFeed.d.ts.map +1 -0
- package/dist/changeFeed.js +324 -0
- package/dist/changeFeed.js.map +1 -0
- package/dist/channels.d.ts +28 -0
- package/dist/channels.d.ts.map +1 -0
- package/dist/channels.js +23 -0
- package/dist/channels.js.map +1 -0
- package/dist/chat-approval-detector.d.ts +47 -0
- package/dist/chat-approval-detector.d.ts.map +1 -0
- package/dist/chat-approval-detector.js +224 -0
- package/dist/chat-approval-detector.js.map +1 -0
- package/dist/chat.d.ts +119 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +666 -0
- package/dist/chat.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1142 -0
- package/dist/cli.js.map +1 -0
- package/dist/cloud.d.ts +45 -0
- package/dist/cloud.d.ts.map +1 -0
- package/dist/cloud.js +962 -0
- package/dist/cloud.js.map +1 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +33 -0
- package/dist/config.js.map +1 -0
- package/dist/connectivity.d.ts +59 -0
- package/dist/connectivity.d.ts.map +1 -0
- package/dist/connectivity.js +173 -0
- package/dist/connectivity.js.map +1 -0
- package/dist/contacts.d.ts +59 -0
- package/dist/contacts.d.ts.map +1 -0
- package/dist/contacts.js +183 -0
- package/dist/contacts.js.map +1 -0
- package/dist/content.d.ts +130 -0
- package/dist/content.d.ts.map +1 -0
- package/dist/content.js +186 -0
- package/dist/content.js.map +1 -0
- package/dist/context-budget.d.ts +87 -0
- package/dist/context-budget.d.ts.map +1 -0
- package/dist/context-budget.js +459 -0
- package/dist/context-budget.js.map +1 -0
- package/dist/continuity-loop.d.ts +55 -0
- package/dist/continuity-loop.d.ts.map +1 -0
- package/dist/continuity-loop.js +267 -0
- package/dist/continuity-loop.js.map +1 -0
- package/dist/dashboard.d.ts +6 -0
- package/dist/dashboard.d.ts.map +1 -0
- package/dist/dashboard.js +2348 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/db.d.ts +44 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +648 -0
- package/dist/db.js.map +1 -0
- package/dist/doctor.d.ts +30 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +159 -0
- package/dist/doctor.js.map +1 -0
- package/dist/duplicateClosureGuard.d.ts +31 -0
- package/dist/duplicateClosureGuard.d.ts.map +1 -0
- package/dist/duplicateClosureGuard.js +83 -0
- package/dist/duplicateClosureGuard.js.map +1 -0
- package/dist/embeddings.d.ts +13 -0
- package/dist/embeddings.d.ts.map +1 -0
- package/dist/embeddings.js +78 -0
- package/dist/embeddings.js.map +1 -0
- package/dist/escalation.d.ts +80 -0
- package/dist/escalation.d.ts.map +1 -0
- package/dist/escalation.js +213 -0
- package/dist/escalation.js.map +1 -0
- package/dist/events.d.ts +130 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +382 -0
- package/dist/events.js.map +1 -0
- package/dist/executionSweeper.d.ts +97 -0
- package/dist/executionSweeper.d.ts.map +1 -0
- package/dist/executionSweeper.js +875 -0
- package/dist/executionSweeper.js.map +1 -0
- package/dist/experiments.d.ts +47 -0
- package/dist/experiments.d.ts.map +1 -0
- package/dist/experiments.js +133 -0
- package/dist/experiments.js.map +1 -0
- package/dist/feedback.d.ts +179 -0
- package/dist/feedback.d.ts.map +1 -0
- package/dist/feedback.js +397 -0
- package/dist/feedback.js.map +1 -0
- package/dist/files.d.ts +52 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +172 -0
- package/dist/files.js.map +1 -0
- package/dist/format-duration.d.ts +19 -0
- package/dist/format-duration.d.ts.map +1 -0
- package/dist/format-duration.js +33 -0
- package/dist/format-duration.js.map +1 -0
- package/dist/github-actor-auth.d.ts +20 -0
- package/dist/github-actor-auth.d.ts.map +1 -0
- package/dist/github-actor-auth.js +54 -0
- package/dist/github-actor-auth.js.map +1 -0
- package/dist/github-ci.d.ts +16 -0
- package/dist/github-ci.d.ts.map +1 -0
- package/dist/github-ci.js +37 -0
- package/dist/github-ci.js.map +1 -0
- package/dist/github-identity.d.ts +30 -0
- package/dist/github-identity.d.ts.map +1 -0
- package/dist/github-identity.js +96 -0
- package/dist/github-identity.js.map +1 -0
- package/dist/github-reviews.d.ts +24 -0
- package/dist/github-reviews.d.ts.map +1 -0
- package/dist/github-reviews.js +56 -0
- package/dist/github-reviews.js.map +1 -0
- package/dist/health.d.ts +391 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +1841 -0
- package/dist/health.js.map +1 -0
- package/dist/host-keepalive.d.ts +22 -0
- package/dist/host-keepalive.d.ts.map +1 -0
- package/dist/host-keepalive.js +126 -0
- package/dist/host-keepalive.js.map +1 -0
- package/dist/host-registry.d.ts +43 -0
- package/dist/host-registry.d.ts.map +1 -0
- package/dist/host-registry.js +93 -0
- package/dist/host-registry.js.map +1 -0
- package/dist/inbox.d.ts +87 -0
- package/dist/inbox.d.ts.map +1 -0
- package/dist/inbox.js +410 -0
- package/dist/inbox.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +306 -0
- package/dist/index.js.map +1 -0
- package/dist/insight-mutation.d.ts +32 -0
- package/dist/insight-mutation.d.ts.map +1 -0
- package/dist/insight-mutation.js +160 -0
- package/dist/insight-mutation.js.map +1 -0
- package/dist/insight-promotion.d.ts +89 -0
- package/dist/insight-promotion.d.ts.map +1 -0
- package/dist/insight-promotion.js +278 -0
- package/dist/insight-promotion.js.map +1 -0
- package/dist/insight-task-bridge.d.ts +77 -0
- package/dist/insight-task-bridge.d.ts.map +1 -0
- package/dist/insight-task-bridge.js +556 -0
- package/dist/insight-task-bridge.js.map +1 -0
- package/dist/insights.d.ts +222 -0
- package/dist/insights.d.ts.map +1 -0
- package/dist/insights.js +871 -0
- package/dist/insights.js.map +1 -0
- package/dist/intake-pipeline.d.ts +74 -0
- package/dist/intake-pipeline.d.ts.map +1 -0
- package/dist/intake-pipeline.js +199 -0
- package/dist/intake-pipeline.js.map +1 -0
- package/dist/intensity.d.ts +31 -0
- package/dist/intensity.d.ts.map +1 -0
- package/dist/intensity.js +94 -0
- package/dist/intensity.js.map +1 -0
- package/dist/knowledge-auto-index.d.ts +37 -0
- package/dist/knowledge-auto-index.d.ts.map +1 -0
- package/dist/knowledge-auto-index.js +149 -0
- package/dist/knowledge-auto-index.js.map +1 -0
- package/dist/knowledge-docs.d.ts +45 -0
- package/dist/knowledge-docs.d.ts.map +1 -0
- package/dist/knowledge-docs.js +188 -0
- package/dist/knowledge-docs.js.map +1 -0
- package/dist/lane-config.d.ts +25 -0
- package/dist/lane-config.d.ts.map +1 -0
- package/dist/lane-config.js +105 -0
- package/dist/lane-config.js.map +1 -0
- package/dist/lineage.d.ts +86 -0
- package/dist/lineage.d.ts.map +1 -0
- package/dist/lineage.js +303 -0
- package/dist/lineage.js.map +1 -0
- package/dist/logStore.d.ts +25 -0
- package/dist/logStore.d.ts.map +1 -0
- package/dist/logStore.js +83 -0
- package/dist/logStore.js.map +1 -0
- package/dist/manage.d.ts +12 -0
- package/dist/manage.d.ts.map +1 -0
- package/dist/manage.js +253 -0
- package/dist/manage.js.map +1 -0
- package/dist/mcp.d.ts +5 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +604 -0
- package/dist/mcp.js.map +1 -0
- package/dist/memory.d.ts +47 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +149 -0
- package/dist/memory.js.map +1 -0
- package/dist/mention-ack.d.ts +80 -0
- package/dist/mention-ack.d.ts.map +1 -0
- package/dist/mention-ack.js +175 -0
- package/dist/mention-ack.js.map +1 -0
- package/dist/messageRouter.d.ts +60 -0
- package/dist/messageRouter.d.ts.map +1 -0
- package/dist/messageRouter.js +309 -0
- package/dist/messageRouter.js.map +1 -0
- package/dist/mutationAlert.d.ts +44 -0
- package/dist/mutationAlert.d.ts.map +1 -0
- package/dist/mutationAlert.js +174 -0
- package/dist/mutationAlert.js.map +1 -0
- package/dist/noise-budget.d.ts +136 -0
- package/dist/noise-budget.d.ts.map +1 -0
- package/dist/noise-budget.js +340 -0
- package/dist/noise-budget.js.map +1 -0
- package/dist/notifications.d.ts +67 -0
- package/dist/notifications.d.ts.map +1 -0
- package/dist/notifications.js +253 -0
- package/dist/notifications.js.map +1 -0
- package/dist/openclaw.d.ts +34 -0
- package/dist/openclaw.d.ts.map +1 -0
- package/dist/openclaw.js +208 -0
- package/dist/openclaw.js.map +1 -0
- package/dist/pause-controls.d.ts +31 -0
- package/dist/pause-controls.d.ts.map +1 -0
- package/dist/pause-controls.js +130 -0
- package/dist/pause-controls.js.map +1 -0
- package/dist/pidlock.d.ts +25 -0
- package/dist/pidlock.d.ts.map +1 -0
- package/dist/pidlock.js +179 -0
- package/dist/pidlock.js.map +1 -0
- package/dist/policy.d.ts +139 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +264 -0
- package/dist/policy.js.map +1 -0
- package/dist/polls.d.ts +47 -0
- package/dist/polls.d.ts.map +1 -0
- package/dist/polls.js +162 -0
- package/dist/polls.js.map +1 -0
- package/dist/portability.d.ts +55 -0
- package/dist/portability.d.ts.map +1 -0
- package/dist/portability.js +292 -0
- package/dist/portability.js.map +1 -0
- package/dist/pr-integrity.d.ts +45 -0
- package/dist/pr-integrity.d.ts.map +1 -0
- package/dist/pr-integrity.js +124 -0
- package/dist/pr-integrity.js.map +1 -0
- package/dist/prAutoMerge.d.ts +62 -0
- package/dist/prAutoMerge.d.ts.map +1 -0
- package/dist/prAutoMerge.js +493 -0
- package/dist/prAutoMerge.js.map +1 -0
- package/dist/preflight.d.ts +66 -0
- package/dist/preflight.d.ts.map +1 -0
- package/dist/preflight.js +864 -0
- package/dist/preflight.js.map +1 -0
- package/dist/presence.d.ts +98 -0
- package/dist/presence.d.ts.map +1 -0
- package/dist/presence.js +347 -0
- package/dist/presence.js.map +1 -0
- package/dist/provisioning.d.ts +101 -0
- package/dist/provisioning.d.ts.map +1 -0
- package/dist/provisioning.js +430 -0
- package/dist/provisioning.js.map +1 -0
- package/dist/reflection-automation.d.ts +59 -0
- package/dist/reflection-automation.d.ts.map +1 -0
- package/dist/reflection-automation.js +350 -0
- package/dist/reflection-automation.js.map +1 -0
- package/dist/reflections.d.ts +65 -0
- package/dist/reflections.d.ts.map +1 -0
- package/dist/reflections.js +306 -0
- package/dist/reflections.js.map +1 -0
- package/dist/release.d.ts +67 -0
- package/dist/release.d.ts.map +1 -0
- package/dist/release.js +275 -0
- package/dist/release.js.map +1 -0
- package/dist/request-tracker.d.ts +36 -0
- package/dist/request-tracker.d.ts.map +1 -0
- package/dist/request-tracker.js +109 -0
- package/dist/request-tracker.js.map +1 -0
- package/dist/research.d.ts +75 -0
- package/dist/research.d.ts.map +1 -0
- package/dist/research.js +171 -0
- package/dist/research.js.map +1 -0
- package/dist/routing-approvals.d.ts +73 -0
- package/dist/routing-approvals.d.ts.map +1 -0
- package/dist/routing-approvals.js +88 -0
- package/dist/routing-approvals.js.map +1 -0
- package/dist/routing-override.d.ts +94 -0
- package/dist/routing-override.d.ts.map +1 -0
- package/dist/routing-override.js +290 -0
- package/dist/routing-override.js.map +1 -0
- package/dist/scope-routing.d.ts +18 -0
- package/dist/scope-routing.d.ts.map +1 -0
- package/dist/scope-routing.js +29 -0
- package/dist/scope-routing.js.map +1 -0
- package/dist/secrets.d.ts +77 -0
- package/dist/secrets.d.ts.map +1 -0
- package/dist/secrets.js +287 -0
- package/dist/secrets.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +10887 -0
- package/dist/server.js.map +1 -0
- package/dist/service-probe.d.ts +53 -0
- package/dist/service-probe.d.ts.map +1 -0
- package/dist/service-probe.js +225 -0
- package/dist/service-probe.js.map +1 -0
- package/dist/shared-workspace-api.d.ts +73 -0
- package/dist/shared-workspace-api.d.ts.map +1 -0
- package/dist/shared-workspace-api.js +281 -0
- package/dist/shared-workspace-api.js.map +1 -0
- package/dist/shipped-heartbeat.d.ts +91 -0
- package/dist/shipped-heartbeat.d.ts.map +1 -0
- package/dist/shipped-heartbeat.js +272 -0
- package/dist/shipped-heartbeat.js.map +1 -0
- package/dist/starter-team.d.ts +23 -0
- package/dist/starter-team.d.ts.map +1 -0
- package/dist/starter-team.js +88 -0
- package/dist/starter-team.js.map +1 -0
- package/dist/suppression-ledger.d.ts +73 -0
- package/dist/suppression-ledger.d.ts.map +1 -0
- package/dist/suppression-ledger.js +125 -0
- package/dist/suppression-ledger.js.map +1 -0
- package/dist/system-loop-state.d.ts +4 -0
- package/dist/system-loop-state.d.ts.map +1 -0
- package/dist/system-loop-state.js +40 -0
- package/dist/system-loop-state.js.map +1 -0
- package/dist/taskCommentIngest.d.ts +43 -0
- package/dist/taskCommentIngest.d.ts.map +1 -0
- package/dist/taskCommentIngest.js +59 -0
- package/dist/taskCommentIngest.js.map +1 -0
- package/dist/taskPrecheck.d.ts +20 -0
- package/dist/taskPrecheck.d.ts.map +1 -0
- package/dist/taskPrecheck.js +329 -0
- package/dist/taskPrecheck.js.map +1 -0
- package/dist/taskStateSync.d.ts +8 -0
- package/dist/taskStateSync.d.ts.map +1 -0
- package/dist/taskStateSync.js +79 -0
- package/dist/taskStateSync.js.map +1 -0
- package/dist/tasks.d.ts +140 -0
- package/dist/tasks.d.ts.map +1 -0
- package/dist/tasks.js +1281 -0
- package/dist/tasks.js.map +1 -0
- package/dist/team-config.d.ts +24 -0
- package/dist/team-config.d.ts.map +1 -0
- package/dist/team-config.js +221 -0
- package/dist/team-config.js.map +1 -0
- package/dist/team-doctor.d.ts +22 -0
- package/dist/team-doctor.d.ts.map +1 -0
- package/dist/team-doctor.js +270 -0
- package/dist/team-doctor.js.map +1 -0
- package/dist/team-pulse.d.ts +52 -0
- package/dist/team-pulse.d.ts.map +1 -0
- package/dist/team-pulse.js +176 -0
- package/dist/team-pulse.js.map +1 -0
- package/dist/telemetry.d.ts +74 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +256 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/test-task-filter.d.ts +21 -0
- package/dist/test-task-filter.d.ts.map +1 -0
- package/dist/test-task-filter.js +48 -0
- package/dist/test-task-filter.js.map +1 -0
- package/dist/types.d.ts +126 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/dist/usage-tracking.d.ts +101 -0
- package/dist/usage-tracking.d.ts.map +1 -0
- package/dist/usage-tracking.js +325 -0
- package/dist/usage-tracking.js.map +1 -0
- package/dist/vector-store.d.ts +87 -0
- package/dist/vector-store.d.ts.map +1 -0
- package/dist/vector-store.js +247 -0
- package/dist/vector-store.js.map +1 -0
- package/dist/watchdog/idleNudgeLane.d.ts +22 -0
- package/dist/watchdog/idleNudgeLane.d.ts.map +1 -0
- package/dist/watchdog/idleNudgeLane.js +98 -0
- package/dist/watchdog/idleNudgeLane.js.map +1 -0
- package/dist/webhooks.d.ts +103 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +398 -0
- package/dist/webhooks.js.map +1 -0
- package/dist/working-contract.d.ts +42 -0
- package/dist/working-contract.d.ts.map +1 -0
- package/dist/working-contract.js +228 -0
- package/dist/working-contract.js.map +1 -0
- package/dist/ws-heartbeat.d.ts +66 -0
- package/dist/ws-heartbeat.d.ts.map +1 -0
- package/dist/ws-heartbeat.js +174 -0
- package/dist/ws-heartbeat.js.map +1 -0
- package/package.json +87 -0
- package/plugins/reflectt-channel/README.md +96 -0
- package/plugins/reflectt-channel/index.ts +789 -0
- package/plugins/reflectt-channel/openclaw.plugin.json +23 -0
- package/plugins/reflectt-channel/package.json +23 -0
- package/plugins/reflectt-channel/src/channel.ts +433 -0
- package/plugins/reflectt-channel/src/types.ts +29 -0
- package/public/avatars/echo.png +0 -0
- package/public/avatars/harmony.png +0 -0
- package/public/avatars/kai.png +0 -0
- package/public/avatars/link.png +0 -0
- package/public/avatars/pixel.png +0 -0
- package/public/avatars/rhythm.png +0 -0
- package/public/avatars/ryan.png +0 -0
- package/public/avatars/sage.png +0 -0
- package/public/avatars/scout.png +0 -0
- package/public/avatars/spark.png +0 -0
- package/public/dashboard-animations.css +381 -0
- package/public/dashboard.js +3479 -0
- package/public/docs.md +1062 -0
- package/public/file-upload-mock.html +1097 -0
- package/public/og-card.png +0 -0
- package/public/ui-kit.html +318 -0
- package/public/widget/feedback.js +194 -0
package/dist/insights.js
ADDED
|
@@ -0,0 +1,871 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Insight clustering + dedupe/cooldown engine
|
|
3
|
+
// Clusters reflections → insights with promotion gates and cooldown controls
|
|
4
|
+
import { getDb, safeJsonStringify, safeJsonParse } from './db.js';
|
|
5
|
+
import { eventBus } from './events.js';
|
|
6
|
+
// ── Constants ──
|
|
7
|
+
const COOLDOWN_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
8
|
+
// v1.1: 2-unique-authors gate is tuned for a small team (~6-8 agents).
|
|
9
|
+
// At scale, replace with a ratio/volume gate: max(2, ceil(team_size * X%))
|
|
10
|
+
const PROMOTION_THRESHOLD = 2; // independent reflections needed
|
|
11
|
+
// v1.1: max reopen count per 24h sliding window. After this, route to pending_triage.
|
|
12
|
+
const MAX_REOPEN_COUNT_24H = 3;
|
|
13
|
+
/** Scoring engine version — increment on any rule change for audit trail */
|
|
14
|
+
export const SCORING_ENGINE_VERSION = '1.1.0';
|
|
15
|
+
// ── Hysteresis config ──
|
|
16
|
+
// Buffer zone around priority thresholds to prevent flapping.
|
|
17
|
+
// A score must cross threshold + buffer to change priority upward,
|
|
18
|
+
// or drop below threshold - buffer to change downward.
|
|
19
|
+
export const HYSTERESIS_BUFFER = 0.3;
|
|
20
|
+
export const PRIORITY_THRESHOLDS = { P0: 8, P1: 5, P2: 3 };
|
|
21
|
+
export const INSIGHT_STATUSES = ['candidate', 'promoted', 'pending_triage', 'task_created', 'cooldown', 'closed'];
|
|
22
|
+
export const PROMOTION_READINESS = ['not_ready', 'ready', 'promoted', 'override'];
|
|
23
|
+
function rowToInsight(row) {
|
|
24
|
+
return {
|
|
25
|
+
id: row.id,
|
|
26
|
+
cluster_key: row.cluster_key,
|
|
27
|
+
workflow_stage: row.workflow_stage,
|
|
28
|
+
failure_family: row.failure_family,
|
|
29
|
+
impacted_unit: row.impacted_unit,
|
|
30
|
+
title: row.title,
|
|
31
|
+
status: row.status,
|
|
32
|
+
score: row.score,
|
|
33
|
+
priority: row.priority,
|
|
34
|
+
reflection_ids: safeJsonParse(row.reflection_ids) ?? [],
|
|
35
|
+
independent_count: row.independent_count,
|
|
36
|
+
evidence_refs: safeJsonParse(row.evidence_refs) ?? [],
|
|
37
|
+
authors: safeJsonParse(row.authors) ?? [],
|
|
38
|
+
promotion_readiness: row.promotion_readiness,
|
|
39
|
+
recurring_candidate: row.recurring_candidate === 1,
|
|
40
|
+
cooldown_until: row.cooldown_until,
|
|
41
|
+
cooldown_reason: row.cooldown_reason,
|
|
42
|
+
severity_max: row.severity_max,
|
|
43
|
+
task_id: row.task_id ?? null,
|
|
44
|
+
metadata: safeJsonParse(row.metadata),
|
|
45
|
+
created_at: row.created_at,
|
|
46
|
+
updated_at: row.updated_at,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// ── Helpers ──
|
|
50
|
+
function generateId() {
|
|
51
|
+
return `ins-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Derive priority from score (no hysteresis — used for initial assignment).
|
|
55
|
+
*/
|
|
56
|
+
export function scoreToPriority(score) {
|
|
57
|
+
if (score >= PRIORITY_THRESHOLDS.P0)
|
|
58
|
+
return 'P0';
|
|
59
|
+
if (score >= PRIORITY_THRESHOLDS.P1)
|
|
60
|
+
return 'P1';
|
|
61
|
+
if (score >= PRIORITY_THRESHOLDS.P2)
|
|
62
|
+
return 'P2';
|
|
63
|
+
return 'P3';
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Priority with hysteresis: prevents flapping near threshold boundaries.
|
|
67
|
+
*
|
|
68
|
+
* - To upgrade priority (e.g. P1→P0), score must exceed threshold + buffer
|
|
69
|
+
* - To downgrade (e.g. P0→P1), score must drop below threshold - buffer
|
|
70
|
+
* - If score is in the buffer zone, previous priority is retained
|
|
71
|
+
*/
|
|
72
|
+
export function scoreToPriorityWithHysteresis(score, previousPriority) {
|
|
73
|
+
if (!previousPriority)
|
|
74
|
+
return scoreToPriority(score);
|
|
75
|
+
const buf = HYSTERESIS_BUFFER;
|
|
76
|
+
const { P0, P1, P2 } = PRIORITY_THRESHOLDS;
|
|
77
|
+
if (previousPriority === 'P0') {
|
|
78
|
+
if (score >= P0 - buf)
|
|
79
|
+
return 'P0';
|
|
80
|
+
}
|
|
81
|
+
if (previousPriority === 'P1') {
|
|
82
|
+
if (score >= P0 + buf)
|
|
83
|
+
return 'P0';
|
|
84
|
+
if (score >= P1 - buf)
|
|
85
|
+
return 'P1';
|
|
86
|
+
}
|
|
87
|
+
if (previousPriority === 'P2') {
|
|
88
|
+
if (score >= P0 + buf)
|
|
89
|
+
return 'P0';
|
|
90
|
+
if (score >= P1 + buf)
|
|
91
|
+
return 'P1';
|
|
92
|
+
if (score >= P2 - buf)
|
|
93
|
+
return 'P2';
|
|
94
|
+
}
|
|
95
|
+
if (previousPriority === 'P3') {
|
|
96
|
+
if (score >= P0 + buf)
|
|
97
|
+
return 'P0';
|
|
98
|
+
if (score >= P1 + buf)
|
|
99
|
+
return 'P1';
|
|
100
|
+
if (score >= P2 + buf)
|
|
101
|
+
return 'P2';
|
|
102
|
+
return 'P3';
|
|
103
|
+
}
|
|
104
|
+
return scoreToPriority(score);
|
|
105
|
+
}
|
|
106
|
+
function buildClusterKeyString(key) {
|
|
107
|
+
return `${key.workflow_stage}::${key.failure_family}::${key.impacted_unit}`;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Compute aggregate score from linked reflections.
|
|
111
|
+
* Base = max confidence, +severity boost, +volume boost.
|
|
112
|
+
*/
|
|
113
|
+
export function computeScore(reflections) {
|
|
114
|
+
if (reflections.length === 0)
|
|
115
|
+
return 0;
|
|
116
|
+
const maxConf = Math.max(...reflections.map(r => r.confidence));
|
|
117
|
+
const severityBoost = reflections.reduce((max, r) => {
|
|
118
|
+
if (r.severity === 'critical')
|
|
119
|
+
return Math.max(max, 2);
|
|
120
|
+
if (r.severity === 'high')
|
|
121
|
+
return Math.max(max, 1);
|
|
122
|
+
return max;
|
|
123
|
+
}, 0);
|
|
124
|
+
// v1.1: volumeBoost cap=2 — flag for day-7 review (n=10 and n=5 score similarly)
|
|
125
|
+
const volumeBoost = Math.min((reflections.length - 1) * 0.5, 2);
|
|
126
|
+
return Math.min(10, Math.round((maxConf + severityBoost + volumeBoost) * 10) / 10);
|
|
127
|
+
}
|
|
128
|
+
function maxSeverity(reflections) {
|
|
129
|
+
const order = ['low', 'medium', 'high', 'critical'];
|
|
130
|
+
let best = -1;
|
|
131
|
+
for (const r of reflections) {
|
|
132
|
+
if (r.severity) {
|
|
133
|
+
const idx = order.indexOf(r.severity);
|
|
134
|
+
if (idx > best)
|
|
135
|
+
best = idx;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return best >= 0 ? order[best] : null;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Build cluster key from a reflection.
|
|
142
|
+
*
|
|
143
|
+
* NOTE: When a reflection lacks explicit `unit:` / `stage:` tags, we try to
|
|
144
|
+
* avoid collapsing everything into `unknown::*::unknown` by:
|
|
145
|
+
* - deriving unit from the first non-reserved tag (e.g. `chat`, `status-discipline`)
|
|
146
|
+
* - as a last resort, deriving a short topic signature from the pain text
|
|
147
|
+
*/
|
|
148
|
+
export function extractClusterKey(reflection) {
|
|
149
|
+
const tags = reflection.tags ?? [];
|
|
150
|
+
const stageRaw = tags.find(t => t.startsWith('stage:'))?.slice(6) ?? 'unknown';
|
|
151
|
+
const familyRaw = tags.find(t => t.startsWith('family:'))?.slice(7) ?? _inferFailureFamily(reflection.pain);
|
|
152
|
+
const explicitUnit = tags.find(t => t.startsWith('unit:'))?.slice(5);
|
|
153
|
+
const inferredUnitFromTags = _inferImpactedUnitFromTags(tags);
|
|
154
|
+
let unitRaw = explicitUnit
|
|
155
|
+
?? inferredUnitFromTags
|
|
156
|
+
?? reflection.team_id
|
|
157
|
+
?? 'unknown';
|
|
158
|
+
// If we still have no unit, derive a small topic signature so unrelated
|
|
159
|
+
// reflections don't cluster into the same `unknown::*::unknown` bucket.
|
|
160
|
+
//
|
|
161
|
+
// Additionally: if the only signal we have is a *very broad umbrella* tag
|
|
162
|
+
// (e.g. "openclaw"), append a topic signature so distinct problems inside
|
|
163
|
+
// that umbrella don't get accidentally clustered together.
|
|
164
|
+
if (!explicitUnit) {
|
|
165
|
+
const topic = _inferTopicFromPain(reflection.pain);
|
|
166
|
+
if (topic && unitRaw && _isUmbrellaUnit(unitRaw) && _hasOnlyUmbrellaOrGenericUnitTags(tags)) {
|
|
167
|
+
unitRaw = `${unitRaw}-${topic}`;
|
|
168
|
+
}
|
|
169
|
+
if ((!unitRaw || unitRaw === 'unknown') && topic) {
|
|
170
|
+
unitRaw = topic;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
workflow_stage: _sanitizeClusterPart(stageRaw) || 'unknown',
|
|
175
|
+
failure_family: _sanitizeClusterPart(familyRaw) || 'uncategorized',
|
|
176
|
+
impacted_unit: _sanitizeClusterPart(unitRaw) || 'unknown',
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function _sanitizeClusterPart(part) {
|
|
180
|
+
if (!part)
|
|
181
|
+
return '';
|
|
182
|
+
return part
|
|
183
|
+
.trim()
|
|
184
|
+
.toLowerCase()
|
|
185
|
+
// prevent delimiter collisions
|
|
186
|
+
.replace(/::+/g, '-')
|
|
187
|
+
.replace(/:+/g, '-')
|
|
188
|
+
.replace(/\s+/g, '-')
|
|
189
|
+
// keep keys compact + url/db friendly
|
|
190
|
+
.replace(/[^a-z0-9._-]/g, '')
|
|
191
|
+
.slice(0, 64);
|
|
192
|
+
}
|
|
193
|
+
function _inferImpactedUnitFromTags(tags) {
|
|
194
|
+
if (!tags || tags.length === 0)
|
|
195
|
+
return null;
|
|
196
|
+
const reservedPrefixes = ['stage:', 'family:', 'unit:', 'team:'];
|
|
197
|
+
const candidates = tags
|
|
198
|
+
.map(t => t.trim())
|
|
199
|
+
.filter(Boolean)
|
|
200
|
+
.filter(t => !reservedPrefixes.some(p => t.startsWith(p)));
|
|
201
|
+
if (candidates.length === 0)
|
|
202
|
+
return null;
|
|
203
|
+
// Prefer a non-generic tag if possible, but keep deterministic ordering.
|
|
204
|
+
const generic = new Set([
|
|
205
|
+
// broad families
|
|
206
|
+
'performance', 'data-loss', 'runtime-error', 'access', 'ui', 'config', 'deployment', 'testing', 'uncategorized',
|
|
207
|
+
// broad/ambiguous unit tags (avoid clustering everything on these)
|
|
208
|
+
'openclaw', 'reflectt', 'reflectt-node', 'reflectt-cloud',
|
|
209
|
+
// too-generic symptoms
|
|
210
|
+
'memory', 'latency', 'timeout',
|
|
211
|
+
]);
|
|
212
|
+
return candidates.find(t => !generic.has(t.toLowerCase())) ?? candidates[0];
|
|
213
|
+
}
|
|
214
|
+
function _isUmbrellaUnit(unit) {
|
|
215
|
+
const u = (unit || '').trim().toLowerCase();
|
|
216
|
+
return u === 'openclaw' || u === 'reflectt' || u === 'reflectt-node' || u === 'reflectt-cloud';
|
|
217
|
+
}
|
|
218
|
+
function _hasOnlyUmbrellaOrGenericUnitTags(tags) {
|
|
219
|
+
const reservedPrefixes = ['stage:', 'family:', 'unit:', 'team:'];
|
|
220
|
+
const candidates = (tags || [])
|
|
221
|
+
.map(t => t.trim())
|
|
222
|
+
.filter(Boolean)
|
|
223
|
+
.filter(t => !reservedPrefixes.some(p => t.startsWith(p)));
|
|
224
|
+
if (candidates.length === 0)
|
|
225
|
+
return true;
|
|
226
|
+
const generic = new Set([
|
|
227
|
+
// umbrellas
|
|
228
|
+
'openclaw', 'reflectt', 'reflectt-node', 'reflectt-cloud',
|
|
229
|
+
// broad families + symptoms that are often not "the unit"
|
|
230
|
+
'performance', 'data-loss', 'runtime-error', 'access', 'ui', 'config', 'deployment', 'testing', 'uncategorized',
|
|
231
|
+
'memory', 'latency', 'timeout',
|
|
232
|
+
]);
|
|
233
|
+
return candidates.every(t => generic.has(t.toLowerCase()));
|
|
234
|
+
}
|
|
235
|
+
function _inferTopicFromPain(pain) {
|
|
236
|
+
const lower = (pain || '').toLowerCase();
|
|
237
|
+
if (!lower.trim())
|
|
238
|
+
return null;
|
|
239
|
+
// Simple, stable signature: first 3 meaningful words (len>=4), stopword-filtered.
|
|
240
|
+
const stop = new Set(['this', 'that', 'with', 'from', 'into', 'onto', 'when', 'then', 'than', 'over', 'under', 'only', 'just', 'some', 'much', 'very', 'more', 'most', 'less', 'have', 'has', 'had', 'been', 'were', 'was', 'are', 'and', 'the', 'for', 'but', 'not', 'too', 'yet']);
|
|
241
|
+
const words = lower
|
|
242
|
+
.replace(/[^a-z0-9\s-]/g, ' ')
|
|
243
|
+
.split(/\s+/)
|
|
244
|
+
.map(w => w.trim())
|
|
245
|
+
.filter(w => w.length >= 4)
|
|
246
|
+
.filter(w => !stop.has(w));
|
|
247
|
+
if (words.length === 0)
|
|
248
|
+
return null;
|
|
249
|
+
const sig = words.slice(0, 3).join('-');
|
|
250
|
+
return `topic-${sig}`;
|
|
251
|
+
}
|
|
252
|
+
function _inferFailureFamily(pain) {
|
|
253
|
+
const lower = pain.toLowerCase();
|
|
254
|
+
if (/truncat|cut.?off|missing.?text|incomplete/i.test(lower))
|
|
255
|
+
return 'data-loss';
|
|
256
|
+
if (/crash|exception|error|fail/i.test(lower))
|
|
257
|
+
return 'runtime-error';
|
|
258
|
+
if (/slow|timeout|latency|performance/i.test(lower))
|
|
259
|
+
return 'performance';
|
|
260
|
+
if (/auth|permission|denied|forbidden/i.test(lower))
|
|
261
|
+
return 'access';
|
|
262
|
+
if (/ui|display|render|layout|style/i.test(lower))
|
|
263
|
+
return 'ui';
|
|
264
|
+
if (/config|setting|env/i.test(lower))
|
|
265
|
+
return 'config';
|
|
266
|
+
if (/deploy|release|build|ci/i.test(lower))
|
|
267
|
+
return 'deployment';
|
|
268
|
+
if (/test|coverage|flak/i.test(lower))
|
|
269
|
+
return 'testing';
|
|
270
|
+
return 'uncategorized';
|
|
271
|
+
}
|
|
272
|
+
export function buildDecisionTrace(reflections, clusterKeyStr, readiness, previousPriority, score) {
|
|
273
|
+
const maxConf = reflections.length > 0 ? Math.max(...reflections.map(r => r.confidence)) : 0;
|
|
274
|
+
const severityBoost = reflections.reduce((max, r) => {
|
|
275
|
+
if (r.severity === 'critical')
|
|
276
|
+
return Math.max(max, 2);
|
|
277
|
+
if (r.severity === 'high')
|
|
278
|
+
return Math.max(max, 1);
|
|
279
|
+
return max;
|
|
280
|
+
}, 0);
|
|
281
|
+
const volumeBoost = Math.min((reflections.length - 1) * 0.5, 2);
|
|
282
|
+
const contributors = [];
|
|
283
|
+
contributors.push({ factor: 'max_confidence', value: maxConf, description: 'Highest reflection confidence' });
|
|
284
|
+
if (severityBoost > 0)
|
|
285
|
+
contributors.push({ factor: 'severity_boost', value: severityBoost, description: 'Max severity boost (high=+1, critical=+2)' });
|
|
286
|
+
if (volumeBoost > 0)
|
|
287
|
+
contributors.push({ factor: 'volume_boost', value: volumeBoost, description: `${reflections.length} reflections (+0.5 each, max +2)` });
|
|
288
|
+
const withHysteresis = scoreToPriorityWithHysteresis(score, previousPriority);
|
|
289
|
+
const without = scoreToPriority(score);
|
|
290
|
+
return {
|
|
291
|
+
version: SCORING_ENGINE_VERSION,
|
|
292
|
+
dedupe_cluster_id: clusterKeyStr,
|
|
293
|
+
promotion_band: readiness,
|
|
294
|
+
top_contributors: contributors.sort((a, b) => b.value - a.value),
|
|
295
|
+
hysteresis_applied: withHysteresis !== without,
|
|
296
|
+
previous_priority: previousPriority,
|
|
297
|
+
raw_score: score,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
// ── Promotion gate ──
|
|
301
|
+
/**
|
|
302
|
+
* Minimum content quality check for a reflection.
|
|
303
|
+
* Prevents synthetic/test reflections with sparse content from triggering
|
|
304
|
+
* promotion. Each key field must have at least MIN_FIELD_LENGTH chars
|
|
305
|
+
* and at least MIN_QUALITY_FIELDS fields must pass.
|
|
306
|
+
*/
|
|
307
|
+
const MIN_FIELD_LENGTH = 10;
|
|
308
|
+
const MIN_QUALITY_FIELDS = 3;
|
|
309
|
+
export function hasMinimumQuality(reflection) {
|
|
310
|
+
const fields = [reflection.pain, reflection.impact, reflection.suspected_why, reflection.proposed_fix];
|
|
311
|
+
const qualifyingFields = fields.filter(f => f && f.trim().length >= MIN_FIELD_LENGTH);
|
|
312
|
+
return qualifyingFields.length >= MIN_QUALITY_FIELDS;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Can this set of reflections promote an insight?
|
|
316
|
+
*
|
|
317
|
+
* Rules:
|
|
318
|
+
* 0. At least one reflection must pass minimum quality gate
|
|
319
|
+
* 1. >= 2 independent reflections (different authors)
|
|
320
|
+
* 2. OR 1 high/critical-severity reflection with evidence (override)
|
|
321
|
+
*/
|
|
322
|
+
export function canPromote(reflections) {
|
|
323
|
+
// Quality gate: at least one substantive reflection required
|
|
324
|
+
const hasQualityReflection = reflections.some(hasMinimumQuality);
|
|
325
|
+
if (!hasQualityReflection)
|
|
326
|
+
return false;
|
|
327
|
+
// High-severity override (only from quality reflections)
|
|
328
|
+
const hasHighSeverityWithEvidence = reflections.some(r => (r.severity === 'high' || r.severity === 'critical') && r.evidence.length > 0 && hasMinimumQuality(r));
|
|
329
|
+
if (hasHighSeverityWithEvidence)
|
|
330
|
+
return true;
|
|
331
|
+
// Standard: 2 independent authors (at least one quality)
|
|
332
|
+
const uniqueAuthors = new Set(reflections.map(r => r.author));
|
|
333
|
+
return uniqueAuthors.size >= PROMOTION_THRESHOLD;
|
|
334
|
+
}
|
|
335
|
+
// ── Core engine ──
|
|
336
|
+
/**
|
|
337
|
+
* Find existing active insight matching a cluster key.
|
|
338
|
+
*/
|
|
339
|
+
export function findByCluster(clusterKeyStr) {
|
|
340
|
+
const db = getDb();
|
|
341
|
+
const row = db.prepare(`
|
|
342
|
+
SELECT * FROM insights
|
|
343
|
+
WHERE cluster_key = ? AND status != 'closed'
|
|
344
|
+
ORDER BY created_at DESC LIMIT 1
|
|
345
|
+
`).get(clusterKeyStr);
|
|
346
|
+
return row ? rowToInsight(row) : null;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Ingest a reflection into the insight engine.
|
|
350
|
+
*/
|
|
351
|
+
export function ingestReflection(reflection) {
|
|
352
|
+
const db = getDb();
|
|
353
|
+
const now = Date.now();
|
|
354
|
+
const clusterKey = extractClusterKey(reflection);
|
|
355
|
+
const clusterKeyStr = buildClusterKeyString(clusterKey);
|
|
356
|
+
let existing = findByCluster(clusterKeyStr);
|
|
357
|
+
if (existing) {
|
|
358
|
+
// Cooldown: if in cooldown and new reflection arrives, check reopen cap then reopen
|
|
359
|
+
if (existing.status === 'cooldown') {
|
|
360
|
+
if (existing.cooldown_until && now < existing.cooldown_until) {
|
|
361
|
+
// v1.1: enforce max reopen count (3 per 24h) — route to pending_triage if exceeded
|
|
362
|
+
const reopenCount = countRecentReopens(existing);
|
|
363
|
+
if (reopenCount >= MAX_REOPEN_COUNT_24H) {
|
|
364
|
+
return routeToPendingTriage(existing, reflection, 'reopen_cap_exceeded');
|
|
365
|
+
}
|
|
366
|
+
return reopenInsight(existing, reflection);
|
|
367
|
+
}
|
|
368
|
+
// Cooldown expired → close and create fresh
|
|
369
|
+
db.prepare('UPDATE insights SET status = ?, updated_at = ? WHERE id = ?')
|
|
370
|
+
.run('closed', now, existing.id);
|
|
371
|
+
existing = null;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (existing) {
|
|
375
|
+
return addReflectionToInsight(existing, reflection);
|
|
376
|
+
}
|
|
377
|
+
return createInsight(clusterKey, clusterKeyStr, reflection);
|
|
378
|
+
}
|
|
379
|
+
function createInsight(key, clusterKeyStr, reflection) {
|
|
380
|
+
const db = getDb();
|
|
381
|
+
const now = Date.now();
|
|
382
|
+
const id = generateId();
|
|
383
|
+
const reflections = [reflection];
|
|
384
|
+
const score = computeScore(reflections);
|
|
385
|
+
const priority = scoreToPriority(score);
|
|
386
|
+
const authors = [reflection.author];
|
|
387
|
+
const evidenceRefs = [...reflection.evidence];
|
|
388
|
+
const shouldPromote = canPromote(reflections);
|
|
389
|
+
const status = shouldPromote ? 'promoted' : 'candidate';
|
|
390
|
+
const readiness = shouldPromote
|
|
391
|
+
? (reflection.severity === 'high' || reflection.severity === 'critical' ? 'override' : 'promoted')
|
|
392
|
+
: 'not_ready';
|
|
393
|
+
const cooldownUntil = shouldPromote ? now + COOLDOWN_MS : null;
|
|
394
|
+
const cooldownReason = shouldPromote ? 'auto-promoted' : null;
|
|
395
|
+
const sevMax = maxSeverity(reflections);
|
|
396
|
+
const title = `${key.failure_family}: ${reflection.pain.slice(0, 80)}`;
|
|
397
|
+
// Build initial decision trace for audit
|
|
398
|
+
const decisionTrace = buildDecisionTrace(reflections, clusterKeyStr, readiness, null, score);
|
|
399
|
+
const metadata = {
|
|
400
|
+
decision_trace: decisionTrace,
|
|
401
|
+
dedupe_cluster_id: clusterKeyStr,
|
|
402
|
+
promotion_band: readiness,
|
|
403
|
+
scoring_version: SCORING_ENGINE_VERSION,
|
|
404
|
+
};
|
|
405
|
+
const insight = {
|
|
406
|
+
id,
|
|
407
|
+
cluster_key: clusterKeyStr,
|
|
408
|
+
workflow_stage: key.workflow_stage,
|
|
409
|
+
failure_family: key.failure_family,
|
|
410
|
+
impacted_unit: key.impacted_unit,
|
|
411
|
+
title,
|
|
412
|
+
status,
|
|
413
|
+
score,
|
|
414
|
+
priority,
|
|
415
|
+
reflection_ids: [reflection.id],
|
|
416
|
+
independent_count: 1,
|
|
417
|
+
evidence_refs: evidenceRefs,
|
|
418
|
+
authors,
|
|
419
|
+
promotion_readiness: readiness,
|
|
420
|
+
recurring_candidate: false,
|
|
421
|
+
cooldown_until: cooldownUntil,
|
|
422
|
+
cooldown_reason: cooldownReason,
|
|
423
|
+
severity_max: sevMax,
|
|
424
|
+
metadata,
|
|
425
|
+
created_at: now,
|
|
426
|
+
updated_at: now,
|
|
427
|
+
};
|
|
428
|
+
db.prepare(`
|
|
429
|
+
INSERT INTO insights (
|
|
430
|
+
id, cluster_key, workflow_stage, failure_family, impacted_unit,
|
|
431
|
+
title, status, score, priority, reflection_ids, independent_count,
|
|
432
|
+
evidence_refs, authors, promotion_readiness, recurring_candidate,
|
|
433
|
+
cooldown_until, cooldown_reason, severity_max, metadata, created_at, updated_at
|
|
434
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
435
|
+
`).run(id, clusterKeyStr, key.workflow_stage, key.failure_family, key.impacted_unit, title, status, score, priority, safeJsonStringify(insight.reflection_ids), 1, safeJsonStringify(evidenceRefs), safeJsonStringify(authors), readiness, 0, cooldownUntil, cooldownReason, sevMax, safeJsonStringify(metadata), now, now);
|
|
436
|
+
if (shouldPromote) {
|
|
437
|
+
eventBus.emit({
|
|
438
|
+
id: `evt-insight-promoted-${id}`,
|
|
439
|
+
type: 'insight_created',
|
|
440
|
+
timestamp: Date.now(),
|
|
441
|
+
data: { kind: 'insight:promoted', insightId: id, priority, score },
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
eventBus.emit({
|
|
446
|
+
id: `evt-insight-created-${id}`,
|
|
447
|
+
type: 'insight_created',
|
|
448
|
+
timestamp: Date.now(),
|
|
449
|
+
data: { kind: 'insight:created', insightId: id },
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
return insight;
|
|
453
|
+
}
|
|
454
|
+
function addReflectionToInsight(existing, reflection) {
|
|
455
|
+
const db = getDb();
|
|
456
|
+
const now = Date.now();
|
|
457
|
+
// Dedupe
|
|
458
|
+
if (existing.reflection_ids.includes(reflection.id))
|
|
459
|
+
return existing;
|
|
460
|
+
const updatedIds = [...existing.reflection_ids, reflection.id];
|
|
461
|
+
const updatedAuthors = [...new Set([...existing.authors, reflection.author])];
|
|
462
|
+
const updatedEvidence = [...new Set([...existing.evidence_refs, ...reflection.evidence])];
|
|
463
|
+
const allReflections = loadReflectionsById(updatedIds);
|
|
464
|
+
const score = computeScore(allReflections);
|
|
465
|
+
const sevMax = maxSeverity(allReflections);
|
|
466
|
+
const wasCandidate = existing.status === 'candidate';
|
|
467
|
+
const shouldPromote = wasCandidate && canPromote(allReflections);
|
|
468
|
+
// Apply hysteresis to prevent priority flapping
|
|
469
|
+
const priority = scoreToPriorityWithHysteresis(score, existing.priority);
|
|
470
|
+
const status = shouldPromote ? 'promoted' : existing.status;
|
|
471
|
+
const readiness = shouldPromote
|
|
472
|
+
? (updatedAuthors.length >= PROMOTION_THRESHOLD ? 'promoted' : 'override')
|
|
473
|
+
: (canPromote(allReflections) ? 'ready' : existing.promotion_readiness);
|
|
474
|
+
const cooldownUntil = shouldPromote ? now + COOLDOWN_MS : existing.cooldown_until;
|
|
475
|
+
const cooldownReason = shouldPromote ? 'auto-promoted' : existing.cooldown_reason;
|
|
476
|
+
// Recurring if reopened or has many reflections
|
|
477
|
+
const recurring = updatedIds.length >= 4;
|
|
478
|
+
// Build decision trace for audit
|
|
479
|
+
const decisionTrace = buildDecisionTrace(allReflections, existing.cluster_key, readiness, existing.priority, score);
|
|
480
|
+
const updatedMetadata = {
|
|
481
|
+
...(existing.metadata || {}),
|
|
482
|
+
decision_trace: decisionTrace,
|
|
483
|
+
dedupe_cluster_id: existing.cluster_key,
|
|
484
|
+
promotion_band: readiness,
|
|
485
|
+
scoring_version: SCORING_ENGINE_VERSION,
|
|
486
|
+
};
|
|
487
|
+
db.prepare(`
|
|
488
|
+
UPDATE insights SET
|
|
489
|
+
reflection_ids = ?, independent_count = ?, evidence_refs = ?,
|
|
490
|
+
authors = ?, score = ?, priority = ?, status = ?,
|
|
491
|
+
promotion_readiness = ?, recurring_candidate = ?,
|
|
492
|
+
cooldown_until = ?, cooldown_reason = ?, severity_max = ?,
|
|
493
|
+
metadata = ?, updated_at = ?
|
|
494
|
+
WHERE id = ?
|
|
495
|
+
`).run(safeJsonStringify(updatedIds), updatedAuthors.length, safeJsonStringify(updatedEvidence), safeJsonStringify(updatedAuthors), score, priority, status, readiness, recurring ? 1 : 0, cooldownUntil, cooldownReason, sevMax, safeJsonStringify(updatedMetadata), now, existing.id);
|
|
496
|
+
if (shouldPromote && wasCandidate) {
|
|
497
|
+
eventBus.emit({
|
|
498
|
+
id: `evt-insight-promoted-${existing.id}`,
|
|
499
|
+
type: 'insight_created',
|
|
500
|
+
timestamp: Date.now(),
|
|
501
|
+
data: { kind: 'insight:promoted', insightId: existing.id, priority, score },
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
return {
|
|
505
|
+
...existing,
|
|
506
|
+
reflection_ids: updatedIds,
|
|
507
|
+
independent_count: updatedAuthors.length,
|
|
508
|
+
evidence_refs: updatedEvidence,
|
|
509
|
+
authors: updatedAuthors,
|
|
510
|
+
score,
|
|
511
|
+
priority,
|
|
512
|
+
status,
|
|
513
|
+
promotion_readiness: readiness,
|
|
514
|
+
recurring_candidate: recurring,
|
|
515
|
+
cooldown_until: cooldownUntil,
|
|
516
|
+
cooldown_reason: cooldownReason,
|
|
517
|
+
severity_max: sevMax,
|
|
518
|
+
metadata: updatedMetadata,
|
|
519
|
+
updated_at: now,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Count recent reopens for an insight within a 24h sliding window.
|
|
524
|
+
* Uses reopen_count and reopen_window_start stored directly on the insight row.
|
|
525
|
+
* Resets window if >24h has passed since the window started.
|
|
526
|
+
*/
|
|
527
|
+
function countRecentReopens(existing) {
|
|
528
|
+
const now = Date.now();
|
|
529
|
+
const meta = (existing.metadata ?? {});
|
|
530
|
+
const windowStart = meta.reopen_window_start ?? 0;
|
|
531
|
+
const count = meta.reopen_count_24h ?? 0;
|
|
532
|
+
// If window has expired (>24h), count resets
|
|
533
|
+
if (now - windowStart > COOLDOWN_MS) {
|
|
534
|
+
return 0;
|
|
535
|
+
}
|
|
536
|
+
return count;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Route an insight to pending_triage when reopen cap is exceeded.
|
|
540
|
+
* v1.1: prevents repeated auto-promote loops while capturing recurrence signal.
|
|
541
|
+
*/
|
|
542
|
+
function routeToPendingTriage(existing, reflection, reason) {
|
|
543
|
+
const db = getDb();
|
|
544
|
+
const now = Date.now();
|
|
545
|
+
const updatedIds = existing.reflection_ids.includes(reflection.id)
|
|
546
|
+
? existing.reflection_ids
|
|
547
|
+
: [...existing.reflection_ids, reflection.id];
|
|
548
|
+
const updatedAuthors = [...new Set([...existing.authors, reflection.author])];
|
|
549
|
+
const updatedEvidence = [...new Set([...existing.evidence_refs, ...reflection.evidence])];
|
|
550
|
+
const allReflections = loadReflectionsById(updatedIds);
|
|
551
|
+
const score = computeScore(allReflections);
|
|
552
|
+
const priority = scoreToPriority(score);
|
|
553
|
+
const sevMax = maxSeverity(allReflections);
|
|
554
|
+
db.prepare(`
|
|
555
|
+
UPDATE insights SET
|
|
556
|
+
status = 'pending_triage', reflection_ids = ?, independent_count = ?,
|
|
557
|
+
evidence_refs = ?, authors = ?, score = ?, priority = ?,
|
|
558
|
+
promotion_readiness = 'promoted', recurring_candidate = 1,
|
|
559
|
+
cooldown_reason = ?, severity_max = ?, updated_at = ?
|
|
560
|
+
WHERE id = ?
|
|
561
|
+
`).run(safeJsonStringify(updatedIds), updatedAuthors.length, safeJsonStringify(updatedEvidence), safeJsonStringify(updatedAuthors), score, priority, reason, sevMax, now, existing.id);
|
|
562
|
+
eventBus.emit({
|
|
563
|
+
id: `evt-insight-triage-${existing.id}-${now}`,
|
|
564
|
+
type: 'task_updated',
|
|
565
|
+
timestamp: now,
|
|
566
|
+
data: { kind: 'insight:reopen_cap_exceeded', insightId: existing.id, reopenReason: reason },
|
|
567
|
+
});
|
|
568
|
+
return {
|
|
569
|
+
...existing,
|
|
570
|
+
status: 'pending_triage',
|
|
571
|
+
reflection_ids: updatedIds,
|
|
572
|
+
independent_count: updatedAuthors.length,
|
|
573
|
+
evidence_refs: updatedEvidence,
|
|
574
|
+
authors: updatedAuthors,
|
|
575
|
+
score,
|
|
576
|
+
priority,
|
|
577
|
+
promotion_readiness: 'promoted',
|
|
578
|
+
recurring_candidate: true,
|
|
579
|
+
cooldown_reason: reason,
|
|
580
|
+
severity_max: sevMax,
|
|
581
|
+
updated_at: now,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
function reopenInsight(existing, reflection) {
|
|
585
|
+
const db = getDb();
|
|
586
|
+
const now = Date.now();
|
|
587
|
+
const updatedIds = existing.reflection_ids.includes(reflection.id)
|
|
588
|
+
? existing.reflection_ids
|
|
589
|
+
: [...existing.reflection_ids, reflection.id];
|
|
590
|
+
const updatedAuthors = [...new Set([...existing.authors, reflection.author])];
|
|
591
|
+
const updatedEvidence = [...new Set([...existing.evidence_refs, ...reflection.evidence])];
|
|
592
|
+
const allReflections = loadReflectionsById(updatedIds);
|
|
593
|
+
const score = computeScore(allReflections);
|
|
594
|
+
const priority = scoreToPriority(score);
|
|
595
|
+
const sevMax = maxSeverity(allReflections);
|
|
596
|
+
// v1.1: track reopen count in metadata for cap enforcement
|
|
597
|
+
const meta = (existing.metadata ?? {});
|
|
598
|
+
const windowStart = meta.reopen_window_start ?? 0;
|
|
599
|
+
const prevCount = meta.reopen_count_24h ?? 0;
|
|
600
|
+
const windowExpired = now - windowStart > COOLDOWN_MS;
|
|
601
|
+
const newCount = windowExpired ? 1 : prevCount + 1;
|
|
602
|
+
const newWindowStart = windowExpired ? now : windowStart;
|
|
603
|
+
const updatedMetadata = { ...meta, reopen_count_24h: newCount, reopen_window_start: newWindowStart };
|
|
604
|
+
db.prepare(`
|
|
605
|
+
UPDATE insights SET
|
|
606
|
+
status = 'promoted', reflection_ids = ?, independent_count = ?,
|
|
607
|
+
evidence_refs = ?, authors = ?, score = ?, priority = ?,
|
|
608
|
+
promotion_readiness = 'promoted', recurring_candidate = 1,
|
|
609
|
+
cooldown_until = ?, cooldown_reason = 'reopened',
|
|
610
|
+
severity_max = ?, metadata = ?, updated_at = ?
|
|
611
|
+
WHERE id = ?
|
|
612
|
+
`).run(safeJsonStringify(updatedIds), updatedAuthors.length, safeJsonStringify(updatedEvidence), safeJsonStringify(updatedAuthors), score, priority, now + COOLDOWN_MS, sevMax, safeJsonStringify(updatedMetadata), now, existing.id);
|
|
613
|
+
eventBus.emit({
|
|
614
|
+
id: `evt-insight-reopened-${existing.id}`,
|
|
615
|
+
type: 'task_updated',
|
|
616
|
+
timestamp: Date.now(),
|
|
617
|
+
data: { kind: 'insight:reopened', insightId: existing.id, reopenCount: newCount },
|
|
618
|
+
});
|
|
619
|
+
return {
|
|
620
|
+
...existing,
|
|
621
|
+
status: 'promoted',
|
|
622
|
+
reflection_ids: updatedIds,
|
|
623
|
+
independent_count: updatedAuthors.length,
|
|
624
|
+
evidence_refs: updatedEvidence,
|
|
625
|
+
authors: updatedAuthors,
|
|
626
|
+
score,
|
|
627
|
+
priority,
|
|
628
|
+
promotion_readiness: 'promoted',
|
|
629
|
+
recurring_candidate: true,
|
|
630
|
+
cooldown_until: now + COOLDOWN_MS,
|
|
631
|
+
cooldown_reason: 'reopened',
|
|
632
|
+
severity_max: sevMax,
|
|
633
|
+
metadata: updatedMetadata,
|
|
634
|
+
updated_at: now,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
// ── Cooldown management ──
|
|
638
|
+
/**
|
|
639
|
+
* Tick cooldowns: move promoted past window → cooldown, expired cooldowns → closed.
|
|
640
|
+
*/
|
|
641
|
+
export function tickCooldowns() {
|
|
642
|
+
const db = getDb();
|
|
643
|
+
const now = Date.now();
|
|
644
|
+
const cooled = db.prepare(`
|
|
645
|
+
UPDATE insights SET status = 'cooldown', cooldown_reason = 'auto-cooldown', updated_at = ?
|
|
646
|
+
WHERE status = 'promoted' AND cooldown_until IS NOT NULL AND cooldown_until <= ?
|
|
647
|
+
`).run(now, now);
|
|
648
|
+
const closed = db.prepare(`
|
|
649
|
+
UPDATE insights SET status = 'closed', updated_at = ?
|
|
650
|
+
WHERE status = 'cooldown' AND updated_at < ?
|
|
651
|
+
`).run(now, now - COOLDOWN_MS);
|
|
652
|
+
return { cooled: cooled.changes, closed: closed.changes };
|
|
653
|
+
}
|
|
654
|
+
// ── CRUD ──
|
|
655
|
+
export function getInsight(id) {
|
|
656
|
+
const db = getDb();
|
|
657
|
+
const row = db.prepare('SELECT * FROM insights WHERE id = ?').get(id);
|
|
658
|
+
return row ? rowToInsight(row) : null;
|
|
659
|
+
}
|
|
660
|
+
export function listInsights(opts = {}) {
|
|
661
|
+
const db = getDb();
|
|
662
|
+
const where = [];
|
|
663
|
+
const params = [];
|
|
664
|
+
if (opts.status && opts.status !== 'all') {
|
|
665
|
+
where.push('status = ?');
|
|
666
|
+
params.push(opts.status);
|
|
667
|
+
}
|
|
668
|
+
if (opts.priority) {
|
|
669
|
+
where.push('priority = ?');
|
|
670
|
+
params.push(opts.priority);
|
|
671
|
+
}
|
|
672
|
+
if (opts.workflow_stage) {
|
|
673
|
+
where.push('workflow_stage = ?');
|
|
674
|
+
params.push(opts.workflow_stage);
|
|
675
|
+
}
|
|
676
|
+
if (opts.failure_family) {
|
|
677
|
+
where.push('failure_family = ?');
|
|
678
|
+
params.push(opts.failure_family);
|
|
679
|
+
}
|
|
680
|
+
if (opts.impacted_unit) {
|
|
681
|
+
where.push('impacted_unit = ?');
|
|
682
|
+
params.push(opts.impacted_unit);
|
|
683
|
+
}
|
|
684
|
+
const whereClause = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
|
|
685
|
+
const limit = Math.min(opts.limit ?? 50, 200);
|
|
686
|
+
const offset = opts.offset ?? 0;
|
|
687
|
+
const total = db.prepare(`SELECT COUNT(*) as c FROM insights ${whereClause}`).get(...params).c;
|
|
688
|
+
const rows = db.prepare(`SELECT * FROM insights ${whereClause} ORDER BY score DESC, created_at DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
|
|
689
|
+
return { insights: rows.map(rowToInsight), total };
|
|
690
|
+
}
|
|
691
|
+
export function insightStats() {
|
|
692
|
+
const db = getDb();
|
|
693
|
+
const total = db.prepare('SELECT COUNT(*) as c FROM insights').get().c;
|
|
694
|
+
const byStatus = db.prepare('SELECT status, COUNT(*) as c FROM insights GROUP BY status').all();
|
|
695
|
+
const byPriority = db.prepare('SELECT priority, COUNT(*) as c FROM insights GROUP BY priority').all();
|
|
696
|
+
const byFamily = db.prepare('SELECT failure_family, COUNT(*) as c FROM insights GROUP BY failure_family ORDER BY c DESC LIMIT 20').all();
|
|
697
|
+
return {
|
|
698
|
+
total,
|
|
699
|
+
by_status: Object.fromEntries(byStatus.map(r => [r.status, r.c])),
|
|
700
|
+
by_priority: Object.fromEntries(byPriority.map(r => [r.priority, r.c])),
|
|
701
|
+
by_failure_family: Object.fromEntries(byFamily.map(r => [r.failure_family, r.c])),
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
// ── Internal helpers ──
|
|
705
|
+
function loadReflectionsById(ids) {
|
|
706
|
+
if (ids.length === 0)
|
|
707
|
+
return [];
|
|
708
|
+
const db = getDb();
|
|
709
|
+
const placeholders = ids.map(() => '?').join(',');
|
|
710
|
+
const rows = db.prepare(`SELECT * FROM reflections WHERE id IN (${placeholders})`).all(...ids);
|
|
711
|
+
return rows.map(row => ({
|
|
712
|
+
id: row.id,
|
|
713
|
+
pain: row.pain,
|
|
714
|
+
impact: row.impact,
|
|
715
|
+
evidence: safeJsonParse(row.evidence) ?? [],
|
|
716
|
+
went_well: row.went_well,
|
|
717
|
+
suspected_why: row.suspected_why,
|
|
718
|
+
proposed_fix: row.proposed_fix,
|
|
719
|
+
confidence: row.confidence,
|
|
720
|
+
role_type: row.role_type,
|
|
721
|
+
severity: row.severity ?? undefined,
|
|
722
|
+
author: row.author,
|
|
723
|
+
task_id: row.task_id ?? undefined,
|
|
724
|
+
tags: safeJsonParse(row.tags),
|
|
725
|
+
team_id: row.team_id ?? undefined,
|
|
726
|
+
metadata: safeJsonParse(row.metadata),
|
|
727
|
+
created_at: row.created_at,
|
|
728
|
+
updated_at: row.updated_at,
|
|
729
|
+
}));
|
|
730
|
+
}
|
|
731
|
+
// ── Test helpers ──
|
|
732
|
+
export function _clearInsightStore() {
|
|
733
|
+
try {
|
|
734
|
+
const db = getDb();
|
|
735
|
+
db.prepare('DELETE FROM insights').run();
|
|
736
|
+
}
|
|
737
|
+
catch {
|
|
738
|
+
// Table may not exist
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Update an insight's status and optionally link a task.
|
|
743
|
+
* Used by the insight→task bridge when auto-creating or triaging.
|
|
744
|
+
*/
|
|
745
|
+
export function updateInsightStatus(insightId, status, taskId) {
|
|
746
|
+
const db = getDb();
|
|
747
|
+
const now = Date.now();
|
|
748
|
+
if (taskId) {
|
|
749
|
+
const result = db.prepare('UPDATE insights SET status = ?, task_id = ?, updated_at = ? WHERE id = ?').run(status, taskId, now, insightId);
|
|
750
|
+
return result.changes > 0;
|
|
751
|
+
}
|
|
752
|
+
else {
|
|
753
|
+
const result = db.prepare('UPDATE insights SET status = ?, updated_at = ? WHERE id = ?').run(status, now, insightId);
|
|
754
|
+
return result.changes > 0;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
export { COOLDOWN_MS, PROMOTION_THRESHOLD, SCORING_ENGINE_VERSION as _SCORING_ENGINE_VERSION };
|
|
758
|
+
/**
|
|
759
|
+
* Returns top insight signals from the reflection loop, ranked by score.
|
|
760
|
+
* Each entry includes linked task details and evidence status.
|
|
761
|
+
*
|
|
762
|
+
* "Addressed" means the insight has a linked task in done/validating status,
|
|
763
|
+
* or the insight itself is in cooldown/closed status.
|
|
764
|
+
*/
|
|
765
|
+
export function getLoopSummary(opts = {}) {
|
|
766
|
+
const db = getDb();
|
|
767
|
+
const limit = Math.min(opts.limit ?? 20, 100);
|
|
768
|
+
const minScore = opts.min_score ?? 0;
|
|
769
|
+
const excludeAddressed = opts.exclude_addressed ?? false;
|
|
770
|
+
const where = [];
|
|
771
|
+
const params = [];
|
|
772
|
+
if (minScore > 0) {
|
|
773
|
+
where.push('score >= ?');
|
|
774
|
+
params.push(minScore);
|
|
775
|
+
}
|
|
776
|
+
if (excludeAddressed) {
|
|
777
|
+
where.push("status NOT IN ('cooldown', 'closed')");
|
|
778
|
+
}
|
|
779
|
+
const whereClause = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
|
|
780
|
+
const total = db.prepare(`SELECT COUNT(*) as c FROM insights ${whereClause}`).get(...params).c;
|
|
781
|
+
const rows = db.prepare(`SELECT * FROM insights ${whereClause} ORDER BY score DESC, updated_at DESC LIMIT ?`).all(...params, limit);
|
|
782
|
+
// Resolve linked tasks directly from DB (avoids circular import with tasks.ts)
|
|
783
|
+
const taskStmt = db.prepare('SELECT id, title, status, assignee FROM tasks WHERE id = ?');
|
|
784
|
+
const entries = rows.map(row => {
|
|
785
|
+
const insight = rowToInsight(row);
|
|
786
|
+
// Resolve linked task
|
|
787
|
+
let linkedTask = null;
|
|
788
|
+
if (insight.task_id) {
|
|
789
|
+
const taskRow = taskStmt.get(insight.task_id);
|
|
790
|
+
if (taskRow) {
|
|
791
|
+
linkedTask = {
|
|
792
|
+
id: taskRow.id,
|
|
793
|
+
title: taskRow.title,
|
|
794
|
+
status: taskRow.status,
|
|
795
|
+
assignee: taskRow.assignee || null,
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
// Addressed: insight closed/cooldown, or linked task is done/validating
|
|
800
|
+
const addressed = insight.status === 'cooldown' || insight.status === 'closed'
|
|
801
|
+
|| (linkedTask !== null && (linkedTask.status === 'done' || linkedTask.status === 'validating'));
|
|
802
|
+
return {
|
|
803
|
+
insight_id: insight.id,
|
|
804
|
+
title: insight.title,
|
|
805
|
+
score: insight.score,
|
|
806
|
+
priority: insight.priority,
|
|
807
|
+
status: insight.status,
|
|
808
|
+
workflow_stage: insight.workflow_stage,
|
|
809
|
+
failure_family: insight.failure_family,
|
|
810
|
+
impacted_unit: insight.impacted_unit,
|
|
811
|
+
independent_count: insight.independent_count,
|
|
812
|
+
authors: insight.authors,
|
|
813
|
+
evidence_count: insight.evidence_refs.length,
|
|
814
|
+
evidence_refs: insight.evidence_refs,
|
|
815
|
+
linked_task: linkedTask,
|
|
816
|
+
addressed,
|
|
817
|
+
created_at: insight.created_at,
|
|
818
|
+
updated_at: insight.updated_at,
|
|
819
|
+
};
|
|
820
|
+
});
|
|
821
|
+
// Post-filter addressed entries if requested (can't fully do in SQL because
|
|
822
|
+
// "addressed" depends on linked task status which is in a different table)
|
|
823
|
+
const filtered = excludeAddressed ? entries.filter(e => !e.addressed) : entries;
|
|
824
|
+
return {
|
|
825
|
+
entries: filtered,
|
|
826
|
+
total: excludeAddressed ? filtered.length : total,
|
|
827
|
+
filters: { limit, min_score: minScore, exclude_addressed: excludeAddressed },
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
// ── Reconciler: find promoted insights without task links ──
|
|
831
|
+
export function getOrphanedInsights() {
|
|
832
|
+
const db = getDb();
|
|
833
|
+
const rows = db.prepare("SELECT * FROM insights WHERE status IN ('promoted', 'task_created') AND (task_id IS NULL OR task_id = '') ORDER BY score DESC").all();
|
|
834
|
+
return rows.map(rowToInsight);
|
|
835
|
+
}
|
|
836
|
+
export function reconcileInsightTaskLinks(createTaskFn, dryRun = false) {
|
|
837
|
+
const orphans = getOrphanedInsights();
|
|
838
|
+
const result = {
|
|
839
|
+
scanned: orphans.length,
|
|
840
|
+
linked: 0,
|
|
841
|
+
created: 0,
|
|
842
|
+
skipped: 0,
|
|
843
|
+
errors: [],
|
|
844
|
+
details: [],
|
|
845
|
+
};
|
|
846
|
+
for (const insight of orphans) {
|
|
847
|
+
try {
|
|
848
|
+
if (dryRun) {
|
|
849
|
+
result.details.push({ insight_id: insight.id, action: 'would_create', reason: 'dry run' });
|
|
850
|
+
result.created++;
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
853
|
+
const taskResult = createTaskFn(insight);
|
|
854
|
+
if (taskResult) {
|
|
855
|
+
updateInsightStatus(insight.id, 'task_created', taskResult.taskId);
|
|
856
|
+
result.created++;
|
|
857
|
+
result.details.push({ insight_id: insight.id, action: 'created', task_id: taskResult.taskId });
|
|
858
|
+
}
|
|
859
|
+
else {
|
|
860
|
+
result.skipped++;
|
|
861
|
+
result.details.push({ insight_id: insight.id, action: 'skipped', reason: 'createTaskFn returned null' });
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
catch (err) {
|
|
865
|
+
result.errors.push(`${insight.id}: ${err.message}`);
|
|
866
|
+
result.details.push({ insight_id: insight.id, action: 'error', reason: err.message });
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return result;
|
|
870
|
+
}
|
|
871
|
+
//# sourceMappingURL=insights.js.map
|