forge-openclaw-plugin 0.2.19 → 0.2.21
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 +133 -2
- package/dist/assets/board-_C6oMy5w.js +6 -0
- package/dist/assets/{board-8L3uX7_O.js.map → board-_C6oMy5w.js.map} +1 -1
- package/dist/assets/index-B4A6TooJ.js +63 -0
- package/dist/assets/index-B4A6TooJ.js.map +1 -0
- package/dist/assets/index-D6Xs_2mo.css +1 -0
- package/dist/assets/{motion-1GAqqi8M.js → motion-D4sZgCHd.js} +2 -2
- package/dist/assets/{motion-1GAqqi8M.js.map → motion-D4sZgCHd.js.map} +1 -1
- package/dist/assets/{table-DBGlgRjk.js → table-BWzTaky1.js} +2 -2
- package/dist/assets/{table-DBGlgRjk.js.map → table-BWzTaky1.js.map} +1 -1
- package/dist/assets/{ui-iTluWjC4.js → ui-BzK4azQb.js} +7 -7
- package/dist/assets/{ui-iTluWjC4.js.map → ui-BzK4azQb.js.map} +1 -1
- package/dist/assets/vendor-DT3pnAKJ.css +1 -0
- package/dist/assets/vendor-De38P6YR.js +729 -0
- package/dist/assets/vendor-De38P6YR.js.map +1 -0
- package/dist/assets/viz-C6hfyqzu.js +34 -0
- package/dist/assets/viz-C6hfyqzu.js.map +1 -0
- package/dist/index.html +9 -9
- package/dist/openclaw/parity.d.ts +1 -1
- package/dist/openclaw/parity.js +29 -2
- package/dist/openclaw/routes.js +207 -24
- package/dist/openclaw/tools.js +324 -35
- package/dist/server/app.js +2080 -92
- package/dist/server/db.js +3 -0
- package/dist/server/health.js +1284 -0
- package/dist/server/managers/platform/background-job-manager.js +138 -2
- package/dist/server/managers/platform/llm-manager.js +126 -0
- package/dist/server/managers/platform/openai-responses-provider.js +773 -0
- package/dist/server/managers/runtime.js +6 -1
- package/dist/server/openapi.js +718 -0
- package/dist/server/preferences-seeds.js +409 -0
- package/dist/server/preferences-types.js +368 -0
- package/dist/server/psyche-types.js +42 -18
- package/dist/server/repositories/activity-events.js +53 -4
- package/dist/server/repositories/calendar.js +89 -15
- package/dist/server/repositories/collaboration.js +8 -3
- package/dist/server/repositories/diagnostic-logs.js +243 -0
- package/dist/server/repositories/entity-ownership.js +92 -0
- package/dist/server/repositories/goals.js +7 -2
- package/dist/server/repositories/habits.js +122 -16
- package/dist/server/repositories/notes.js +119 -41
- package/dist/server/repositories/preferences.js +1765 -0
- package/dist/server/repositories/projects.js +18 -7
- package/dist/server/repositories/psyche.js +84 -27
- package/dist/server/repositories/rewards.js +112 -4
- package/dist/server/repositories/strategies.js +450 -0
- package/dist/server/repositories/tags.js +11 -6
- package/dist/server/repositories/task-runs.js +10 -2
- package/dist/server/repositories/tasks.js +99 -17
- package/dist/server/repositories/users.js +417 -0
- package/dist/server/repositories/wiki-memory.js +3366 -0
- package/dist/server/services/context.js +20 -18
- package/dist/server/services/dashboard.js +29 -6
- package/dist/server/services/entity-crud.js +21 -3
- package/dist/server/services/insights.js +9 -7
- package/dist/server/services/projects.js +2 -1
- package/dist/server/services/psyche.js +10 -9
- package/dist/server/types.js +594 -30
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server/migrations/015_multi_user_and_strategies.sql +244 -0
- package/server/migrations/016_health_companion.sql +158 -0
- package/server/migrations/016_strategy_contracts_and_user_graph.sql +22 -0
- package/server/migrations/017_preferences.sql +131 -0
- package/server/migrations/018_preference_catalogs.sql +31 -0
- package/server/migrations/019_wiki_memory.sql +255 -0
- package/server/migrations/020_wiki_page_hierarchy.sql +11 -0
- package/server/migrations/021_hide_evidence_from_wiki_index.sql +3 -0
- package/server/migrations/022_wiki_ingest_background.sql +85 -0
- package/server/migrations/023_diagnostic_logs.sql +28 -0
- package/skills/forge-openclaw/SKILL.md +126 -34
- package/skills/forge-openclaw/entity_conversation_playbooks.md +337 -0
- package/skills/forge-openclaw/psyche_entity_playbooks.md +404 -0
- package/dist/assets/board-8L3uX7_O.js +0 -6
- package/dist/assets/index-Cj1IBH_w.js +0 -36
- package/dist/assets/index-Cj1IBH_w.js.map +0 -1
- package/dist/assets/index-DQT6EbuS.css +0 -1
- package/dist/assets/vendor-BvM2F9Dp.js +0 -503
- package/dist/assets/vendor-BvM2F9Dp.js.map +0 -1
- package/dist/assets/vendor-CRS-psbw.css +0 -1
- package/dist/assets/viz-CNeunkfu.js +0 -34
- package/dist/assets/viz-CNeunkfu.js.map +0 -1
|
@@ -311,7 +311,9 @@ export function getRewardLedgerEventById(rewardId) {
|
|
|
311
311
|
}
|
|
312
312
|
export function getTotalXp() {
|
|
313
313
|
ensureDefaultRewardRules();
|
|
314
|
-
const row = getDatabase()
|
|
314
|
+
const row = getDatabase()
|
|
315
|
+
.prepare(`SELECT COALESCE(SUM(delta_xp), 0) AS total FROM reward_ledger`)
|
|
316
|
+
.get();
|
|
315
317
|
return row.total;
|
|
316
318
|
}
|
|
317
319
|
export function getWeeklyXp(weekStartIso) {
|
|
@@ -410,7 +412,59 @@ export function reverseLatestTaskCompletionReward(task, activity) {
|
|
|
410
412
|
taskId: task.id
|
|
411
413
|
}
|
|
412
414
|
});
|
|
413
|
-
getDatabase()
|
|
415
|
+
getDatabase()
|
|
416
|
+
.prepare(`UPDATE reward_ledger SET reversed_by_reward_id = ? WHERE id = ?`)
|
|
417
|
+
.run(reversal.id, latest.id);
|
|
418
|
+
return reversal;
|
|
419
|
+
}
|
|
420
|
+
export function reverseLatestHabitCheckInReward(habit, dateKey, activity) {
|
|
421
|
+
ensureDefaultRewardRules();
|
|
422
|
+
const reversibleGroup = `habit:${habit.id}:${dateKey}`;
|
|
423
|
+
const latest = getDatabase()
|
|
424
|
+
.prepare(`SELECT
|
|
425
|
+
id, rule_id, event_log_id, entity_type, entity_id, actor, source, delta_xp, reason_title, reason_summary,
|
|
426
|
+
reversible_group, reversed_by_reward_id, metadata_json, created_at
|
|
427
|
+
FROM reward_ledger
|
|
428
|
+
WHERE reversible_group = ?
|
|
429
|
+
AND reversed_by_reward_id IS NULL
|
|
430
|
+
ORDER BY created_at DESC
|
|
431
|
+
LIMIT 1`)
|
|
432
|
+
.get(reversibleGroup);
|
|
433
|
+
if (!latest) {
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
const reversalEventLog = recordEventLog({
|
|
437
|
+
eventKind: "reward.habit_check_in_reversed",
|
|
438
|
+
entityType: "habit",
|
|
439
|
+
entityId: habit.id,
|
|
440
|
+
actor: activity.actor ?? null,
|
|
441
|
+
source: activity.source,
|
|
442
|
+
metadata: {
|
|
443
|
+
rewardId: latest.id,
|
|
444
|
+
habitId: habit.id,
|
|
445
|
+
dateKey
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
const reversal = insertLedgerEvent({
|
|
449
|
+
ruleId: latest.rule_id,
|
|
450
|
+
eventLogId: reversalEventLog.id,
|
|
451
|
+
entityType: latest.entity_type,
|
|
452
|
+
entityId: latest.entity_id,
|
|
453
|
+
actor: activity.actor ?? null,
|
|
454
|
+
source: activity.source,
|
|
455
|
+
deltaXp: -latest.delta_xp,
|
|
456
|
+
reasonTitle: `Habit entry removed: ${habit.title}`,
|
|
457
|
+
reasonSummary: `Habit check-in removed for ${dateKey}.`,
|
|
458
|
+
reversibleGroup: latest.reversible_group,
|
|
459
|
+
metadata: {
|
|
460
|
+
reversedRewardId: latest.id,
|
|
461
|
+
habitId: habit.id,
|
|
462
|
+
dateKey
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
getDatabase()
|
|
466
|
+
.prepare(`UPDATE reward_ledger SET reversed_by_reward_id = ? WHERE id = ?`)
|
|
467
|
+
.run(reversal.id, latest.id);
|
|
414
468
|
return reversal;
|
|
415
469
|
}
|
|
416
470
|
export function recordInsightAppliedReward(insightId, entityType, entityId, activity) {
|
|
@@ -653,7 +707,9 @@ export function recordWorkAdjustmentReward(input) {
|
|
|
653
707
|
actor: input.actor ?? null,
|
|
654
708
|
source: input.source,
|
|
655
709
|
deltaXp,
|
|
656
|
-
reasonTitle: bucketDelta > 0
|
|
710
|
+
reasonTitle: bucketDelta > 0
|
|
711
|
+
? "Manual work minutes added"
|
|
712
|
+
: "Manual work minutes removed",
|
|
657
713
|
reasonSummary: `${appliedMinutes} manual minute${appliedMinutes === 1 ? "" : "s"} ${direction}, shifting ${Math.abs(bucketDelta)} ${intervalMinutes}-minute reward bucket${Math.abs(bucketDelta) === 1 ? "" : "s"} for ${input.targetTitle}.`,
|
|
658
714
|
reversibleGroup: `work_adjustment:${entityType}:${input.entityId}:${input.adjustmentId}`,
|
|
659
715
|
metadata: {
|
|
@@ -695,7 +751,8 @@ export function recordSessionEvent(input, activity, now = new Date()) {
|
|
|
695
751
|
}, now);
|
|
696
752
|
const day = sessionEvent.createdAt.slice(0, 10);
|
|
697
753
|
const currentAmbientXp = getDailyAmbientXp(day);
|
|
698
|
-
const active = sessionEvent.metrics.visible === true &&
|
|
754
|
+
const active = sessionEvent.metrics.visible === true &&
|
|
755
|
+
sessionEvent.metrics.interacted === true;
|
|
699
756
|
const ruleCode = sessionEvent.eventType === "dwell_120_seconds"
|
|
700
757
|
? "session_dwell_120"
|
|
701
758
|
: sessionEvent.eventType === "scroll_depth_75"
|
|
@@ -770,6 +827,57 @@ export function recordHabitCheckInReward(habit, status, dateKey, activity) {
|
|
|
770
827
|
}
|
|
771
828
|
});
|
|
772
829
|
}
|
|
830
|
+
export function recordHabitGeneratedWorkoutReward(input, activity) {
|
|
831
|
+
ensureDefaultRewardRules();
|
|
832
|
+
if (input.xpReward <= 0) {
|
|
833
|
+
return null;
|
|
834
|
+
}
|
|
835
|
+
const reversibleGroup = `habit_generated_workout:${input.checkInId}`;
|
|
836
|
+
const existing = getDatabase()
|
|
837
|
+
.prepare(`SELECT
|
|
838
|
+
id, rule_id, event_log_id, entity_type, entity_id, actor, source, delta_xp, reason_title, reason_summary,
|
|
839
|
+
reversible_group, reversed_by_reward_id, metadata_json, created_at
|
|
840
|
+
FROM reward_ledger
|
|
841
|
+
WHERE reversible_group = ?
|
|
842
|
+
LIMIT 1`)
|
|
843
|
+
.get(reversibleGroup);
|
|
844
|
+
if (existing) {
|
|
845
|
+
return mapLedger(existing);
|
|
846
|
+
}
|
|
847
|
+
const eventLog = recordEventLog({
|
|
848
|
+
eventKind: "reward.habit_generated_workout",
|
|
849
|
+
entityType: "habit",
|
|
850
|
+
entityId: input.habitId,
|
|
851
|
+
actor: activity.actor ?? null,
|
|
852
|
+
source: activity.source,
|
|
853
|
+
metadata: {
|
|
854
|
+
habitId: input.habitId,
|
|
855
|
+
checkInId: input.checkInId,
|
|
856
|
+
workoutId: input.workoutId,
|
|
857
|
+
workoutType: input.workoutType,
|
|
858
|
+
xpReward: input.xpReward
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
return insertLedgerEvent({
|
|
862
|
+
ruleId: null,
|
|
863
|
+
eventLogId: eventLog.id,
|
|
864
|
+
entityType: "habit",
|
|
865
|
+
entityId: input.habitId,
|
|
866
|
+
actor: activity.actor ?? null,
|
|
867
|
+
source: activity.source,
|
|
868
|
+
deltaXp: input.xpReward,
|
|
869
|
+
reasonTitle: `Generated workout: ${input.habitTitle}`,
|
|
870
|
+
reasonSummary: `Created a ${input.workoutType} session from a completed habit.`,
|
|
871
|
+
reversibleGroup,
|
|
872
|
+
metadata: {
|
|
873
|
+
habitId: input.habitId,
|
|
874
|
+
checkInId: input.checkInId,
|
|
875
|
+
workoutId: input.workoutId,
|
|
876
|
+
workoutType: input.workoutType,
|
|
877
|
+
rewardCategory: "habit_generated_workout"
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
}
|
|
773
881
|
export function recordWeeklyReviewCompletionReward(input, activity) {
|
|
774
882
|
ensureDefaultRewardRules();
|
|
775
883
|
const rule = getRuleByCode("weekly_review_completed");
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { getDatabase, runInTransaction } from "../db.js";
|
|
3
|
+
import { HttpError } from "../errors.js";
|
|
4
|
+
import { decorateOwnedEntity, filterOwnedEntities, inferFirstOwnedUserId, setEntityOwner } from "./entity-ownership.js";
|
|
5
|
+
import { getUserById, resolveUserForMutation } from "./users.js";
|
|
6
|
+
import { getBehaviorById, getBehaviorPatternById, getBeliefEntryById, getEmotionDefinitionById, getEventTypeById, getModeGuideSessionById, getModeProfileById, getPsycheValueById, getTriggerReportById } from "./psyche.js";
|
|
7
|
+
import { getCalendarEventById, getTaskTimeboxById, getWorkBlockTemplateById } from "./calendar.js";
|
|
8
|
+
import { getGoalById } from "./goals.js";
|
|
9
|
+
import { getHabitById } from "./habits.js";
|
|
10
|
+
import { getInsightById } from "./collaboration.js";
|
|
11
|
+
import { getNoteById } from "./notes.js";
|
|
12
|
+
import { getProjectById } from "./projects.js";
|
|
13
|
+
import { getTagById } from "./tags.js";
|
|
14
|
+
import { getTaskById, listTasks } from "./tasks.js";
|
|
15
|
+
import { getProjectSummary } from "../services/projects.js";
|
|
16
|
+
import { createStrategySchema, strategySchema, updateStrategySchema } from "../types.js";
|
|
17
|
+
function statusProgress(status) {
|
|
18
|
+
switch (status) {
|
|
19
|
+
case "done":
|
|
20
|
+
case "completed":
|
|
21
|
+
case "reviewed":
|
|
22
|
+
case "integrated":
|
|
23
|
+
return 1;
|
|
24
|
+
case "in_progress":
|
|
25
|
+
case "active":
|
|
26
|
+
return 0.66;
|
|
27
|
+
case "focus":
|
|
28
|
+
return 0.5;
|
|
29
|
+
case "blocked":
|
|
30
|
+
case "paused":
|
|
31
|
+
return 0.25;
|
|
32
|
+
default:
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function goalProgress(goalId) {
|
|
37
|
+
const tasks = listTasks({ goalId });
|
|
38
|
+
if (tasks.length === 0) {
|
|
39
|
+
const goal = getGoalById(goalId);
|
|
40
|
+
return goal?.status === "completed" ? 1 : 0;
|
|
41
|
+
}
|
|
42
|
+
const completed = tasks.filter((task) => task.status === "done").length;
|
|
43
|
+
return completed / tasks.length;
|
|
44
|
+
}
|
|
45
|
+
function resolveLinkedEntity(entityType, entityId) {
|
|
46
|
+
switch (entityType) {
|
|
47
|
+
case "goal":
|
|
48
|
+
return getGoalById(entityId);
|
|
49
|
+
case "project":
|
|
50
|
+
return getProjectById(entityId);
|
|
51
|
+
case "task":
|
|
52
|
+
return getTaskById(entityId);
|
|
53
|
+
case "strategy":
|
|
54
|
+
return getStrategyById(entityId);
|
|
55
|
+
case "habit":
|
|
56
|
+
return getHabitById(entityId);
|
|
57
|
+
case "tag":
|
|
58
|
+
return getTagById(entityId);
|
|
59
|
+
case "note":
|
|
60
|
+
return getNoteById(entityId);
|
|
61
|
+
case "insight":
|
|
62
|
+
return getInsightById(entityId);
|
|
63
|
+
case "calendar_event":
|
|
64
|
+
return getCalendarEventById(entityId);
|
|
65
|
+
case "work_block_template":
|
|
66
|
+
return getWorkBlockTemplateById(entityId);
|
|
67
|
+
case "task_timebox":
|
|
68
|
+
return getTaskTimeboxById(entityId);
|
|
69
|
+
case "psyche_value":
|
|
70
|
+
return getPsycheValueById(entityId);
|
|
71
|
+
case "behavior_pattern":
|
|
72
|
+
return getBehaviorPatternById(entityId);
|
|
73
|
+
case "behavior":
|
|
74
|
+
return getBehaviorById(entityId);
|
|
75
|
+
case "belief_entry":
|
|
76
|
+
return getBeliefEntryById(entityId);
|
|
77
|
+
case "mode_profile":
|
|
78
|
+
return getModeProfileById(entityId);
|
|
79
|
+
case "mode_guide_session":
|
|
80
|
+
return getModeGuideSessionById(entityId);
|
|
81
|
+
case "event_type":
|
|
82
|
+
return getEventTypeById(entityId);
|
|
83
|
+
case "emotion_definition":
|
|
84
|
+
return getEmotionDefinitionById(entityId);
|
|
85
|
+
case "trigger_report":
|
|
86
|
+
return getTriggerReportById(entityId);
|
|
87
|
+
default:
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function assertStrategyRelations(input) {
|
|
92
|
+
for (const goalId of input.targetGoalIds) {
|
|
93
|
+
if (!getGoalById(goalId)) {
|
|
94
|
+
throw new Error(`Goal ${goalId} does not exist`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
for (const projectId of input.targetProjectIds) {
|
|
98
|
+
if (!getProjectById(projectId)) {
|
|
99
|
+
throw new Error(`Project ${projectId} does not exist`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
for (const linked of input.linkedEntities) {
|
|
103
|
+
if (!resolveLinkedEntity(linked.entityType, linked.entityId)) {
|
|
104
|
+
throw new Error(`${linked.entityType} ${linked.entityId} does not exist`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
for (const node of input.graph.nodes) {
|
|
108
|
+
if (node.entityType === "project" && !getProjectById(node.entityId)) {
|
|
109
|
+
throw new Error(`Project ${node.entityId} does not exist`);
|
|
110
|
+
}
|
|
111
|
+
if (node.entityType === "task" && !getTaskById(node.entityId)) {
|
|
112
|
+
throw new Error(`Task ${node.entityId} does not exist`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function parseJsonArray(value) {
|
|
117
|
+
return JSON.parse(value);
|
|
118
|
+
}
|
|
119
|
+
function nodeProgress(node) {
|
|
120
|
+
if (node.entityType === "project") {
|
|
121
|
+
return (getProjectSummary(node.entityId)?.progress ?? 0) / 100;
|
|
122
|
+
}
|
|
123
|
+
return statusProgress(getTaskById(node.entityId)?.status ?? "backlog");
|
|
124
|
+
}
|
|
125
|
+
function buildStrategyMetrics(graph, targetGoalIds, targetProjectIds) {
|
|
126
|
+
const nodeProgressById = new Map(graph.nodes.map((node) => [node.id, nodeProgress(node)]));
|
|
127
|
+
const incoming = new Map();
|
|
128
|
+
for (const node of graph.nodes) {
|
|
129
|
+
incoming.set(node.id, []);
|
|
130
|
+
}
|
|
131
|
+
for (const edge of graph.edges) {
|
|
132
|
+
incoming.get(edge.to)?.push(edge.from);
|
|
133
|
+
}
|
|
134
|
+
const completedNodeIds = graph.nodes
|
|
135
|
+
.filter((node) => (nodeProgressById.get(node.id) ?? 0) >= 1)
|
|
136
|
+
.map((node) => node.id);
|
|
137
|
+
const startedNodeIds = graph.nodes
|
|
138
|
+
.filter((node) => (nodeProgressById.get(node.id) ?? 0) > 0)
|
|
139
|
+
.map((node) => node.id);
|
|
140
|
+
const blockedNodeIds = graph.nodes
|
|
141
|
+
.filter((node) => {
|
|
142
|
+
if (node.entityType === "project") {
|
|
143
|
+
return getProjectById(node.entityId)?.status === "paused";
|
|
144
|
+
}
|
|
145
|
+
return getTaskById(node.entityId)?.status === "blocked";
|
|
146
|
+
})
|
|
147
|
+
.map((node) => node.id);
|
|
148
|
+
const outOfOrderNodeIds = graph.nodes
|
|
149
|
+
.filter((node) => {
|
|
150
|
+
const progress = nodeProgressById.get(node.id) ?? 0;
|
|
151
|
+
if (progress <= 0) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
const prerequisites = incoming.get(node.id) ?? [];
|
|
155
|
+
return prerequisites.some((dependencyId) => (nodeProgressById.get(dependencyId) ?? 0) < 1);
|
|
156
|
+
})
|
|
157
|
+
.map((node) => node.id);
|
|
158
|
+
const activeNodeIds = graph.nodes
|
|
159
|
+
.filter((node) => {
|
|
160
|
+
const progress = nodeProgressById.get(node.id) ?? 0;
|
|
161
|
+
if (progress >= 1) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
const prerequisites = incoming.get(node.id) ?? [];
|
|
165
|
+
return prerequisites.every((dependencyId) => (nodeProgressById.get(dependencyId) ?? 0) >= 1);
|
|
166
|
+
})
|
|
167
|
+
.map((node) => node.id);
|
|
168
|
+
const goalScores = targetGoalIds.map((goalId) => goalProgress(goalId));
|
|
169
|
+
const projectScores = targetProjectIds.map((projectId) => (getProjectSummary(projectId)?.progress ?? 0) / 100);
|
|
170
|
+
const targetScores = [...goalScores, ...projectScores];
|
|
171
|
+
const nodeAverage = graph.nodes.length === 0
|
|
172
|
+
? 0
|
|
173
|
+
: graph.nodes.reduce((sum, node) => sum + (nodeProgressById.get(node.id) ?? 0), 0) / graph.nodes.length;
|
|
174
|
+
const targetAverage = targetScores.length === 0
|
|
175
|
+
? nodeAverage
|
|
176
|
+
: targetScores.reduce((sum, value) => sum + value, 0) /
|
|
177
|
+
targetScores.length;
|
|
178
|
+
const graphProjectIds = new Set(graph.nodes
|
|
179
|
+
.filter((node) => node.entityType === "project")
|
|
180
|
+
.map((node) => node.entityId));
|
|
181
|
+
const graphTaskIds = new Set(graph.nodes
|
|
182
|
+
.filter((node) => node.entityType === "task")
|
|
183
|
+
.map((node) => node.entityId));
|
|
184
|
+
const offPlanEntityKeys = new Set();
|
|
185
|
+
const offPlanActiveEntityKeys = new Set();
|
|
186
|
+
const offPlanCompletedEntityKeys = new Set();
|
|
187
|
+
const markOffPlanTask = (taskId) => {
|
|
188
|
+
const task = getTaskById(taskId);
|
|
189
|
+
if (!task) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const entityKey = `task:${task.id}`;
|
|
193
|
+
offPlanEntityKeys.add(entityKey);
|
|
194
|
+
if (task.status === "done") {
|
|
195
|
+
offPlanCompletedEntityKeys.add(entityKey);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (["focus", "in_progress", "blocked"].includes(task.status)) {
|
|
199
|
+
offPlanActiveEntityKeys.add(entityKey);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
for (const projectId of targetProjectIds) {
|
|
203
|
+
const project = getProjectById(projectId);
|
|
204
|
+
if (project &&
|
|
205
|
+
!graphProjectIds.has(project.id) &&
|
|
206
|
+
project.status !== "completed") {
|
|
207
|
+
const entityKey = `project:${project.id}`;
|
|
208
|
+
offPlanEntityKeys.add(entityKey);
|
|
209
|
+
offPlanActiveEntityKeys.add(entityKey);
|
|
210
|
+
}
|
|
211
|
+
for (const task of listTasks({ projectId })) {
|
|
212
|
+
if (!graphTaskIds.has(task.id) &&
|
|
213
|
+
["focus", "in_progress", "done", "blocked"].includes(task.status)) {
|
|
214
|
+
markOffPlanTask(task.id);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
for (const goalId of targetGoalIds) {
|
|
219
|
+
for (const task of listTasks({ goalId })) {
|
|
220
|
+
if (!graphTaskIds.has(task.id) &&
|
|
221
|
+
["focus", "in_progress", "done", "blocked"].includes(task.status)) {
|
|
222
|
+
markOffPlanTask(task.id);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const totalNodes = Math.max(1, graph.nodes.length);
|
|
227
|
+
const offPlanEntityCount = offPlanEntityKeys.size;
|
|
228
|
+
const offPlanActiveEntityCount = offPlanActiveEntityKeys.size;
|
|
229
|
+
const offPlanCompletedEntityCount = offPlanCompletedEntityKeys.size;
|
|
230
|
+
const planCoverageScore = Math.max(0, Math.min(100, Math.round(nodeAverage * 100)));
|
|
231
|
+
const sequencingScore = Math.max(0, Math.min(100, Math.round(100 - (outOfOrderNodeIds.length / totalNodes) * 100)));
|
|
232
|
+
const scopeDisciplineScore = Math.max(0, Math.min(100, Math.round(100 - (offPlanEntityCount / totalNodes) * 100)));
|
|
233
|
+
const blockedRatio = blockedNodeIds.length / totalNodes;
|
|
234
|
+
const qualityScore = Math.max(0, Math.min(100, Math.round(Math.max(0, Math.min(1, targetAverage * 0.8 + (1 - blockedRatio) * 0.2)) * 100)));
|
|
235
|
+
const targetProgressScore = Math.max(0, Math.min(100, Math.round(targetAverage * 100)));
|
|
236
|
+
const alignmentScore = Math.max(0, Math.min(100, Math.round(planCoverageScore * 0.35 +
|
|
237
|
+
sequencingScore * 0.3 +
|
|
238
|
+
scopeDisciplineScore * 0.2 +
|
|
239
|
+
qualityScore * 0.15)));
|
|
240
|
+
return {
|
|
241
|
+
alignmentScore,
|
|
242
|
+
planCoverageScore,
|
|
243
|
+
sequencingScore,
|
|
244
|
+
scopeDisciplineScore,
|
|
245
|
+
qualityScore,
|
|
246
|
+
targetProgressScore,
|
|
247
|
+
completedNodeCount: completedNodeIds.length,
|
|
248
|
+
startedNodeCount: startedNodeIds.length,
|
|
249
|
+
readyNodeCount: activeNodeIds.length,
|
|
250
|
+
totalNodeCount: totalNodes,
|
|
251
|
+
completedTargetCount: targetScores.filter((score) => score >= 1).length,
|
|
252
|
+
totalTargetCount: targetScores.length,
|
|
253
|
+
offPlanEntityCount,
|
|
254
|
+
offPlanActiveEntityCount,
|
|
255
|
+
offPlanCompletedEntityCount,
|
|
256
|
+
activeNodeIds: activeNodeIds.slice(0, 8),
|
|
257
|
+
nextNodeIds: activeNodeIds.slice(0, 5),
|
|
258
|
+
blockedNodeIds,
|
|
259
|
+
outOfOrderNodeIds
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function assertStrategyContractReady(input) {
|
|
263
|
+
if (input.graph.nodes.length === 0) {
|
|
264
|
+
throw new HttpError(400, "strategy_contract_invalid", "A locked strategy needs at least one project or task node in its graph.", { fields: ["graph.nodes"] });
|
|
265
|
+
}
|
|
266
|
+
if (input.targetGoalIds.length === 0 && input.targetProjectIds.length === 0) {
|
|
267
|
+
throw new HttpError(400, "strategy_contract_invalid", "A locked strategy must target at least one goal or project.", { fields: ["targetGoalIds", "targetProjectIds"] });
|
|
268
|
+
}
|
|
269
|
+
if (input.overview.trim().length === 0 &&
|
|
270
|
+
input.endStateDescription.trim().length === 0) {
|
|
271
|
+
throw new HttpError(400, "strategy_contract_invalid", "A locked strategy needs an overview or end-state description so the contract is explicit.", { fields: ["overview", "endStateDescription"] });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function mapStrategy(row) {
|
|
275
|
+
const graph = JSON.parse(row.graph_json);
|
|
276
|
+
return strategySchema.parse(decorateOwnedEntity("strategy", {
|
|
277
|
+
id: row.id,
|
|
278
|
+
title: row.title,
|
|
279
|
+
overview: row.overview,
|
|
280
|
+
endStateDescription: row.end_state_description,
|
|
281
|
+
status: row.status,
|
|
282
|
+
targetGoalIds: parseJsonArray(row.target_goal_ids_json),
|
|
283
|
+
targetProjectIds: parseJsonArray(row.target_project_ids_json),
|
|
284
|
+
linkedEntities: parseJsonArray(row.linked_entities_json),
|
|
285
|
+
graph,
|
|
286
|
+
metrics: buildStrategyMetrics(graph, parseJsonArray(row.target_goal_ids_json), parseJsonArray(row.target_project_ids_json)),
|
|
287
|
+
isLocked: row.is_locked === 1,
|
|
288
|
+
lockedAt: row.locked_at,
|
|
289
|
+
lockedByUserId: row.locked_by_user_id,
|
|
290
|
+
lockedByUser: row.locked_by_user_id
|
|
291
|
+
? (getUserById(row.locked_by_user_id) ?? null)
|
|
292
|
+
: null,
|
|
293
|
+
createdAt: row.created_at,
|
|
294
|
+
updatedAt: row.updated_at
|
|
295
|
+
}));
|
|
296
|
+
}
|
|
297
|
+
export function listStrategies(filters = {}) {
|
|
298
|
+
const whereClauses = [];
|
|
299
|
+
const params = [];
|
|
300
|
+
if (filters.status) {
|
|
301
|
+
whereClauses.push("status = ?");
|
|
302
|
+
params.push(filters.status);
|
|
303
|
+
}
|
|
304
|
+
const whereSql = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
|
|
305
|
+
const limitSql = filters.limit ? "LIMIT ?" : "";
|
|
306
|
+
if (filters.limit) {
|
|
307
|
+
params.push(filters.limit);
|
|
308
|
+
}
|
|
309
|
+
const rows = getDatabase()
|
|
310
|
+
.prepare(`SELECT id, title, overview, end_state_description, status, target_goal_ids_json, target_project_ids_json,
|
|
311
|
+
linked_entities_json, graph_json, is_locked, locked_at, locked_by_user_id, created_at, updated_at
|
|
312
|
+
FROM strategies
|
|
313
|
+
${whereSql}
|
|
314
|
+
ORDER BY updated_at DESC
|
|
315
|
+
${limitSql}`)
|
|
316
|
+
.all(...params);
|
|
317
|
+
return filterOwnedEntities("strategy", rows.map(mapStrategy), filters.userIds);
|
|
318
|
+
}
|
|
319
|
+
export function getStrategyById(strategyId) {
|
|
320
|
+
const row = getDatabase()
|
|
321
|
+
.prepare(`SELECT id, title, overview, end_state_description, status, target_goal_ids_json, target_project_ids_json,
|
|
322
|
+
linked_entities_json, graph_json, is_locked, locked_at, locked_by_user_id, created_at, updated_at
|
|
323
|
+
FROM strategies
|
|
324
|
+
WHERE id = ?`)
|
|
325
|
+
.get(strategyId);
|
|
326
|
+
return row ? mapStrategy(row) : undefined;
|
|
327
|
+
}
|
|
328
|
+
export function createStrategy(input) {
|
|
329
|
+
return runInTransaction(() => {
|
|
330
|
+
const parsed = createStrategySchema.parse(input);
|
|
331
|
+
assertStrategyRelations(parsed);
|
|
332
|
+
if (parsed.isLocked) {
|
|
333
|
+
assertStrategyContractReady(parsed);
|
|
334
|
+
}
|
|
335
|
+
const now = new Date().toISOString();
|
|
336
|
+
const id = `strategy_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
|
|
337
|
+
const inferredOwnerUserId = parsed.userId ??
|
|
338
|
+
inferFirstOwnedUserId([
|
|
339
|
+
...parsed.targetProjectIds.map((entityId) => ({
|
|
340
|
+
entityType: "project",
|
|
341
|
+
entityId
|
|
342
|
+
})),
|
|
343
|
+
...parsed.targetGoalIds.map((entityId) => ({
|
|
344
|
+
entityType: "goal",
|
|
345
|
+
entityId
|
|
346
|
+
})),
|
|
347
|
+
...parsed.graph.nodes.map((node) => ({
|
|
348
|
+
entityType: node.entityType,
|
|
349
|
+
entityId: node.entityId
|
|
350
|
+
})),
|
|
351
|
+
...parsed.linkedEntities.map((entity) => ({
|
|
352
|
+
entityType: entity.entityType,
|
|
353
|
+
entityId: entity.entityId
|
|
354
|
+
}))
|
|
355
|
+
]);
|
|
356
|
+
const ownerUser = resolveUserForMutation(inferredOwnerUserId);
|
|
357
|
+
const lockedByUserId = parsed.isLocked
|
|
358
|
+
? resolveUserForMutation(parsed.lockedByUserId ?? ownerUser.id).id
|
|
359
|
+
: null;
|
|
360
|
+
getDatabase()
|
|
361
|
+
.prepare(`INSERT INTO strategies (
|
|
362
|
+
id, title, overview, end_state_description, status, target_goal_ids_json, target_project_ids_json,
|
|
363
|
+
linked_entities_json, graph_json, is_locked, locked_at, locked_by_user_id, created_at, updated_at
|
|
364
|
+
)
|
|
365
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
366
|
+
.run(id, parsed.title, parsed.overview, parsed.endStateDescription, parsed.status, JSON.stringify(parsed.targetGoalIds), JSON.stringify(parsed.targetProjectIds), JSON.stringify(parsed.linkedEntities), JSON.stringify(parsed.graph), parsed.isLocked ? 1 : 0, parsed.isLocked ? now : null, lockedByUserId, now, now);
|
|
367
|
+
setEntityOwner("strategy", id, ownerUser.id);
|
|
368
|
+
return getStrategyById(id);
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
export function updateStrategy(strategyId, patch) {
|
|
372
|
+
const current = getStrategyById(strategyId);
|
|
373
|
+
if (!current) {
|
|
374
|
+
return undefined;
|
|
375
|
+
}
|
|
376
|
+
return runInTransaction(() => {
|
|
377
|
+
const parsed = updateStrategySchema.parse(patch);
|
|
378
|
+
const changesCoreStrategyShape = parsed.title !== undefined ||
|
|
379
|
+
parsed.overview !== undefined ||
|
|
380
|
+
parsed.endStateDescription !== undefined ||
|
|
381
|
+
parsed.targetGoalIds !== undefined ||
|
|
382
|
+
parsed.targetProjectIds !== undefined ||
|
|
383
|
+
parsed.linkedEntities !== undefined ||
|
|
384
|
+
parsed.graph !== undefined ||
|
|
385
|
+
parsed.userId !== undefined;
|
|
386
|
+
if (current.isLocked &&
|
|
387
|
+
parsed.isLocked !== false &&
|
|
388
|
+
changesCoreStrategyShape) {
|
|
389
|
+
throw new Error("Strategy is locked as a contract. Unlock it before changing the plan, targets, links, or owner.");
|
|
390
|
+
}
|
|
391
|
+
const next = {
|
|
392
|
+
title: parsed.title ?? current.title,
|
|
393
|
+
overview: parsed.overview ?? current.overview,
|
|
394
|
+
endStateDescription: parsed.endStateDescription ?? current.endStateDescription,
|
|
395
|
+
status: parsed.status ?? current.status,
|
|
396
|
+
targetGoalIds: parsed.targetGoalIds ?? current.targetGoalIds,
|
|
397
|
+
targetProjectIds: parsed.targetProjectIds ?? current.targetProjectIds,
|
|
398
|
+
linkedEntities: parsed.linkedEntities ?? current.linkedEntities,
|
|
399
|
+
graph: parsed.graph ?? current.graph,
|
|
400
|
+
isLocked: parsed.isLocked ?? current.isLocked,
|
|
401
|
+
lockedAt: parsed.isLocked === false
|
|
402
|
+
? null
|
|
403
|
+
: parsed.isLocked === true && !current.isLocked
|
|
404
|
+
? new Date().toISOString()
|
|
405
|
+
: current.lockedAt,
|
|
406
|
+
lockedByUserId: parsed.isLocked === false
|
|
407
|
+
? null
|
|
408
|
+
: parsed.isLocked === true
|
|
409
|
+
? resolveUserForMutation(parsed.lockedByUserId ??
|
|
410
|
+
current.lockedByUserId ??
|
|
411
|
+
parsed.userId ??
|
|
412
|
+
current.userId ??
|
|
413
|
+
inferFirstOwnedUserId([
|
|
414
|
+
...current.targetProjectIds.map((entityId) => ({
|
|
415
|
+
entityType: "project",
|
|
416
|
+
entityId
|
|
417
|
+
})),
|
|
418
|
+
...current.targetGoalIds.map((entityId) => ({
|
|
419
|
+
entityType: "goal",
|
|
420
|
+
entityId
|
|
421
|
+
}))
|
|
422
|
+
])).id
|
|
423
|
+
: current.lockedByUserId,
|
|
424
|
+
updatedAt: new Date().toISOString()
|
|
425
|
+
};
|
|
426
|
+
assertStrategyRelations(next);
|
|
427
|
+
if (next.isLocked) {
|
|
428
|
+
assertStrategyContractReady(next);
|
|
429
|
+
}
|
|
430
|
+
getDatabase()
|
|
431
|
+
.prepare(`UPDATE strategies
|
|
432
|
+
SET title = ?, overview = ?, end_state_description = ?, status = ?, target_goal_ids_json = ?,
|
|
433
|
+
target_project_ids_json = ?, linked_entities_json = ?, graph_json = ?, is_locked = ?, locked_at = ?,
|
|
434
|
+
locked_by_user_id = ?, updated_at = ?
|
|
435
|
+
WHERE id = ?`)
|
|
436
|
+
.run(next.title, next.overview, next.endStateDescription, next.status, JSON.stringify(next.targetGoalIds), JSON.stringify(next.targetProjectIds), JSON.stringify(next.linkedEntities), JSON.stringify(next.graph), next.isLocked ? 1 : 0, next.lockedAt, next.lockedByUserId, next.updatedAt, strategyId);
|
|
437
|
+
if (parsed.userId !== undefined) {
|
|
438
|
+
setEntityOwner("strategy", strategyId, parsed.userId);
|
|
439
|
+
}
|
|
440
|
+
return getStrategyById(strategyId);
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
export function deleteStrategy(strategyId) {
|
|
444
|
+
const strategy = getStrategyById(strategyId);
|
|
445
|
+
if (!strategy) {
|
|
446
|
+
return undefined;
|
|
447
|
+
}
|
|
448
|
+
getDatabase().prepare(`DELETE FROM strategies WHERE id = ?`).run(strategyId);
|
|
449
|
+
return strategy;
|
|
450
|
+
}
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { getDatabase } from "../db.js";
|
|
3
3
|
import { HttpError } from "../errors.js";
|
|
4
|
+
import { decorateOwnedEntity, setEntityOwner } from "./entity-ownership.js";
|
|
4
5
|
import { filterDeletedEntities, isEntityDeleted } from "./deleted-entities.js";
|
|
5
6
|
import { recordActivityEvent } from "./activity-events.js";
|
|
6
7
|
import { tagSchema, updateTagSchema } from "../types.js";
|
|
7
8
|
function mapTag(row) {
|
|
8
|
-
return tagSchema.parse({
|
|
9
|
-
id: row.id,
|
|
10
|
-
name: row.name,
|
|
9
|
+
return tagSchema.parse(decorateOwnedEntity("tag", {
|
|
10
|
+
id: String(row.id ?? ""),
|
|
11
|
+
name: String(row.name ?? ""),
|
|
11
12
|
kind: row.kind,
|
|
12
|
-
color: row.color,
|
|
13
|
-
description: row.description
|
|
14
|
-
});
|
|
13
|
+
color: String(row.color ?? ""),
|
|
14
|
+
description: String(row.description ?? "")
|
|
15
|
+
}));
|
|
15
16
|
}
|
|
16
17
|
export function listTags() {
|
|
17
18
|
const rows = getDatabase()
|
|
@@ -56,6 +57,7 @@ export function createTag(input, activity) {
|
|
|
56
57
|
.prepare(`INSERT INTO tags (id, name, kind, color, description, created_at)
|
|
57
58
|
VALUES (?, ?, ?, ?, ?, ?)`)
|
|
58
59
|
.run(tag.id, tag.name, tag.kind, tag.color, tag.description, now);
|
|
60
|
+
setEntityOwner("tag", tag.id, input.userId);
|
|
59
61
|
if (activity) {
|
|
60
62
|
recordActivityEvent({
|
|
61
63
|
entityType: "tag",
|
|
@@ -101,6 +103,9 @@ export function updateTag(tagId, input, activity) {
|
|
|
101
103
|
SET name = ?, kind = ?, color = ?, description = ?
|
|
102
104
|
WHERE id = ?`)
|
|
103
105
|
.run(tag.name, tag.kind, tag.color, tag.description, tagId);
|
|
106
|
+
if (parsed.userId !== undefined) {
|
|
107
|
+
setEntityOwner("tag", tagId, parsed.userId);
|
|
108
|
+
}
|
|
104
109
|
if (activity) {
|
|
105
110
|
recordActivityEvent({
|
|
106
111
|
entityType: "tag",
|
|
@@ -55,6 +55,7 @@ function readExecutionConfig() {
|
|
|
55
55
|
}
|
|
56
56
|
function mapTaskRun(row, now = new Date(), cached = computeWorkTime(now)) {
|
|
57
57
|
const metric = cached.runMetrics.get(row.id);
|
|
58
|
+
const task = getTaskById(row.task_id);
|
|
58
59
|
return taskRunSchema.parse({
|
|
59
60
|
id: row.id,
|
|
60
61
|
taskId: row.task_id,
|
|
@@ -77,7 +78,9 @@ function mapTaskRun(row, now = new Date(), cached = computeWorkTime(now)) {
|
|
|
77
78
|
releasedAt: row.released_at,
|
|
78
79
|
timedOutAt: row.timed_out_at,
|
|
79
80
|
overrideReason: row.override_reason ?? null,
|
|
80
|
-
updatedAt: row.updated_at
|
|
81
|
+
updatedAt: row.updated_at,
|
|
82
|
+
userId: task?.userId ?? null,
|
|
83
|
+
user: task?.user ?? null
|
|
81
84
|
});
|
|
82
85
|
}
|
|
83
86
|
function getTaskRunRowById(taskRunId) {
|
|
@@ -284,7 +287,12 @@ export function listTaskRuns(filters = {}, now = new Date()) {
|
|
|
284
287
|
${limitSql}`)
|
|
285
288
|
.all(...params);
|
|
286
289
|
const cached = computeWorkTime(now);
|
|
287
|
-
|
|
290
|
+
const runs = rows.map((row) => mapTaskRun(row, now, cached));
|
|
291
|
+
if (!filters.userIds || filters.userIds.length === 0) {
|
|
292
|
+
return runs;
|
|
293
|
+
}
|
|
294
|
+
const allowed = new Set(filters.userIds);
|
|
295
|
+
return runs.filter((run) => run.userId !== null && allowed.has(run.userId));
|
|
288
296
|
});
|
|
289
297
|
}
|
|
290
298
|
export function claimTaskRun(taskId, input, now = new Date(), activity = { source: "ui" }) {
|