forge-openclaw-plugin 0.2.25 → 0.2.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -3
- package/dist/assets/{board-VmF4FAfr.js → board-C6jCchjI.js} +3 -3
- package/dist/assets/{board-VmF4FAfr.js.map → board-C6jCchjI.js.map} +1 -1
- package/dist/assets/index-DVvS8iiU.css +1 -0
- package/dist/assets/index-zYB-9Dfo.js +85 -0
- package/dist/assets/index-zYB-9Dfo.js.map +1 -0
- package/dist/assets/knowledge-graph-layout.worker-DRvzPxhP.js +2 -0
- package/dist/assets/knowledge-graph-layout.worker-DRvzPxhP.js.map +1 -0
- package/dist/assets/{motion-DvkU14p-.js → motion-DFHrH2rd.js} +2 -2
- package/dist/assets/{motion-DvkU14p-.js.map → motion-DFHrH2rd.js.map} +1 -1
- package/dist/assets/{table-DgiPof9E.js → table-ZL7Di_u3.js} +2 -2
- package/dist/assets/{table-DgiPof9E.js.map → table-ZL7Di_u3.js.map} +1 -1
- package/dist/assets/{ui-nYfoC0Gq.js → ui-CKNPpz7q.js} +2 -2
- package/dist/assets/{ui-nYfoC0Gq.js.map → ui-CKNPpz7q.js.map} +1 -1
- package/dist/assets/vendor-DoNZuFhn.js +1247 -0
- package/dist/assets/vendor-DoNZuFhn.js.map +1 -0
- package/dist/index.html +7 -8
- package/dist/openclaw/local-runtime.d.ts +3 -1
- package/dist/openclaw/local-runtime.js +67 -15
- package/dist/openclaw/plugin-entry-shared.js +24 -2
- package/dist/openclaw/plugin-sdk-types.d.ts +17 -0
- package/dist/openclaw/routes.d.ts +27 -0
- package/dist/openclaw/routes.js +16 -12
- package/dist/openclaw/tools.js +0 -3
- package/dist/server/server/migrations/001_core.sql +411 -0
- package/dist/server/server/migrations/002_psyche.sql +392 -0
- package/dist/server/server/migrations/003_habits.sql +30 -0
- package/dist/server/server/migrations/004_habit_links.sql +8 -0
- package/dist/server/server/migrations/005_habit_psyche_links.sql +24 -0
- package/dist/server/server/migrations/006_work_adjustments.sql +14 -0
- package/dist/server/server/migrations/007_weekly_review_closures.sql +17 -0
- package/dist/server/server/migrations/008_calendar_execution.sql +147 -0
- package/dist/server/server/migrations/009_true_calendar_events.sql +195 -0
- package/dist/server/server/migrations/010_calendar_selection_state.sql +6 -0
- package/dist/server/server/migrations/011_calendar_timezone_backfill.sql +11 -0
- package/dist/server/server/migrations/012_work_block_ranges.sql +7 -0
- package/dist/server/server/migrations/013_microsoft_local_auth_settings.sql +8 -0
- package/dist/server/server/migrations/014_note_tags_and_ephemeral.sql +8 -0
- package/dist/server/server/migrations/015_multi_user_and_strategies.sql +244 -0
- package/dist/server/server/migrations/016_health_companion.sql +158 -0
- package/dist/server/server/migrations/016_strategy_contracts_and_user_graph.sql +22 -0
- package/dist/server/server/migrations/017_preferences.sql +131 -0
- package/dist/server/server/migrations/018_preference_catalogs.sql +31 -0
- package/dist/server/server/migrations/019_wiki_memory.sql +255 -0
- package/dist/server/server/migrations/020_wiki_page_hierarchy.sql +11 -0
- package/dist/server/server/migrations/021_hide_evidence_from_wiki_index.sql +3 -0
- package/dist/server/server/migrations/022_wiki_ingest_background.sql +85 -0
- package/dist/server/server/migrations/023_diagnostic_logs.sql +28 -0
- package/dist/server/server/migrations/024_questionnaires.sql +96 -0
- package/dist/server/server/migrations/025_ai_model_connections.sql +26 -0
- package/dist/server/server/migrations/026_custom_theme_settings.sql +2 -0
- package/dist/server/server/migrations/027_ai_processors.sql +31 -0
- package/dist/server/server/migrations/028_movement_domain.sql +136 -0
- package/dist/server/server/migrations/029_watch_micro_capture.sql +23 -0
- package/dist/server/server/migrations/030_surface_layouts.sql +5 -0
- package/dist/server/server/migrations/031_ai_processor_runtime_upgrades.sql +10 -0
- package/dist/server/server/migrations/032_ai_connectors.sql +44 -0
- package/dist/server/server/migrations/033_movement_trip_point_sync.sql +36 -0
- package/dist/server/server/migrations/034_movement_segment_sync.sql +49 -0
- package/dist/server/server/migrations/035_google_local_auth_settings.sql +2 -0
- package/dist/server/server/migrations/036_google_local_auth_client_secret.sql +2 -0
- package/dist/server/server/migrations/037_workbench_public_inputs_and_run_inputs.sql +5 -0
- package/dist/server/server/migrations/038_data_management_settings.sql +11 -0
- package/dist/server/server/migrations/039_life_force_and_action_points.sql +114 -0
- package/dist/server/server/migrations/040_screen_time_domain.sql +89 -0
- package/dist/server/server/migrations/041_companion_source_states.sql +21 -0
- package/dist/server/server/migrations/042_movement_boxes.sql +47 -0
- package/dist/server/server/migrations/043_movement_box_overlap_overrides.sql +26 -0
- package/dist/server/{app.js → server/src/app.js} +2112 -414
- package/dist/server/server/src/connectors/box-registry.js +223 -0
- package/dist/server/server/src/data-management-types.js +107 -0
- package/dist/server/{db.js → server/src/db.js} +72 -4
- package/dist/server/server/src/debug.js +19 -0
- package/dist/server/{demo-data.js → server/src/demo-data.js} +2 -2
- package/dist/server/{health.js → server/src/health.js} +702 -18
- package/dist/server/{managers → server/src/managers}/platform/llm-manager.js +7 -4
- package/dist/server/server/src/managers/platform/mock-workbench-provider.js +149 -0
- package/dist/server/{managers → server/src/managers}/platform/secrets-manager.js +18 -1
- package/dist/server/{managers → server/src/managers}/runtime.js +9 -0
- package/dist/server/{movement.js → server/src/movement.js} +1971 -112
- package/dist/server/{openapi.js → server/src/openapi.js} +491 -3
- package/dist/server/{psyche-types.js → server/src/psyche-types.js} +9 -1
- package/dist/server/{repositories → server/src/repositories}/activity-events.js +8 -0
- package/dist/server/{repositories → server/src/repositories}/ai-connectors.js +758 -47
- package/dist/server/{repositories → server/src/repositories}/calendar.js +1 -1
- package/dist/server/{repositories → server/src/repositories}/habits.js +37 -1
- package/dist/server/{repositories → server/src/repositories}/model-settings.js +13 -3
- package/dist/server/{repositories → server/src/repositories}/notes.js +3 -0
- package/dist/server/{repositories → server/src/repositories}/settings.js +431 -21
- package/dist/server/{repositories → server/src/repositories}/tasks.js +170 -10
- package/dist/server/server/src/runtime-data-root.js +82 -0
- package/dist/server/server/src/screen-time.js +802 -0
- package/dist/server/{services → server/src/services}/calendar-runtime.js +775 -58
- package/dist/server/server/src/services/data-management.js +788 -0
- package/dist/server/{services → server/src/services}/entity-crud.js +205 -2
- package/dist/server/server/src/services/google-calendar-oauth-config.js +176 -0
- package/dist/server/server/src/services/knowledge-graph.js +1455 -0
- package/dist/server/server/src/services/life-force-model.js +197 -0
- package/dist/server/server/src/services/life-force.js +1270 -0
- package/dist/server/server/src/services/psyche-observation-calendar.js +413 -0
- package/dist/server/{types.js → server/src/types.js} +420 -29
- package/dist/server/server/src/web.js +332 -0
- package/dist/server/src/components/customization/utility-widgets.js +439 -0
- package/dist/server/src/components/ui/info-tooltip.js +25 -0
- package/dist/server/src/components/workbench-boxes/calendar/calendar-boxes.js +78 -0
- package/dist/server/src/components/workbench-boxes/goals/goals-boxes.js +62 -0
- package/dist/server/src/components/workbench-boxes/habits/habits-boxes.js +62 -0
- package/dist/server/src/components/workbench-boxes/health/health-boxes.js +147 -0
- package/dist/server/src/components/workbench-boxes/insights/insights-boxes.js +50 -0
- package/dist/server/src/components/workbench-boxes/kanban/kanban-boxes.js +136 -0
- package/dist/server/src/components/workbench-boxes/movement/movement-boxes.js +47 -0
- package/dist/server/src/components/workbench-boxes/notes/notes-boxes.js +132 -0
- package/dist/server/src/components/workbench-boxes/overview/overview-boxes.js +65 -0
- package/dist/server/src/components/workbench-boxes/preferences/preferences-boxes.js +78 -0
- package/dist/server/src/components/workbench-boxes/projects/projects-boxes.js +62 -0
- package/dist/server/src/components/workbench-boxes/psyche/psyche-boxes.js +88 -0
- package/dist/server/src/components/workbench-boxes/questionnaires/questionnaires-boxes.js +61 -0
- package/dist/server/src/components/workbench-boxes/review/review-boxes.js +53 -0
- package/dist/server/src/components/workbench-boxes/shared/define-workbench-box.js +6 -0
- package/dist/server/src/components/workbench-boxes/shared/generic-node-view.js +49 -0
- package/dist/server/src/components/workbench-boxes/strategies/strategies-boxes.js +62 -0
- package/dist/server/src/components/workbench-boxes/tasks/tasks-boxes.js +76 -0
- package/dist/server/src/components/workbench-boxes/today/today-boxes.js +78 -0
- package/dist/server/src/components/workbench-boxes/wiki/wiki-boxes.js +60 -0
- package/dist/server/src/lib/api-error.js +37 -0
- package/dist/server/src/lib/api.js +2118 -0
- package/dist/server/src/lib/calendar-name-deduper.js +144 -0
- package/dist/server/src/lib/data-management-types.js +1 -0
- package/dist/server/src/lib/diagnostics.js +67 -0
- package/dist/server/src/lib/entity-visuals.js +279 -0
- package/dist/server/src/lib/knowledge-graph-types.js +276 -0
- package/dist/server/src/lib/knowledge-graph.js +470 -0
- package/dist/server/src/lib/psyche-types.js +1 -0
- package/dist/server/src/lib/questionnaire-types.js +1 -0
- package/dist/server/src/lib/runtime-paths.js +24 -0
- package/dist/server/src/lib/schemas.js +238 -0
- package/dist/server/src/lib/snapshot-normalizer.js +416 -0
- package/dist/server/src/lib/theme-system.js +319 -0
- package/dist/server/src/lib/types.js +1 -0
- package/dist/server/src/lib/utils.js +22 -0
- package/dist/server/src/lib/workbench/boxes.js +16 -0
- package/dist/server/src/lib/workbench/contracts.js +229 -0
- package/dist/server/src/lib/workbench/nodes.js +215 -0
- package/dist/server/src/lib/workbench/registry.js +120 -0
- package/dist/server/src/lib/workbench/runtime.js +397 -0
- package/dist/server/src/lib/workbench/tool-catalog.js +68 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server/index.js +68 -0
- package/server/migrations/035_google_local_auth_settings.sql +2 -0
- package/server/migrations/036_google_local_auth_client_secret.sql +2 -0
- package/server/migrations/037_workbench_public_inputs_and_run_inputs.sql +5 -0
- package/server/migrations/038_data_management_settings.sql +11 -0
- package/server/migrations/039_life_force_and_action_points.sql +114 -0
- package/server/migrations/040_screen_time_domain.sql +89 -0
- package/server/migrations/041_companion_source_states.sql +21 -0
- package/server/migrations/042_movement_boxes.sql +47 -0
- package/server/migrations/043_movement_box_overlap_overrides.sql +26 -0
- package/skills/forge-openclaw/SKILL.md +27 -11
- package/skills/forge-openclaw/entity_conversation_playbooks.md +411 -46
- package/skills/forge-openclaw/psyche_entity_playbooks.md +195 -20
- package/dist/assets/index-CFCKDIMH.js +0 -67
- package/dist/assets/index-CFCKDIMH.js.map +0 -1
- package/dist/assets/index-ZPY6U1TU.css +0 -1
- package/dist/assets/vendor-D9PTEPSB.js +0 -824
- package/dist/assets/vendor-D9PTEPSB.js.map +0 -1
- package/dist/assets/viz-Cqb6s--o.js +0 -34
- package/dist/assets/viz-Cqb6s--o.js.map +0 -1
- package/dist/server/connectors/box-registry.js +0 -257
- package/dist/server/services/psyche-observation-calendar.js +0 -46
- package/dist/server/web.js +0 -98
- /package/dist/server/{discovery-advertiser.js → server/src/discovery-advertiser.js} +0 -0
- /package/dist/server/{e2e-server.js → server/src/e2e-server.js} +0 -0
- /package/dist/server/{errors.js → server/src/errors.js} +0 -0
- /package/dist/server/{index.js → server/src/index.js} +0 -0
- /package/dist/server/{managers → server/src/managers}/base.js +0 -0
- /package/dist/server/{managers → server/src/managers}/contracts.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/api-gateway-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/audit-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/authentication-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/authorization-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/background-job-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/configuration-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/database-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/event-bus-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/external-service-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/health-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/migration-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/openai-responses-provider.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/search-index-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/session-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/storage-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/token-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/transaction-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/trusted-network.js +0 -0
- /package/dist/server/{managers → server/src/managers}/type-guards.js +0 -0
- /package/dist/server/{preferences-seeds.js → server/src/preferences-seeds.js} +0 -0
- /package/dist/server/{preferences-types.js → server/src/preferences-types.js} +0 -0
- /package/dist/server/{questionnaire-flow.js → server/src/questionnaire-flow.js} +0 -0
- /package/dist/server/{questionnaire-seeds.js → server/src/questionnaire-seeds.js} +0 -0
- /package/dist/server/{questionnaire-types.js → server/src/questionnaire-types.js} +0 -0
- /package/dist/server/{repositories → server/src/repositories}/ai-processors.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/collaboration.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/deleted-entities.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/diagnostic-logs.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/domains.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/entity-ownership.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/event-log.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/goals.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/preferences.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/projects.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/psyche.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/questionnaires.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/rewards.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/strategies.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/surface-layouts.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/tags.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/task-runs.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/users.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/weekly-reviews.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/wiki-memory.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/work-adjustments.js +0 -0
- /package/dist/server/{seed-demo.js → server/src/seed-demo.js} +0 -0
- /package/dist/server/{services → server/src/services}/context.js +0 -0
- /package/dist/server/{services → server/src/services}/dashboard.js +0 -0
- /package/dist/server/{services → server/src/services}/gamification.js +0 -0
- /package/dist/server/{services → server/src/services}/insights.js +0 -0
- /package/dist/server/{services → server/src/services}/openai-codex-oauth.js +0 -0
- /package/dist/server/{services → server/src/services}/projects.js +0 -0
- /package/dist/server/{services → server/src/services}/psyche.js +0 -0
- /package/dist/server/{services → server/src/services}/relations.js +0 -0
- /package/dist/server/{services → server/src/services}/reviews.js +0 -0
- /package/dist/server/{services → server/src/services}/run-recovery.js +0 -0
- /package/dist/server/{services → server/src/services}/tagging.js +0 -0
- /package/dist/server/{services → server/src/services}/task-run-watchdog.js +0 -0
- /package/dist/server/{services → server/src/services}/work-time.js +0 -0
- /package/dist/server/{watch-mobile.js → server/src/watch-mobile.js} +0 -0
|
@@ -0,0 +1,1270 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { getDatabase, runInTransaction } from "../db.js";
|
|
3
|
+
import { getDefaultUser, getUserById } from "../repositories/users.js";
|
|
4
|
+
import { actionProfileSchema, fatigueSignalCreateSchema, lifeForcePayloadSchema, lifeForceProfilePatchSchema, lifeForceTemplateUpdateSchema } from "../types.js";
|
|
5
|
+
import { DEFAULT_TASK_TOTAL_AP, LIFE_FORCE_BASELINE_DAILY_AP, buildDefaultTaskActionProfile, buildTaskActionPointSummary, buildTaskSplitSuggestion, clamp, interpolateCurveRate, normalizeCurveToBudget, resolveBandTotalCostAp, resolveTaskExpectedDurationSeconds } from "./life-force-model.js";
|
|
6
|
+
import { computeWorkTime } from "./work-time.js";
|
|
7
|
+
const DEFAULT_TEMPLATE_POINTS = [
|
|
8
|
+
{ minuteOfDay: 0, rateApPerHour: 0 },
|
|
9
|
+
{ minuteOfDay: 7 * 60, rateApPerHour: 0 },
|
|
10
|
+
{ minuteOfDay: 8 * 60, rateApPerHour: 8 },
|
|
11
|
+
{ minuteOfDay: 10 * 60, rateApPerHour: 13 },
|
|
12
|
+
{ minuteOfDay: 13 * 60, rateApPerHour: 9 },
|
|
13
|
+
{ minuteOfDay: 14 * 60, rateApPerHour: 11 },
|
|
14
|
+
{ minuteOfDay: 19 * 60, rateApPerHour: 7 },
|
|
15
|
+
{ minuteOfDay: 23 * 60, rateApPerHour: 0 },
|
|
16
|
+
{ minuteOfDay: 24 * 60, rateApPerHour: 0 }
|
|
17
|
+
];
|
|
18
|
+
const LIFE_FORCE_STAT_LABELS = {
|
|
19
|
+
life_force: "Life Force",
|
|
20
|
+
activation: "Activation",
|
|
21
|
+
focus: "Focus",
|
|
22
|
+
vigor: "Vigor",
|
|
23
|
+
composure: "Composure",
|
|
24
|
+
flow: "Flow"
|
|
25
|
+
};
|
|
26
|
+
function nowIso() {
|
|
27
|
+
return new Date().toISOString();
|
|
28
|
+
}
|
|
29
|
+
function toDateKey(date) {
|
|
30
|
+
return date.toISOString().slice(0, 10);
|
|
31
|
+
}
|
|
32
|
+
function startOfUtcDay(date) {
|
|
33
|
+
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
|
|
34
|
+
}
|
|
35
|
+
function buildDayRange(date) {
|
|
36
|
+
const start = startOfUtcDay(date);
|
|
37
|
+
const end = new Date(start);
|
|
38
|
+
end.setUTCDate(end.getUTCDate() + 1);
|
|
39
|
+
return {
|
|
40
|
+
startMs: start.getTime(),
|
|
41
|
+
endMs: end.getTime(),
|
|
42
|
+
dateKey: toDateKey(start),
|
|
43
|
+
from: start.toISOString(),
|
|
44
|
+
to: end.toISOString()
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function parseCurvePoints(raw) {
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(raw);
|
|
50
|
+
if (!Array.isArray(parsed)) {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
return parsed
|
|
54
|
+
.filter((value) => !!value &&
|
|
55
|
+
typeof value === "object" &&
|
|
56
|
+
typeof value.minuteOfDay === "number" &&
|
|
57
|
+
typeof value.rateApPerHour === "number")
|
|
58
|
+
.map((point) => ({
|
|
59
|
+
minuteOfDay: Math.round(point.minuteOfDay),
|
|
60
|
+
rateApPerHour: point.rateApPerHour,
|
|
61
|
+
locked: point.locked
|
|
62
|
+
}))
|
|
63
|
+
.sort((left, right) => left.minuteOfDay - right.minuteOfDay);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function defaultTemplatePoints() {
|
|
70
|
+
return normalizeCurveToBudget(DEFAULT_TEMPLATE_POINTS, LIFE_FORCE_BASELINE_DAILY_AP);
|
|
71
|
+
}
|
|
72
|
+
function seededActionProfiles() {
|
|
73
|
+
const now = nowIso();
|
|
74
|
+
const taskDefault = buildDefaultTaskActionProfile({});
|
|
75
|
+
return [
|
|
76
|
+
taskDefault,
|
|
77
|
+
actionProfileSchema.parse({
|
|
78
|
+
id: "profile_note_quick",
|
|
79
|
+
profileKey: "note_quick",
|
|
80
|
+
title: "Quick note",
|
|
81
|
+
entityType: "note",
|
|
82
|
+
mode: "impulse",
|
|
83
|
+
startupAp: 1,
|
|
84
|
+
totalCostAp: 1,
|
|
85
|
+
expectedDurationSeconds: null,
|
|
86
|
+
sustainRateApPerHour: 0,
|
|
87
|
+
demandWeights: {
|
|
88
|
+
activation: 0.15,
|
|
89
|
+
focus: 0.5,
|
|
90
|
+
vigor: 0,
|
|
91
|
+
composure: 0,
|
|
92
|
+
flow: 0.35
|
|
93
|
+
},
|
|
94
|
+
doubleCountPolicy: "primary_only",
|
|
95
|
+
sourceMethod: "seeded",
|
|
96
|
+
costBand: "tiny",
|
|
97
|
+
recoveryEffect: 0,
|
|
98
|
+
metadata: {},
|
|
99
|
+
createdAt: now,
|
|
100
|
+
updatedAt: now
|
|
101
|
+
}),
|
|
102
|
+
actionProfileSchema.parse({
|
|
103
|
+
id: "profile_habit_default",
|
|
104
|
+
profileKey: "habit_default",
|
|
105
|
+
title: "Habit check-in",
|
|
106
|
+
entityType: "habit",
|
|
107
|
+
mode: "impulse",
|
|
108
|
+
startupAp: 3,
|
|
109
|
+
totalCostAp: 3,
|
|
110
|
+
expectedDurationSeconds: null,
|
|
111
|
+
sustainRateApPerHour: 0,
|
|
112
|
+
demandWeights: {
|
|
113
|
+
activation: 0.4,
|
|
114
|
+
focus: 0.1,
|
|
115
|
+
vigor: 0.15,
|
|
116
|
+
composure: 0.05,
|
|
117
|
+
flow: 0.3
|
|
118
|
+
},
|
|
119
|
+
doubleCountPolicy: "primary_only",
|
|
120
|
+
sourceMethod: "seeded",
|
|
121
|
+
costBand: "light",
|
|
122
|
+
recoveryEffect: 0,
|
|
123
|
+
metadata: {},
|
|
124
|
+
createdAt: now,
|
|
125
|
+
updatedAt: now
|
|
126
|
+
}),
|
|
127
|
+
actionProfileSchema.parse({
|
|
128
|
+
id: "profile_workout_default",
|
|
129
|
+
profileKey: "workout_default",
|
|
130
|
+
title: "Workout",
|
|
131
|
+
entityType: "workout_session",
|
|
132
|
+
mode: "rate",
|
|
133
|
+
startupAp: 1,
|
|
134
|
+
totalCostAp: 24,
|
|
135
|
+
expectedDurationSeconds: 3_600,
|
|
136
|
+
sustainRateApPerHour: 24,
|
|
137
|
+
demandWeights: {
|
|
138
|
+
activation: 0.1,
|
|
139
|
+
focus: 0.05,
|
|
140
|
+
vigor: 0.75,
|
|
141
|
+
composure: 0,
|
|
142
|
+
flow: 0.1
|
|
143
|
+
},
|
|
144
|
+
doubleCountPolicy: "primary_only",
|
|
145
|
+
sourceMethod: "seeded",
|
|
146
|
+
costBand: "standard",
|
|
147
|
+
recoveryEffect: 0,
|
|
148
|
+
metadata: {},
|
|
149
|
+
createdAt: now,
|
|
150
|
+
updatedAt: now
|
|
151
|
+
}),
|
|
152
|
+
actionProfileSchema.parse({
|
|
153
|
+
id: "profile_calendar_default",
|
|
154
|
+
profileKey: "calendar_event_default",
|
|
155
|
+
title: "Calendar event",
|
|
156
|
+
entityType: "calendar_event",
|
|
157
|
+
mode: "container",
|
|
158
|
+
startupAp: 0,
|
|
159
|
+
totalCostAp: 12,
|
|
160
|
+
expectedDurationSeconds: 3_600,
|
|
161
|
+
sustainRateApPerHour: 12,
|
|
162
|
+
demandWeights: {
|
|
163
|
+
activation: 0.05,
|
|
164
|
+
focus: 0.35,
|
|
165
|
+
vigor: 0.05,
|
|
166
|
+
composure: 0.35,
|
|
167
|
+
flow: 0.2
|
|
168
|
+
},
|
|
169
|
+
doubleCountPolicy: "container_only",
|
|
170
|
+
sourceMethod: "seeded",
|
|
171
|
+
costBand: "light",
|
|
172
|
+
recoveryEffect: 0,
|
|
173
|
+
metadata: {},
|
|
174
|
+
createdAt: now,
|
|
175
|
+
updatedAt: now
|
|
176
|
+
}),
|
|
177
|
+
actionProfileSchema.parse({
|
|
178
|
+
id: "profile_recovery_break",
|
|
179
|
+
profileKey: "recovery_break",
|
|
180
|
+
title: "Recovery break",
|
|
181
|
+
entityType: "system",
|
|
182
|
+
mode: "recovery",
|
|
183
|
+
startupAp: 0,
|
|
184
|
+
totalCostAp: 0,
|
|
185
|
+
expectedDurationSeconds: null,
|
|
186
|
+
sustainRateApPerHour: 0,
|
|
187
|
+
demandWeights: {
|
|
188
|
+
activation: 0,
|
|
189
|
+
focus: 0,
|
|
190
|
+
vigor: 0,
|
|
191
|
+
composure: 0,
|
|
192
|
+
flow: 0
|
|
193
|
+
},
|
|
194
|
+
doubleCountPolicy: "primary_only",
|
|
195
|
+
sourceMethod: "seeded",
|
|
196
|
+
costBand: "tiny",
|
|
197
|
+
recoveryEffect: 6,
|
|
198
|
+
metadata: {},
|
|
199
|
+
createdAt: now,
|
|
200
|
+
updatedAt: now
|
|
201
|
+
})
|
|
202
|
+
];
|
|
203
|
+
}
|
|
204
|
+
function mapTemplateProfileRow(row) {
|
|
205
|
+
return actionProfileSchema.parse({
|
|
206
|
+
id: row.id,
|
|
207
|
+
profileKey: row.profile_key,
|
|
208
|
+
entityType: row.entity_type,
|
|
209
|
+
title: row.title,
|
|
210
|
+
createdAt: row.created_at,
|
|
211
|
+
updatedAt: row.updated_at,
|
|
212
|
+
...JSON.parse(row.profile_json)
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
function mapEntityProfileRow(row, fallback) {
|
|
216
|
+
return actionProfileSchema.parse({
|
|
217
|
+
id: row.id,
|
|
218
|
+
profileKey: fallback.profileKey,
|
|
219
|
+
title: fallback.title,
|
|
220
|
+
entityType: fallback.entityType,
|
|
221
|
+
createdAt: row.created_at,
|
|
222
|
+
updatedAt: row.updated_at,
|
|
223
|
+
...JSON.parse(row.profile_json)
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
function ensureActionProfileTemplates() {
|
|
227
|
+
const database = getDatabase();
|
|
228
|
+
const insert = database.prepare(`INSERT OR IGNORE INTO action_profile_templates (
|
|
229
|
+
id,
|
|
230
|
+
profile_key,
|
|
231
|
+
entity_type,
|
|
232
|
+
title,
|
|
233
|
+
profile_json,
|
|
234
|
+
created_at,
|
|
235
|
+
updated_at
|
|
236
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)`);
|
|
237
|
+
for (const profile of seededActionProfiles()) {
|
|
238
|
+
insert.run(profile.id, profile.profileKey, profile.entityType, profile.title, JSON.stringify({
|
|
239
|
+
mode: profile.mode,
|
|
240
|
+
startupAp: profile.startupAp,
|
|
241
|
+
totalCostAp: profile.totalCostAp,
|
|
242
|
+
expectedDurationSeconds: profile.expectedDurationSeconds,
|
|
243
|
+
sustainRateApPerHour: profile.sustainRateApPerHour,
|
|
244
|
+
demandWeights: profile.demandWeights,
|
|
245
|
+
doubleCountPolicy: profile.doubleCountPolicy,
|
|
246
|
+
sourceMethod: profile.sourceMethod,
|
|
247
|
+
costBand: profile.costBand,
|
|
248
|
+
recoveryEffect: profile.recoveryEffect,
|
|
249
|
+
metadata: profile.metadata
|
|
250
|
+
}), profile.createdAt, profile.updatedAt);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
export function upsertTaskActionProfile(input) {
|
|
254
|
+
const now = nowIso();
|
|
255
|
+
const profile = buildDefaultTaskActionProfile({
|
|
256
|
+
id: `profile_task_${input.taskId}`,
|
|
257
|
+
profileKey: `task_${input.taskId}`,
|
|
258
|
+
title: input.title || "Task",
|
|
259
|
+
expectedDurationSeconds: input.plannedDurationSeconds,
|
|
260
|
+
totalCostAp: input.totalCostAp ?? resolveBandTotalCostAp(input.actionCostBand ?? "standard"),
|
|
261
|
+
costBand: input.actionCostBand ?? "standard",
|
|
262
|
+
sourceMethod: "manual"
|
|
263
|
+
});
|
|
264
|
+
getDatabase()
|
|
265
|
+
.prepare(`INSERT INTO entity_action_profiles (
|
|
266
|
+
id,
|
|
267
|
+
entity_type,
|
|
268
|
+
entity_id,
|
|
269
|
+
profile_json,
|
|
270
|
+
created_at,
|
|
271
|
+
updated_at
|
|
272
|
+
) VALUES (?, 'task', ?, ?, ?, ?)
|
|
273
|
+
ON CONFLICT(entity_type, entity_id) DO UPDATE SET
|
|
274
|
+
profile_json = excluded.profile_json,
|
|
275
|
+
updated_at = excluded.updated_at`)
|
|
276
|
+
.run(profile.id, input.taskId, JSON.stringify({
|
|
277
|
+
profileKey: profile.profileKey,
|
|
278
|
+
title: profile.title,
|
|
279
|
+
entityType: profile.entityType,
|
|
280
|
+
mode: profile.mode,
|
|
281
|
+
startupAp: profile.startupAp,
|
|
282
|
+
totalCostAp: profile.totalCostAp,
|
|
283
|
+
expectedDurationSeconds: profile.expectedDurationSeconds,
|
|
284
|
+
sustainRateApPerHour: profile.sustainRateApPerHour,
|
|
285
|
+
demandWeights: profile.demandWeights,
|
|
286
|
+
doubleCountPolicy: profile.doubleCountPolicy,
|
|
287
|
+
sourceMethod: profile.sourceMethod,
|
|
288
|
+
costBand: profile.costBand,
|
|
289
|
+
recoveryEffect: profile.recoveryEffect,
|
|
290
|
+
metadata: profile.metadata
|
|
291
|
+
}), now, now);
|
|
292
|
+
}
|
|
293
|
+
function ensureLifeForceProfile(userId) {
|
|
294
|
+
const database = getDatabase();
|
|
295
|
+
const existing = database
|
|
296
|
+
.prepare(`SELECT *
|
|
297
|
+
FROM life_force_profiles
|
|
298
|
+
WHERE user_id = ?`)
|
|
299
|
+
.get(userId);
|
|
300
|
+
if (existing) {
|
|
301
|
+
return existing;
|
|
302
|
+
}
|
|
303
|
+
const now = nowIso();
|
|
304
|
+
database
|
|
305
|
+
.prepare(`INSERT INTO life_force_profiles (
|
|
306
|
+
user_id,
|
|
307
|
+
base_daily_ap,
|
|
308
|
+
readiness_multiplier,
|
|
309
|
+
life_force_level,
|
|
310
|
+
activation_level,
|
|
311
|
+
focus_level,
|
|
312
|
+
vigor_level,
|
|
313
|
+
composure_level,
|
|
314
|
+
flow_level,
|
|
315
|
+
created_at,
|
|
316
|
+
updated_at
|
|
317
|
+
) VALUES (?, 200, 1.0, 1, 1, 1, 1, 1, 1, ?, ?)`)
|
|
318
|
+
.run(userId, now, now);
|
|
319
|
+
return database
|
|
320
|
+
.prepare(`SELECT *
|
|
321
|
+
FROM life_force_profiles
|
|
322
|
+
WHERE user_id = ?`)
|
|
323
|
+
.get(userId);
|
|
324
|
+
}
|
|
325
|
+
function ensureWeekdayTemplates(userId) {
|
|
326
|
+
const database = getDatabase();
|
|
327
|
+
const insert = database.prepare(`INSERT OR IGNORE INTO life_force_weekday_templates (
|
|
328
|
+
id,
|
|
329
|
+
user_id,
|
|
330
|
+
weekday,
|
|
331
|
+
baseline_daily_ap,
|
|
332
|
+
points_json,
|
|
333
|
+
created_at,
|
|
334
|
+
updated_at
|
|
335
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)`);
|
|
336
|
+
const now = nowIso();
|
|
337
|
+
const defaultPoints = JSON.stringify(defaultTemplatePoints());
|
|
338
|
+
for (let weekday = 0; weekday < 7; weekday += 1) {
|
|
339
|
+
insert.run(`lf_template_${userId}_${weekday}`, userId, weekday, LIFE_FORCE_BASELINE_DAILY_AP, defaultPoints, now, now);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
function readWeekdayTemplate(userId, weekday) {
|
|
343
|
+
ensureWeekdayTemplates(userId);
|
|
344
|
+
return getDatabase()
|
|
345
|
+
.prepare(`SELECT *
|
|
346
|
+
FROM life_force_weekday_templates
|
|
347
|
+
WHERE user_id = ? AND weekday = ?`)
|
|
348
|
+
.get(userId, weekday);
|
|
349
|
+
}
|
|
350
|
+
function readTaskRunRows(range, userId) {
|
|
351
|
+
return getDatabase()
|
|
352
|
+
.prepare(`SELECT
|
|
353
|
+
task_runs.id,
|
|
354
|
+
task_runs.task_id,
|
|
355
|
+
task_runs.actor,
|
|
356
|
+
task_runs.status,
|
|
357
|
+
task_runs.is_current,
|
|
358
|
+
task_runs.claimed_at,
|
|
359
|
+
task_runs.heartbeat_at,
|
|
360
|
+
task_runs.lease_expires_at,
|
|
361
|
+
task_runs.completed_at,
|
|
362
|
+
task_runs.released_at,
|
|
363
|
+
task_runs.timed_out_at,
|
|
364
|
+
task_runs.updated_at,
|
|
365
|
+
tasks.title AS task_title,
|
|
366
|
+
task_runs.planned_duration_seconds,
|
|
367
|
+
tasks.planned_duration_seconds AS task_expected_duration_seconds
|
|
368
|
+
FROM task_runs
|
|
369
|
+
INNER JOIN tasks ON tasks.id = task_runs.task_id
|
|
370
|
+
INNER JOIN entity_owners
|
|
371
|
+
ON entity_owners.entity_type = 'task'
|
|
372
|
+
AND entity_owners.entity_id = tasks.id
|
|
373
|
+
AND entity_owners.role = 'owner'
|
|
374
|
+
WHERE entity_owners.user_id = ?
|
|
375
|
+
AND task_runs.claimed_at < ?
|
|
376
|
+
AND COALESCE(task_runs.completed_at, task_runs.released_at, task_runs.timed_out_at, task_runs.updated_at, task_runs.lease_expires_at, task_runs.heartbeat_at) >= ?`)
|
|
377
|
+
.all(userId, range.to, range.from);
|
|
378
|
+
}
|
|
379
|
+
function terminalRunMs(row, now) {
|
|
380
|
+
if (row.status === "active") {
|
|
381
|
+
return Math.max(Date.parse(row.claimed_at), Math.min(now.getTime(), Date.parse(row.lease_expires_at)));
|
|
382
|
+
}
|
|
383
|
+
const terminal = row.completed_at ??
|
|
384
|
+
row.released_at ??
|
|
385
|
+
row.timed_out_at ??
|
|
386
|
+
row.updated_at ??
|
|
387
|
+
row.lease_expires_at ??
|
|
388
|
+
row.heartbeat_at;
|
|
389
|
+
return Math.max(Date.parse(row.claimed_at), Date.parse(terminal));
|
|
390
|
+
}
|
|
391
|
+
function overlapSeconds(range, row, now) {
|
|
392
|
+
const start = Math.max(range.startMs, Date.parse(row.claimed_at));
|
|
393
|
+
const end = Math.min(range.endMs, terminalRunMs(row, now));
|
|
394
|
+
return Math.max(0, Math.floor((end - start) / 1000));
|
|
395
|
+
}
|
|
396
|
+
function readStatXpByKey(userId) {
|
|
397
|
+
const rows = getDatabase()
|
|
398
|
+
.prepare(`SELECT stat_key, COALESCE(SUM(delta_xp), 0) AS xp
|
|
399
|
+
FROM stat_xp_events
|
|
400
|
+
WHERE user_id = ?
|
|
401
|
+
GROUP BY stat_key`)
|
|
402
|
+
.all(userId);
|
|
403
|
+
return new Map(rows.map((row) => [row.stat_key, row.xp]));
|
|
404
|
+
}
|
|
405
|
+
function buildStats(profile, userId) {
|
|
406
|
+
const xpByKey = readStatXpByKey(userId);
|
|
407
|
+
const levelByKey = {
|
|
408
|
+
life_force: profile.life_force_level,
|
|
409
|
+
activation: profile.activation_level,
|
|
410
|
+
focus: profile.focus_level,
|
|
411
|
+
vigor: profile.vigor_level,
|
|
412
|
+
composure: profile.composure_level,
|
|
413
|
+
flow: profile.flow_level
|
|
414
|
+
};
|
|
415
|
+
return Object.keys(levelByKey).map((key) => {
|
|
416
|
+
const level = levelByKey[key];
|
|
417
|
+
return {
|
|
418
|
+
key,
|
|
419
|
+
label: LIFE_FORCE_STAT_LABELS[key],
|
|
420
|
+
level,
|
|
421
|
+
xp: xpByKey.get(key) ?? 0,
|
|
422
|
+
xpToNextLevel: level * 100,
|
|
423
|
+
costModifier: key === "life_force"
|
|
424
|
+
? 1 + level * 0.03
|
|
425
|
+
: Math.max(0.55, Number((1 - level * 0.02).toFixed(3)))
|
|
426
|
+
};
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
function computeLifeForceMultiplier(profile) {
|
|
430
|
+
return 1 + (profile.life_force_level - 1) * 0.03;
|
|
431
|
+
}
|
|
432
|
+
function computeSleepRecoveryMultiplier() {
|
|
433
|
+
return 1;
|
|
434
|
+
}
|
|
435
|
+
function computeFatigueDebtCarry(userId, date) {
|
|
436
|
+
const previous = new Date(date);
|
|
437
|
+
previous.setUTCDate(previous.getUTCDate() - 1);
|
|
438
|
+
const previousKey = toDateKey(previous);
|
|
439
|
+
const snapshot = getDatabase()
|
|
440
|
+
.prepare(`SELECT daily_budget_ap
|
|
441
|
+
FROM life_force_day_snapshots
|
|
442
|
+
WHERE user_id = ? AND date_key = ?`)
|
|
443
|
+
.get(userId, previousKey);
|
|
444
|
+
if (!snapshot) {
|
|
445
|
+
return 0;
|
|
446
|
+
}
|
|
447
|
+
const spent = getDatabase()
|
|
448
|
+
.prepare(`SELECT COALESCE(SUM(total_ap), 0) AS total_ap
|
|
449
|
+
FROM ap_ledger_events
|
|
450
|
+
WHERE user_id = ? AND date_key = ?`)
|
|
451
|
+
.get(userId, previousKey);
|
|
452
|
+
return Math.max(0, Number((spent.total_ap - snapshot.daily_budget_ap).toFixed(2)));
|
|
453
|
+
}
|
|
454
|
+
function getOrCreateDaySnapshot(userId, date) {
|
|
455
|
+
const range = buildDayRange(date);
|
|
456
|
+
const existing = getDatabase()
|
|
457
|
+
.prepare(`SELECT *
|
|
458
|
+
FROM life_force_day_snapshots
|
|
459
|
+
WHERE user_id = ? AND date_key = ?`)
|
|
460
|
+
.get(userId, range.dateKey);
|
|
461
|
+
if (existing) {
|
|
462
|
+
return existing;
|
|
463
|
+
}
|
|
464
|
+
const profile = ensureLifeForceProfile(userId);
|
|
465
|
+
const template = readWeekdayTemplate(userId, date.getUTCDay());
|
|
466
|
+
const sleepRecoveryMultiplier = computeSleepRecoveryMultiplier();
|
|
467
|
+
const fatigueDebtCarry = computeFatigueDebtCarry(userId, date);
|
|
468
|
+
const readinessMultiplier = profile.readiness_multiplier;
|
|
469
|
+
const dailyBudgetAp = Math.max(40, Math.round(profile.base_daily_ap *
|
|
470
|
+
computeLifeForceMultiplier(profile) *
|
|
471
|
+
sleepRecoveryMultiplier *
|
|
472
|
+
readinessMultiplier) - fatigueDebtCarry);
|
|
473
|
+
const points = normalizeCurveToBudget(parseCurvePoints(template.points_json), dailyBudgetAp);
|
|
474
|
+
const now = nowIso();
|
|
475
|
+
const id = `lf_day_${userId}_${range.dateKey}`;
|
|
476
|
+
getDatabase()
|
|
477
|
+
.prepare(`INSERT INTO life_force_day_snapshots (
|
|
478
|
+
id,
|
|
479
|
+
user_id,
|
|
480
|
+
date_key,
|
|
481
|
+
daily_budget_ap,
|
|
482
|
+
sleep_recovery_multiplier,
|
|
483
|
+
readiness_multiplier,
|
|
484
|
+
fatigue_debt_carry,
|
|
485
|
+
points_json,
|
|
486
|
+
created_at,
|
|
487
|
+
updated_at
|
|
488
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
489
|
+
.run(id, userId, range.dateKey, dailyBudgetAp, sleepRecoveryMultiplier, readinessMultiplier, fatigueDebtCarry, JSON.stringify(points), now, now);
|
|
490
|
+
return getDatabase()
|
|
491
|
+
.prepare(`SELECT *
|
|
492
|
+
FROM life_force_day_snapshots
|
|
493
|
+
WHERE id = ?`)
|
|
494
|
+
.get(id);
|
|
495
|
+
}
|
|
496
|
+
function resolveTaskActionProfile(task) {
|
|
497
|
+
const row = getDatabase()
|
|
498
|
+
.prepare(`SELECT id, entity_type, entity_id, profile_json, created_at, updated_at
|
|
499
|
+
FROM entity_action_profiles
|
|
500
|
+
WHERE entity_type = 'task' AND entity_id = ?`)
|
|
501
|
+
.get(task.id);
|
|
502
|
+
if (!row) {
|
|
503
|
+
return buildDefaultTaskActionProfile({
|
|
504
|
+
id: `profile_task_${task.id}`,
|
|
505
|
+
expectedDurationSeconds: task.plannedDurationSeconds
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
const profile = mapEntityProfileRow(row, {
|
|
509
|
+
profileKey: `task_${task.id}`,
|
|
510
|
+
title: "Task",
|
|
511
|
+
entityType: "task"
|
|
512
|
+
});
|
|
513
|
+
return {
|
|
514
|
+
...profile,
|
|
515
|
+
expectedDurationSeconds: resolveTaskExpectedDurationSeconds(profile.expectedDurationSeconds ?? task.plannedDurationSeconds)
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
function readTodayAdjustmentRows(userId, range) {
|
|
519
|
+
return getDatabase()
|
|
520
|
+
.prepare(`SELECT
|
|
521
|
+
work_adjustments.id,
|
|
522
|
+
work_adjustments.entity_type,
|
|
523
|
+
work_adjustments.entity_id,
|
|
524
|
+
work_adjustments.applied_delta_minutes,
|
|
525
|
+
work_adjustments.note,
|
|
526
|
+
work_adjustments.created_at,
|
|
527
|
+
tasks.planned_duration_seconds
|
|
528
|
+
FROM work_adjustments
|
|
529
|
+
LEFT JOIN tasks
|
|
530
|
+
ON work_adjustments.entity_type = 'task'
|
|
531
|
+
AND tasks.id = work_adjustments.entity_id
|
|
532
|
+
INNER JOIN entity_owners
|
|
533
|
+
ON entity_owners.entity_type = work_adjustments.entity_type
|
|
534
|
+
AND entity_owners.entity_id = work_adjustments.entity_id
|
|
535
|
+
AND entity_owners.role = 'owner'
|
|
536
|
+
WHERE entity_owners.user_id = ?
|
|
537
|
+
AND work_adjustments.created_at >= ?
|
|
538
|
+
AND work_adjustments.created_at < ?`)
|
|
539
|
+
.all(userId, range.from, range.to);
|
|
540
|
+
}
|
|
541
|
+
function readTodayAdjustmentApByTaskId(userId, range) {
|
|
542
|
+
const rows = readTodayAdjustmentRows(userId, range);
|
|
543
|
+
const totals = new Map();
|
|
544
|
+
for (const row of rows) {
|
|
545
|
+
if (row.entity_type !== "task") {
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
const expectedDurationSeconds = resolveTaskExpectedDurationSeconds(row.planned_duration_seconds);
|
|
549
|
+
const deltaAp = (DEFAULT_TASK_TOTAL_AP / expectedDurationSeconds) *
|
|
550
|
+
row.applied_delta_minutes *
|
|
551
|
+
60;
|
|
552
|
+
totals.set(row.entity_id, (totals.get(row.entity_id) ?? 0) + deltaAp);
|
|
553
|
+
}
|
|
554
|
+
return totals;
|
|
555
|
+
}
|
|
556
|
+
function readTodayAdjustmentSecondsByTaskId(userId, range) {
|
|
557
|
+
const rows = readTodayAdjustmentRows(userId, range);
|
|
558
|
+
const totals = new Map();
|
|
559
|
+
for (const row of rows) {
|
|
560
|
+
if (row.entity_type !== "task") {
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
totals.set(row.entity_id, (totals.get(row.entity_id) ?? 0) + row.applied_delta_minutes * 60);
|
|
564
|
+
}
|
|
565
|
+
return totals;
|
|
566
|
+
}
|
|
567
|
+
function readActiveTaskRunProjectionRows(taskId) {
|
|
568
|
+
return getDatabase()
|
|
569
|
+
.prepare(`SELECT
|
|
570
|
+
id,
|
|
571
|
+
task_id,
|
|
572
|
+
timer_mode,
|
|
573
|
+
planned_duration_seconds,
|
|
574
|
+
claimed_at,
|
|
575
|
+
lease_expires_at,
|
|
576
|
+
status
|
|
577
|
+
FROM task_runs
|
|
578
|
+
WHERE task_id = ?
|
|
579
|
+
AND status = 'active'`)
|
|
580
|
+
.all(taskId);
|
|
581
|
+
}
|
|
582
|
+
function computeProjectedRemainingSeconds(row, now) {
|
|
583
|
+
if (row.timer_mode !== "planned" || row.planned_duration_seconds === null) {
|
|
584
|
+
return 0;
|
|
585
|
+
}
|
|
586
|
+
const endMs = Math.min(now.getTime(), Date.parse(row.lease_expires_at));
|
|
587
|
+
const elapsedWallSeconds = Math.max(0, Math.floor((endMs - Date.parse(row.claimed_at)) / 1000));
|
|
588
|
+
return Math.max(0, row.planned_duration_seconds - elapsedWallSeconds);
|
|
589
|
+
}
|
|
590
|
+
function buildTaskLifeForceRuntime(task, userId, now = new Date()) {
|
|
591
|
+
const range = buildDayRange(now);
|
|
592
|
+
const profile = resolveTaskActionProfile(task);
|
|
593
|
+
const todayRunSeconds = readTaskRunRows(range, userId)
|
|
594
|
+
.filter((row) => row.task_id === task.id)
|
|
595
|
+
.reduce((sum, row) => sum + overlapSeconds(range, row, now), 0);
|
|
596
|
+
const todayAdjustmentSeconds = readTodayAdjustmentSecondsByTaskId(userId, range).get(task.id) ?? 0;
|
|
597
|
+
const todayCreditedSeconds = todayRunSeconds + todayAdjustmentSeconds;
|
|
598
|
+
const spentTodayAp = (todayCreditedSeconds / 3600) * profile.sustainRateApPerHour;
|
|
599
|
+
const spentTotalAp = (task.time.totalCreditedSeconds / 3600) * profile.sustainRateApPerHour;
|
|
600
|
+
const projectedTotalSeconds = task.time.totalCreditedSeconds +
|
|
601
|
+
readActiveTaskRunProjectionRows(task.id).reduce((sum, row) => sum + computeProjectedRemainingSeconds(row, now), 0);
|
|
602
|
+
return {
|
|
603
|
+
taskId: task.id,
|
|
604
|
+
profile,
|
|
605
|
+
todayRunSeconds,
|
|
606
|
+
todayAdjustmentSeconds,
|
|
607
|
+
todayCreditedSeconds,
|
|
608
|
+
spentTodayAp,
|
|
609
|
+
spentTotalAp,
|
|
610
|
+
projectedTotalSeconds
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
function readTaskRunWindowsByTaskId(userId, range, now) {
|
|
614
|
+
const windows = new Map();
|
|
615
|
+
for (const row of readTaskRunRows(range, userId)) {
|
|
616
|
+
const startMs = Math.max(range.startMs, Date.parse(row.claimed_at));
|
|
617
|
+
const endMs = Math.min(range.endMs, terminalRunMs(row, now));
|
|
618
|
+
if (endMs <= startMs) {
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
const list = windows.get(row.task_id) ?? [];
|
|
622
|
+
list.push({ startMs, endMs });
|
|
623
|
+
windows.set(row.task_id, list);
|
|
624
|
+
}
|
|
625
|
+
return windows;
|
|
626
|
+
}
|
|
627
|
+
function buildWorkAdjustmentContributions(userId, range) {
|
|
628
|
+
return readTodayAdjustmentRows(userId, range)
|
|
629
|
+
.filter((row) => row.entity_type === "task")
|
|
630
|
+
.map((row) => {
|
|
631
|
+
const expectedDurationSeconds = resolveTaskExpectedDurationSeconds(row.planned_duration_seconds);
|
|
632
|
+
const totalAp = (DEFAULT_TASK_TOTAL_AP / expectedDurationSeconds) *
|
|
633
|
+
row.applied_delta_minutes *
|
|
634
|
+
60;
|
|
635
|
+
return {
|
|
636
|
+
entityType: "task",
|
|
637
|
+
entityId: row.entity_id,
|
|
638
|
+
eventKind: "work_adjustment",
|
|
639
|
+
sourceKind: "work_adjustment",
|
|
640
|
+
totalAp,
|
|
641
|
+
rateApPerHour: null,
|
|
642
|
+
title: row.note?.trim() || "Manual work adjustment",
|
|
643
|
+
why: "Manual time adjustments count toward today's Action Point spend.",
|
|
644
|
+
startsAt: row.created_at,
|
|
645
|
+
endsAt: row.created_at,
|
|
646
|
+
role: "background",
|
|
647
|
+
metadata: {
|
|
648
|
+
adjustmentId: row.id,
|
|
649
|
+
appliedDeltaMinutes: row.applied_delta_minutes
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
function buildTaskRunContributions(userId, range, now) {
|
|
655
|
+
const contributions = [];
|
|
656
|
+
const totalsByTaskId = new Map();
|
|
657
|
+
const activeDrains = [];
|
|
658
|
+
for (const row of readTaskRunRows(range, userId)) {
|
|
659
|
+
const seconds = overlapSeconds(range, row, now);
|
|
660
|
+
if (seconds <= 0) {
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
const profile = resolveTaskActionProfile({
|
|
664
|
+
id: row.task_id,
|
|
665
|
+
plannedDurationSeconds: row.task_expected_duration_seconds ?? row.planned_duration_seconds
|
|
666
|
+
});
|
|
667
|
+
const totalAp = (seconds / 3600) * profile.sustainRateApPerHour;
|
|
668
|
+
const startsAt = new Date(Math.max(range.startMs, Date.parse(row.claimed_at))).toISOString();
|
|
669
|
+
const endsAt = new Date(Math.min(range.endMs, terminalRunMs(row, now))).toISOString();
|
|
670
|
+
const contribution = {
|
|
671
|
+
entityType: "task",
|
|
672
|
+
entityId: row.task_id,
|
|
673
|
+
eventKind: "task_run",
|
|
674
|
+
sourceKind: "task_run",
|
|
675
|
+
totalAp,
|
|
676
|
+
rateApPerHour: profile.sustainRateApPerHour,
|
|
677
|
+
title: row.task_title,
|
|
678
|
+
why: "Active timed work consumes Action Points proportionally to actual time worked today.",
|
|
679
|
+
startsAt,
|
|
680
|
+
endsAt,
|
|
681
|
+
role: row.is_current === 1 ? "primary" : "secondary",
|
|
682
|
+
metadata: { taskRunId: row.id }
|
|
683
|
+
};
|
|
684
|
+
contributions.push(contribution);
|
|
685
|
+
const existing = totalsByTaskId.get(row.task_id) ?? { todayAp: 0, totalAp: 0 };
|
|
686
|
+
existing.todayAp += totalAp;
|
|
687
|
+
existing.totalAp += totalAp;
|
|
688
|
+
totalsByTaskId.set(row.task_id, existing);
|
|
689
|
+
if (row.status === "active" && Date.parse(row.lease_expires_at) > now.getTime()) {
|
|
690
|
+
activeDrains.push({
|
|
691
|
+
...contribution,
|
|
692
|
+
totalAp: 0
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return { contributions, totalsByTaskId, activeDrains };
|
|
697
|
+
}
|
|
698
|
+
function buildNoteContributions(userId, range, now) {
|
|
699
|
+
try {
|
|
700
|
+
const taskRunWindowsByTaskId = readTaskRunWindowsByTaskId(userId, range, now);
|
|
701
|
+
const rows = getDatabase()
|
|
702
|
+
.prepare(`SELECT
|
|
703
|
+
notes.id,
|
|
704
|
+
notes.title,
|
|
705
|
+
notes.created_at,
|
|
706
|
+
GROUP_CONCAT(
|
|
707
|
+
CASE
|
|
708
|
+
WHEN note_links.entity_type = 'task' THEN note_links.entity_id
|
|
709
|
+
ELSE NULL
|
|
710
|
+
END
|
|
711
|
+
) AS linked_task_ids
|
|
712
|
+
FROM notes
|
|
713
|
+
LEFT JOIN note_links ON note_links.note_id = notes.id
|
|
714
|
+
INNER JOIN entity_owners
|
|
715
|
+
ON entity_owners.entity_type = 'note'
|
|
716
|
+
AND entity_owners.entity_id = notes.id
|
|
717
|
+
AND entity_owners.role = 'owner'
|
|
718
|
+
WHERE entity_owners.user_id = ?
|
|
719
|
+
AND notes.created_at >= ?
|
|
720
|
+
AND notes.created_at < ?
|
|
721
|
+
GROUP BY notes.id, notes.title, notes.created_at`)
|
|
722
|
+
.all(userId, range.from, range.to);
|
|
723
|
+
return rows
|
|
724
|
+
.filter((row) => {
|
|
725
|
+
const createdAtMs = Date.parse(row.created_at);
|
|
726
|
+
const linkedTaskIds = (row.linked_task_ids ?? "")
|
|
727
|
+
.split(",")
|
|
728
|
+
.map((entry) => entry.trim())
|
|
729
|
+
.filter(Boolean);
|
|
730
|
+
return !linkedTaskIds.some((taskId) => (taskRunWindowsByTaskId.get(taskId) ?? []).some((window) => createdAtMs >= window.startMs && createdAtMs <= window.endMs));
|
|
731
|
+
})
|
|
732
|
+
.map((row) => ({
|
|
733
|
+
entityType: "note",
|
|
734
|
+
entityId: row.id,
|
|
735
|
+
eventKind: "note_created",
|
|
736
|
+
sourceKind: "note",
|
|
737
|
+
totalAp: 1,
|
|
738
|
+
rateApPerHour: null,
|
|
739
|
+
title: row.title || "Note",
|
|
740
|
+
why: "Standalone capture takes a small impulse of activation and focus.",
|
|
741
|
+
startsAt: row.created_at,
|
|
742
|
+
endsAt: row.created_at,
|
|
743
|
+
role: "background"
|
|
744
|
+
}));
|
|
745
|
+
}
|
|
746
|
+
catch {
|
|
747
|
+
return [];
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
function buildHabitContributions(userId, range) {
|
|
751
|
+
try {
|
|
752
|
+
const rows = getDatabase()
|
|
753
|
+
.prepare(`SELECT
|
|
754
|
+
habits.id,
|
|
755
|
+
habits.title,
|
|
756
|
+
habit_check_ins.created_at,
|
|
757
|
+
health_workout_sessions.id AS generated_workout_id
|
|
758
|
+
FROM habit_check_ins
|
|
759
|
+
INNER JOIN habits ON habits.id = habit_check_ins.habit_id
|
|
760
|
+
LEFT JOIN health_workout_sessions
|
|
761
|
+
ON health_workout_sessions.generated_from_check_in_id = habit_check_ins.id
|
|
762
|
+
INNER JOIN entity_owners
|
|
763
|
+
ON entity_owners.entity_type = 'habit'
|
|
764
|
+
AND entity_owners.entity_id = habits.id
|
|
765
|
+
AND entity_owners.role = 'owner'
|
|
766
|
+
WHERE entity_owners.user_id = ?
|
|
767
|
+
AND habit_check_ins.created_at >= ?
|
|
768
|
+
AND habit_check_ins.created_at < ?`)
|
|
769
|
+
.all(userId, range.from, range.to);
|
|
770
|
+
return rows
|
|
771
|
+
.filter((row) => row.generated_workout_id === null)
|
|
772
|
+
.map((row) => ({
|
|
773
|
+
entityType: "habit",
|
|
774
|
+
entityId: row.id,
|
|
775
|
+
eventKind: "habit_check_in",
|
|
776
|
+
sourceKind: "habit",
|
|
777
|
+
totalAp: 3,
|
|
778
|
+
rateApPerHour: null,
|
|
779
|
+
title: row.title,
|
|
780
|
+
why: "Habit execution still costs activation even when the action is short.",
|
|
781
|
+
startsAt: row.created_at,
|
|
782
|
+
endsAt: row.created_at,
|
|
783
|
+
role: "background"
|
|
784
|
+
}));
|
|
785
|
+
}
|
|
786
|
+
catch {
|
|
787
|
+
return [];
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
function buildWorkoutContributions(userId, range, now) {
|
|
791
|
+
let rows = [];
|
|
792
|
+
try {
|
|
793
|
+
rows = getDatabase()
|
|
794
|
+
.prepare(`SELECT id, workout_type, started_at, ended_at, duration_seconds, subjective_effort
|
|
795
|
+
FROM health_workout_sessions
|
|
796
|
+
WHERE user_id = ?
|
|
797
|
+
AND started_at < ?
|
|
798
|
+
AND ended_at >= ?`)
|
|
799
|
+
.all(userId, range.to, range.from);
|
|
800
|
+
}
|
|
801
|
+
catch {
|
|
802
|
+
rows = [];
|
|
803
|
+
}
|
|
804
|
+
const contributions = [];
|
|
805
|
+
const activeDrains = [];
|
|
806
|
+
for (const row of rows) {
|
|
807
|
+
const startMs = Math.max(range.startMs, Date.parse(row.started_at));
|
|
808
|
+
const endMs = Math.min(range.endMs, Date.parse(row.ended_at));
|
|
809
|
+
const seconds = Math.max(0, Math.floor((endMs - startMs) / 1000));
|
|
810
|
+
if (seconds <= 0) {
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
const effortMultiplier = row.subjective_effort
|
|
814
|
+
? clamp(row.subjective_effort / 6, 0.8, 1.6)
|
|
815
|
+
: 1;
|
|
816
|
+
const rateApPerHour = 24 * effortMultiplier;
|
|
817
|
+
const contribution = {
|
|
818
|
+
entityType: "workout_session",
|
|
819
|
+
entityId: row.id,
|
|
820
|
+
eventKind: "workout_session",
|
|
821
|
+
sourceKind: "workout",
|
|
822
|
+
totalAp: (seconds / 3600) * rateApPerHour,
|
|
823
|
+
rateApPerHour,
|
|
824
|
+
title: row.workout_type,
|
|
825
|
+
why: "Workout sessions consume real physical capacity and should affect current load.",
|
|
826
|
+
startsAt: new Date(startMs).toISOString(),
|
|
827
|
+
endsAt: new Date(endMs).toISOString(),
|
|
828
|
+
role: "secondary"
|
|
829
|
+
};
|
|
830
|
+
contributions.push(contribution);
|
|
831
|
+
if (Date.parse(row.ended_at) > now.getTime()) {
|
|
832
|
+
activeDrains.push({ ...contribution, totalAp: 0 });
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
return { contributions, activeDrains };
|
|
836
|
+
}
|
|
837
|
+
function buildCalendarDrains(userId, now) {
|
|
838
|
+
const nowIsoValue = now.toISOString();
|
|
839
|
+
try {
|
|
840
|
+
const rows = getDatabase()
|
|
841
|
+
.prepare(`SELECT calendar_events.id, calendar_events.title, calendar_events.start_at, calendar_events.end_at
|
|
842
|
+
FROM calendar_events
|
|
843
|
+
INNER JOIN calendar_event_links
|
|
844
|
+
ON calendar_event_links.forge_event_id = calendar_events.id
|
|
845
|
+
INNER JOIN entity_owners
|
|
846
|
+
ON entity_owners.entity_type = calendar_event_links.entity_type
|
|
847
|
+
AND entity_owners.entity_id = calendar_event_links.entity_id
|
|
848
|
+
AND entity_owners.role = 'owner'
|
|
849
|
+
WHERE entity_owners.user_id = ?
|
|
850
|
+
AND calendar_events.deleted_at IS NULL
|
|
851
|
+
AND calendar_events.start_at <= ?
|
|
852
|
+
AND calendar_events.end_at > ?
|
|
853
|
+
GROUP BY calendar_events.id, calendar_events.title, calendar_events.start_at, calendar_events.end_at`)
|
|
854
|
+
.all(userId, nowIsoValue, nowIsoValue);
|
|
855
|
+
return rows.map((row) => ({
|
|
856
|
+
entityType: "calendar_event",
|
|
857
|
+
entityId: row.id,
|
|
858
|
+
eventKind: "calendar_context",
|
|
859
|
+
sourceKind: "calendar",
|
|
860
|
+
totalAp: 0,
|
|
861
|
+
rateApPerHour: 12,
|
|
862
|
+
title: row.title,
|
|
863
|
+
why: "Calendar context occupies mental and social capacity even before task work is logged.",
|
|
864
|
+
startsAt: row.start_at,
|
|
865
|
+
endsAt: row.end_at,
|
|
866
|
+
role: "background"
|
|
867
|
+
}));
|
|
868
|
+
}
|
|
869
|
+
catch {
|
|
870
|
+
return [];
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
function syncApLedger(userId, range, contributions) {
|
|
874
|
+
runInTransaction(() => {
|
|
875
|
+
const database = getDatabase();
|
|
876
|
+
database
|
|
877
|
+
.prepare(`DELETE FROM ap_ledger_events
|
|
878
|
+
WHERE user_id = ? AND date_key = ?`)
|
|
879
|
+
.run(userId, range.dateKey);
|
|
880
|
+
const insert = database.prepare(`INSERT INTO ap_ledger_events (
|
|
881
|
+
id,
|
|
882
|
+
user_id,
|
|
883
|
+
date_key,
|
|
884
|
+
entity_type,
|
|
885
|
+
entity_id,
|
|
886
|
+
event_kind,
|
|
887
|
+
source_kind,
|
|
888
|
+
starts_at,
|
|
889
|
+
ends_at,
|
|
890
|
+
total_ap,
|
|
891
|
+
rate_ap_per_hour,
|
|
892
|
+
metadata_json,
|
|
893
|
+
created_at
|
|
894
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
895
|
+
const createdAt = nowIso();
|
|
896
|
+
for (const contribution of contributions) {
|
|
897
|
+
insert.run(`ap_${randomUUID().replaceAll("-", "").slice(0, 12)}`, userId, range.dateKey, contribution.entityType, contribution.entityId, contribution.eventKind, contribution.sourceKind, contribution.startsAt, contribution.endsAt, Number(contribution.totalAp.toFixed(4)), contribution.rateApPerHour, JSON.stringify({
|
|
898
|
+
title: contribution.title,
|
|
899
|
+
why: contribution.why,
|
|
900
|
+
role: contribution.role,
|
|
901
|
+
...(contribution.metadata ?? {})
|
|
902
|
+
}), createdAt);
|
|
903
|
+
}
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
function syncStatXpEvents(userId, dateKey, contributions) {
|
|
907
|
+
const totals = new Map();
|
|
908
|
+
for (const contribution of contributions) {
|
|
909
|
+
if (contribution.totalAp <= 0) {
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
const weights = contribution.profile?.demandWeights ?? {
|
|
913
|
+
activation: 0.2,
|
|
914
|
+
focus: 0.25,
|
|
915
|
+
vigor: 0.2,
|
|
916
|
+
composure: 0.15,
|
|
917
|
+
flow: 0.2
|
|
918
|
+
};
|
|
919
|
+
for (const [key, weight] of Object.entries(weights)) {
|
|
920
|
+
totals.set(key, Number(((totals.get(key) ?? 0) + contribution.totalAp * weight).toFixed(4)));
|
|
921
|
+
}
|
|
922
|
+
totals.set("life_force", Number(((totals.get("life_force") ?? 0) + contribution.totalAp * 0.35).toFixed(4)));
|
|
923
|
+
}
|
|
924
|
+
runInTransaction(() => {
|
|
925
|
+
const database = getDatabase();
|
|
926
|
+
database
|
|
927
|
+
.prepare(`DELETE FROM stat_xp_events
|
|
928
|
+
WHERE user_id = ?
|
|
929
|
+
AND json_extract(metadata_json, '$.dateKey') = ?`)
|
|
930
|
+
.run(userId, dateKey);
|
|
931
|
+
const insert = database.prepare(`INSERT INTO stat_xp_events (
|
|
932
|
+
id,
|
|
933
|
+
user_id,
|
|
934
|
+
stat_key,
|
|
935
|
+
delta_xp,
|
|
936
|
+
entity_type,
|
|
937
|
+
entity_id,
|
|
938
|
+
metadata_json,
|
|
939
|
+
created_at
|
|
940
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
941
|
+
const createdAt = nowIso();
|
|
942
|
+
for (const [statKey, deltaXp] of totals.entries()) {
|
|
943
|
+
insert.run(`statxp_${userId}_${dateKey}_${statKey}`, userId, statKey, deltaXp, "system", dateKey, JSON.stringify({
|
|
944
|
+
source: "life_force_daily_rollup",
|
|
945
|
+
dateKey
|
|
946
|
+
}), createdAt);
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
function readTodayLedger(userId, dateKey) {
|
|
951
|
+
return getDatabase()
|
|
952
|
+
.prepare(`SELECT *
|
|
953
|
+
FROM ap_ledger_events
|
|
954
|
+
WHERE user_id = ? AND date_key = ?
|
|
955
|
+
ORDER BY created_at ASC`)
|
|
956
|
+
.all(userId, dateKey);
|
|
957
|
+
}
|
|
958
|
+
function readTodayFatigueSignals(userId, dateKey) {
|
|
959
|
+
return getDatabase()
|
|
960
|
+
.prepare(`SELECT signal_type, delta
|
|
961
|
+
FROM fatigue_signals
|
|
962
|
+
WHERE user_id = ? AND date_key = ?`)
|
|
963
|
+
.all(userId, dateKey);
|
|
964
|
+
}
|
|
965
|
+
function buildWarnings(input) {
|
|
966
|
+
const warnings = [];
|
|
967
|
+
if (input.isOverloadedNow) {
|
|
968
|
+
warnings.push({
|
|
969
|
+
id: "lf_overload",
|
|
970
|
+
tone: "danger",
|
|
971
|
+
title: "You are overloaded right now",
|
|
972
|
+
detail: "Current concurrent work is draining more than the available instant capacity."
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
if (input.spentTodayAp > input.dailyBudgetAp) {
|
|
976
|
+
warnings.push({
|
|
977
|
+
id: "lf_overspent",
|
|
978
|
+
tone: "warning",
|
|
979
|
+
title: "Daily AP is in debt",
|
|
980
|
+
detail: "Today has already exceeded the calibrated Action Point budget."
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
if (input.topTaskIdsNeedingSplit.length > 0) {
|
|
984
|
+
warnings.push({
|
|
985
|
+
id: "lf_split",
|
|
986
|
+
tone: "info",
|
|
987
|
+
title: "A task wants to be split",
|
|
988
|
+
detail: "One or more tasks have grown beyond a healthy expected duration."
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
if (warnings.length === 0) {
|
|
992
|
+
warnings.push({
|
|
993
|
+
id: "lf_stable",
|
|
994
|
+
tone: "success",
|
|
995
|
+
title: "Life Force is stable",
|
|
996
|
+
detail: "Today is still inside a healthy capacity band."
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
return warnings;
|
|
1000
|
+
}
|
|
1001
|
+
export function resolveLifeForceUser(userIds) {
|
|
1002
|
+
if (userIds && userIds.length > 0) {
|
|
1003
|
+
return getUserById(userIds[0]) ?? getDefaultUser();
|
|
1004
|
+
}
|
|
1005
|
+
return getDefaultUser();
|
|
1006
|
+
}
|
|
1007
|
+
export function buildLifeForcePayload(now = new Date(), userIds) {
|
|
1008
|
+
ensureActionProfileTemplates();
|
|
1009
|
+
const user = resolveLifeForceUser(userIds);
|
|
1010
|
+
const profile = ensureLifeForceProfile(user.id);
|
|
1011
|
+
const snapshot = getOrCreateDaySnapshot(user.id, now);
|
|
1012
|
+
const range = buildDayRange(now);
|
|
1013
|
+
const taskRuns = buildTaskRunContributions(user.id, range, now);
|
|
1014
|
+
const notes = buildNoteContributions(user.id, range, now);
|
|
1015
|
+
const habits = buildHabitContributions(user.id, range);
|
|
1016
|
+
const workouts = buildWorkoutContributions(user.id, range, now);
|
|
1017
|
+
const adjustments = buildWorkAdjustmentContributions(user.id, range);
|
|
1018
|
+
const contributions = [
|
|
1019
|
+
...taskRuns.contributions,
|
|
1020
|
+
...adjustments,
|
|
1021
|
+
...notes,
|
|
1022
|
+
...habits,
|
|
1023
|
+
...workouts.contributions
|
|
1024
|
+
];
|
|
1025
|
+
const seededProfilesByKey = new Map(seededActionProfiles().map((entry) => [entry.profileKey, entry]));
|
|
1026
|
+
const taskDurationRows = getDatabase()
|
|
1027
|
+
.prepare(`SELECT id, planned_duration_seconds
|
|
1028
|
+
FROM tasks`)
|
|
1029
|
+
.all();
|
|
1030
|
+
const taskDurationById = new Map(taskDurationRows.map((row) => [row.id, row.planned_duration_seconds]));
|
|
1031
|
+
const profileLookup = new Map();
|
|
1032
|
+
for (const contribution of contributions) {
|
|
1033
|
+
if (contribution.entityType === "task") {
|
|
1034
|
+
profileLookup.set(`${contribution.entityType}:${contribution.entityId}`, resolveTaskActionProfile({
|
|
1035
|
+
id: contribution.entityId,
|
|
1036
|
+
plannedDurationSeconds: taskDurationById.get(contribution.entityId) ?? null
|
|
1037
|
+
}));
|
|
1038
|
+
continue;
|
|
1039
|
+
}
|
|
1040
|
+
if (contribution.entityType === "note") {
|
|
1041
|
+
profileLookup.set(`${contribution.entityType}:${contribution.entityId}`, seededProfilesByKey.get("note_quick") ?? null);
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
if (contribution.entityType === "habit") {
|
|
1045
|
+
profileLookup.set(`${contribution.entityType}:${contribution.entityId}`, seededProfilesByKey.get("habit_default") ?? null);
|
|
1046
|
+
continue;
|
|
1047
|
+
}
|
|
1048
|
+
if (contribution.entityType === "workout_session") {
|
|
1049
|
+
profileLookup.set(`${contribution.entityType}:${contribution.entityId}`, seededProfilesByKey.get("workout_default") ?? null);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
const adjustmentApByTaskId = readTodayAdjustmentApByTaskId(user.id, range);
|
|
1053
|
+
for (const [taskId, adjustmentAp] of adjustmentApByTaskId.entries()) {
|
|
1054
|
+
const existing = taskRuns.totalsByTaskId.get(taskId) ?? { todayAp: 0, totalAp: 0 };
|
|
1055
|
+
existing.todayAp += adjustmentAp;
|
|
1056
|
+
existing.totalAp += adjustmentAp;
|
|
1057
|
+
taskRuns.totalsByTaskId.set(taskId, existing);
|
|
1058
|
+
}
|
|
1059
|
+
syncApLedger(user.id, range, contributions);
|
|
1060
|
+
syncStatXpEvents(user.id, range.dateKey, contributions.map((contribution) => ({
|
|
1061
|
+
...contribution,
|
|
1062
|
+
profile: profileLookup.get(`${contribution.entityType}:${contribution.entityId}`) ?? null
|
|
1063
|
+
})));
|
|
1064
|
+
const ledger = readTodayLedger(user.id, range.dateKey);
|
|
1065
|
+
const spentTodayAp = ledger.reduce((sum, row) => sum + row.total_ap, 0);
|
|
1066
|
+
const currentCurve = parseCurvePoints(snapshot.points_json);
|
|
1067
|
+
const minuteOfDay = now.getUTCHours() * 60 + now.getUTCMinutes();
|
|
1068
|
+
const instantCapacityApPerHour = interpolateCurveRate(currentCurve, minuteOfDay);
|
|
1069
|
+
const activeDrains = [
|
|
1070
|
+
...taskRuns.activeDrains,
|
|
1071
|
+
...workouts.activeDrains,
|
|
1072
|
+
...(taskRuns.activeDrains.length === 0 && workouts.activeDrains.length === 0
|
|
1073
|
+
? buildCalendarDrains(user.id, now)
|
|
1074
|
+
: [])
|
|
1075
|
+
]
|
|
1076
|
+
.sort((left, right) => (right.rateApPerHour ?? 0) - (left.rateApPerHour ?? 0))
|
|
1077
|
+
.map((entry, index) => ({
|
|
1078
|
+
id: `${entry.sourceKind}:${entry.entityId}`,
|
|
1079
|
+
sourceType: entry.entityType,
|
|
1080
|
+
sourceId: entry.entityId,
|
|
1081
|
+
title: entry.title,
|
|
1082
|
+
role: index === 0 ? "primary" : entry.role,
|
|
1083
|
+
apPerHour: Number((entry.rateApPerHour ?? 0).toFixed(2)),
|
|
1084
|
+
instantAp: Number((entry.totalAp ?? 0).toFixed(2)),
|
|
1085
|
+
why: entry.why,
|
|
1086
|
+
startedAt: entry.startsAt,
|
|
1087
|
+
endsAt: entry.endsAt
|
|
1088
|
+
}));
|
|
1089
|
+
const sortedRates = activeDrains
|
|
1090
|
+
.map((entry) => entry.apPerHour)
|
|
1091
|
+
.sort((left, right) => right - left);
|
|
1092
|
+
const currentDrainApPerHour = (sortedRates[0] ?? 0) +
|
|
1093
|
+
(sortedRates[1] ?? 0) * 0.6 +
|
|
1094
|
+
(sortedRates[2] ?? 0) * 0.35 +
|
|
1095
|
+
sortedRates.slice(3).reduce((sum, value) => sum + value * 0.2, 0);
|
|
1096
|
+
const fatigueFromSignals = readTodayFatigueSignals(user.id, range.dateKey).reduce((sum, signal) => sum + signal.delta, 0);
|
|
1097
|
+
const fatigueBufferApPerHour = Math.max(0, Number((fatigueFromSignals + Math.max(0, activeDrains.length - 1) * 1.5).toFixed(2)));
|
|
1098
|
+
const rawInstantFreeApPerHour = Number((instantCapacityApPerHour - currentDrainApPerHour - fatigueBufferApPerHour).toFixed(2));
|
|
1099
|
+
const instantFreeApPerHour = Math.max(0, rawInstantFreeApPerHour);
|
|
1100
|
+
const overloadApPerHour = Math.max(0, Number((-rawInstantFreeApPerHour).toFixed(2)));
|
|
1101
|
+
const remainingAp = Number((snapshot.daily_budget_ap - spentTodayAp).toFixed(2));
|
|
1102
|
+
const forecastAp = Number((spentTodayAp + currentDrainApPerHour * 2).toFixed(2));
|
|
1103
|
+
const targetBandMinAp = Number((snapshot.daily_budget_ap * 0.85).toFixed(2));
|
|
1104
|
+
const targetBandMaxAp = Number(snapshot.daily_budget_ap.toFixed(2));
|
|
1105
|
+
const workTime = computeWorkTime(now);
|
|
1106
|
+
const topTaskIdsNeedingSplit = getDatabase()
|
|
1107
|
+
.prepare(`SELECT tasks.id, tasks.planned_duration_seconds
|
|
1108
|
+
FROM tasks
|
|
1109
|
+
INNER JOIN entity_owners
|
|
1110
|
+
ON entity_owners.entity_type = 'task'
|
|
1111
|
+
AND entity_owners.entity_id = tasks.id
|
|
1112
|
+
AND entity_owners.role = 'owner'
|
|
1113
|
+
WHERE entity_owners.user_id = ?`)
|
|
1114
|
+
.all(user.id)
|
|
1115
|
+
.map((row) => row)
|
|
1116
|
+
.filter((row) => {
|
|
1117
|
+
const time = workTime.taskSummaries.get(row.id);
|
|
1118
|
+
return buildTaskSplitSuggestion({
|
|
1119
|
+
plannedDurationSeconds: row.planned_duration_seconds,
|
|
1120
|
+
totalTrackedSeconds: time?.totalCreditedSeconds ?? 0,
|
|
1121
|
+
projectedTotalSeconds: (time?.totalCreditedSeconds ?? 0) +
|
|
1122
|
+
readActiveTaskRunProjectionRows(row.id).reduce((sum, activeRow) => sum + computeProjectedRemainingSeconds(activeRow, now), 0)
|
|
1123
|
+
}).shouldSplit;
|
|
1124
|
+
})
|
|
1125
|
+
.map((row) => row.id)
|
|
1126
|
+
.slice(0, 3);
|
|
1127
|
+
return lifeForcePayloadSchema.parse({
|
|
1128
|
+
userId: user.id,
|
|
1129
|
+
dateKey: range.dateKey,
|
|
1130
|
+
baselineDailyAp: profile.base_daily_ap,
|
|
1131
|
+
dailyBudgetAp: Number(snapshot.daily_budget_ap.toFixed(2)),
|
|
1132
|
+
spentTodayAp: Number(spentTodayAp.toFixed(2)),
|
|
1133
|
+
remainingAp,
|
|
1134
|
+
forecastAp,
|
|
1135
|
+
targetBandMinAp,
|
|
1136
|
+
targetBandMaxAp,
|
|
1137
|
+
instantCapacityApPerHour: Number(instantCapacityApPerHour.toFixed(2)),
|
|
1138
|
+
instantFreeApPerHour,
|
|
1139
|
+
overloadApPerHour,
|
|
1140
|
+
currentDrainApPerHour: Number(currentDrainApPerHour.toFixed(2)),
|
|
1141
|
+
fatigueBufferApPerHour,
|
|
1142
|
+
sleepRecoveryMultiplier: snapshot.sleep_recovery_multiplier,
|
|
1143
|
+
readinessMultiplier: snapshot.readiness_multiplier,
|
|
1144
|
+
fatigueDebtCarry: snapshot.fatigue_debt_carry,
|
|
1145
|
+
stats: buildStats(profile, user.id),
|
|
1146
|
+
currentCurve: currentCurve.map((point) => ({
|
|
1147
|
+
...point,
|
|
1148
|
+
locked: point.minuteOfDay <= minuteOfDay
|
|
1149
|
+
})),
|
|
1150
|
+
activeDrains,
|
|
1151
|
+
warnings: buildWarnings({
|
|
1152
|
+
spentTodayAp,
|
|
1153
|
+
dailyBudgetAp: snapshot.daily_budget_ap,
|
|
1154
|
+
isOverloadedNow: rawInstantFreeApPerHour < 0,
|
|
1155
|
+
topTaskIdsNeedingSplit
|
|
1156
|
+
}),
|
|
1157
|
+
recommendations: [
|
|
1158
|
+
instantFreeApPerHour <= 0
|
|
1159
|
+
? "Reduce overlap or take a recovery action before starting something new."
|
|
1160
|
+
: instantCapacityApPerHour > currentDrainApPerHour + 4
|
|
1161
|
+
? "This is a good moment for deep work."
|
|
1162
|
+
: "Favor lower-friction admin or recovery until headroom increases."
|
|
1163
|
+
],
|
|
1164
|
+
topTaskIdsNeedingSplit,
|
|
1165
|
+
updatedAt: now.toISOString()
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
export function buildTaskLifeForceFields(task, userId) {
|
|
1169
|
+
const effectiveUserId = userId ?? task.userId ?? getDefaultUser().id;
|
|
1170
|
+
const runtime = buildTaskLifeForceRuntime(task, effectiveUserId);
|
|
1171
|
+
return {
|
|
1172
|
+
actionPointSummary: buildTaskActionPointSummary({
|
|
1173
|
+
plannedDurationSeconds: task.plannedDurationSeconds,
|
|
1174
|
+
totalCostAp: runtime.profile.totalCostAp,
|
|
1175
|
+
spentTodayAp: runtime.spentTodayAp,
|
|
1176
|
+
spentTotalAp: runtime.spentTotalAp
|
|
1177
|
+
}),
|
|
1178
|
+
splitSuggestion: buildTaskSplitSuggestion({
|
|
1179
|
+
plannedDurationSeconds: task.plannedDurationSeconds,
|
|
1180
|
+
totalTrackedSeconds: task.time.totalCreditedSeconds,
|
|
1181
|
+
projectedTotalSeconds: runtime.projectedTotalSeconds
|
|
1182
|
+
})
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
export function getTaskCompletionRequirement(task, userId) {
|
|
1186
|
+
const effectiveUserId = userId ?? task.userId ?? getDefaultUser().id;
|
|
1187
|
+
const runtime = buildTaskLifeForceRuntime(task, effectiveUserId);
|
|
1188
|
+
return {
|
|
1189
|
+
todayCreditedSeconds: runtime.todayCreditedSeconds,
|
|
1190
|
+
requiresWorkLog: runtime.todayCreditedSeconds <= 0
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
export function updateLifeForceProfile(userId, patch) {
|
|
1194
|
+
const parsed = lifeForceProfilePatchSchema.parse(patch);
|
|
1195
|
+
const current = ensureLifeForceProfile(userId);
|
|
1196
|
+
const next = {
|
|
1197
|
+
base_daily_ap: parsed.baseDailyAp ?? current.base_daily_ap,
|
|
1198
|
+
readiness_multiplier: parsed.readinessMultiplier ?? current.readiness_multiplier,
|
|
1199
|
+
life_force_level: parsed.stats?.life_force ?? current.life_force_level,
|
|
1200
|
+
activation_level: parsed.stats?.activation ?? current.activation_level,
|
|
1201
|
+
focus_level: parsed.stats?.focus ?? current.focus_level,
|
|
1202
|
+
vigor_level: parsed.stats?.vigor ?? current.vigor_level,
|
|
1203
|
+
composure_level: parsed.stats?.composure ?? current.composure_level,
|
|
1204
|
+
flow_level: parsed.stats?.flow ?? current.flow_level
|
|
1205
|
+
};
|
|
1206
|
+
getDatabase()
|
|
1207
|
+
.prepare(`UPDATE life_force_profiles
|
|
1208
|
+
SET base_daily_ap = ?,
|
|
1209
|
+
readiness_multiplier = ?,
|
|
1210
|
+
life_force_level = ?,
|
|
1211
|
+
activation_level = ?,
|
|
1212
|
+
focus_level = ?,
|
|
1213
|
+
vigor_level = ?,
|
|
1214
|
+
composure_level = ?,
|
|
1215
|
+
flow_level = ?,
|
|
1216
|
+
updated_at = ?
|
|
1217
|
+
WHERE user_id = ?`)
|
|
1218
|
+
.run(next.base_daily_ap, next.readiness_multiplier, next.life_force_level, next.activation_level, next.focus_level, next.vigor_level, next.composure_level, next.flow_level, nowIso(), userId);
|
|
1219
|
+
const todayKey = toDateKey(new Date());
|
|
1220
|
+
getDatabase()
|
|
1221
|
+
.prepare(`DELETE FROM life_force_day_snapshots
|
|
1222
|
+
WHERE user_id = ? AND date_key = ?`)
|
|
1223
|
+
.run(userId, todayKey);
|
|
1224
|
+
return buildLifeForcePayload(new Date(), [userId]);
|
|
1225
|
+
}
|
|
1226
|
+
export function updateLifeForceTemplate(userId, weekday, input) {
|
|
1227
|
+
const parsed = lifeForceTemplateUpdateSchema.parse(input);
|
|
1228
|
+
ensureWeekdayTemplates(userId);
|
|
1229
|
+
const normalized = normalizeCurveToBudget([...parsed.points].sort((left, right) => left.minuteOfDay - right.minuteOfDay), LIFE_FORCE_BASELINE_DAILY_AP);
|
|
1230
|
+
getDatabase()
|
|
1231
|
+
.prepare(`UPDATE life_force_weekday_templates
|
|
1232
|
+
SET points_json = ?, updated_at = ?
|
|
1233
|
+
WHERE user_id = ? AND weekday = ?`)
|
|
1234
|
+
.run(JSON.stringify(normalized), nowIso(), userId, weekday);
|
|
1235
|
+
return normalized;
|
|
1236
|
+
}
|
|
1237
|
+
export function createFatigueSignal(userId, input) {
|
|
1238
|
+
const parsed = fatigueSignalCreateSchema.parse(input);
|
|
1239
|
+
const observedAt = parsed.observedAt ?? nowIso();
|
|
1240
|
+
const dateKey = observedAt.slice(0, 10);
|
|
1241
|
+
const delta = parsed.signalType === "tired" ? 4 : -4;
|
|
1242
|
+
getDatabase()
|
|
1243
|
+
.prepare(`INSERT INTO fatigue_signals (
|
|
1244
|
+
id,
|
|
1245
|
+
user_id,
|
|
1246
|
+
date_key,
|
|
1247
|
+
signal_type,
|
|
1248
|
+
observed_at,
|
|
1249
|
+
note,
|
|
1250
|
+
delta,
|
|
1251
|
+
created_at
|
|
1252
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1253
|
+
.run(`fatigue_${randomUUID().replaceAll("-", "").slice(0, 12)}`, userId, dateKey, parsed.signalType, observedAt, parsed.note ?? "", delta, nowIso());
|
|
1254
|
+
return buildLifeForcePayload(new Date(observedAt), [userId]);
|
|
1255
|
+
}
|
|
1256
|
+
export function listLifeForceTemplates(userId) {
|
|
1257
|
+
ensureWeekdayTemplates(userId);
|
|
1258
|
+
return getDatabase()
|
|
1259
|
+
.prepare(`SELECT *
|
|
1260
|
+
FROM life_force_weekday_templates
|
|
1261
|
+
WHERE user_id = ?
|
|
1262
|
+
ORDER BY weekday ASC`)
|
|
1263
|
+
.all(userId)
|
|
1264
|
+
.map((row) => row)
|
|
1265
|
+
.map((row) => ({
|
|
1266
|
+
weekday: row.weekday,
|
|
1267
|
+
baselineDailyAp: row.baseline_daily_ap,
|
|
1268
|
+
points: parseCurvePoints(row.points_json)
|
|
1269
|
+
}));
|
|
1270
|
+
}
|