@stoneforge/quarry 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +13 -0
- package/README.md +160 -0
- package/dist/api/index.d.ts +8 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +8 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/quarry-api.d.ts +268 -0
- package/dist/api/quarry-api.d.ts.map +1 -0
- package/dist/api/quarry-api.js +3905 -0
- package/dist/api/quarry-api.js.map +1 -0
- package/dist/api/types.d.ts +1359 -0
- package/dist/api/types.d.ts.map +1 -0
- package/dist/api/types.js +204 -0
- package/dist/api/types.js.map +1 -0
- package/dist/bin/sf.d.ts +3 -0
- package/dist/bin/sf.d.ts.map +1 -0
- package/dist/bin/sf.js +9 -0
- package/dist/bin/sf.js.map +1 -0
- package/dist/cli/commands/admin.d.ts +11 -0
- package/dist/cli/commands/admin.d.ts.map +1 -0
- package/dist/cli/commands/admin.js +465 -0
- package/dist/cli/commands/admin.js.map +1 -0
- package/dist/cli/commands/alias.d.ts +8 -0
- package/dist/cli/commands/alias.d.ts.map +1 -0
- package/dist/cli/commands/alias.js +70 -0
- package/dist/cli/commands/alias.js.map +1 -0
- package/dist/cli/commands/channel.d.ts +13 -0
- package/dist/cli/commands/channel.d.ts.map +1 -0
- package/dist/cli/commands/channel.js +680 -0
- package/dist/cli/commands/channel.js.map +1 -0
- package/dist/cli/commands/completion.d.ts +8 -0
- package/dist/cli/commands/completion.d.ts.map +1 -0
- package/dist/cli/commands/completion.js +87 -0
- package/dist/cli/commands/completion.js.map +1 -0
- package/dist/cli/commands/config.d.ts +12 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +242 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/crud.d.ts +64 -0
- package/dist/cli/commands/crud.d.ts.map +1 -0
- package/dist/cli/commands/crud.js +805 -0
- package/dist/cli/commands/crud.js.map +1 -0
- package/dist/cli/commands/dep.d.ts +16 -0
- package/dist/cli/commands/dep.d.ts.map +1 -0
- package/dist/cli/commands/dep.js +499 -0
- package/dist/cli/commands/dep.js.map +1 -0
- package/dist/cli/commands/document.d.ts +12 -0
- package/dist/cli/commands/document.d.ts.map +1 -0
- package/dist/cli/commands/document.js +1039 -0
- package/dist/cli/commands/document.js.map +1 -0
- package/dist/cli/commands/embeddings.d.ts +12 -0
- package/dist/cli/commands/embeddings.d.ts.map +1 -0
- package/dist/cli/commands/embeddings.js +273 -0
- package/dist/cli/commands/embeddings.js.map +1 -0
- package/dist/cli/commands/entity.d.ts +16 -0
- package/dist/cli/commands/entity.d.ts.map +1 -0
- package/dist/cli/commands/entity.js +522 -0
- package/dist/cli/commands/entity.js.map +1 -0
- package/dist/cli/commands/gc.d.ts +10 -0
- package/dist/cli/commands/gc.d.ts.map +1 -0
- package/dist/cli/commands/gc.js +257 -0
- package/dist/cli/commands/gc.js.map +1 -0
- package/dist/cli/commands/help.d.ts +11 -0
- package/dist/cli/commands/help.d.ts.map +1 -0
- package/dist/cli/commands/help.js +169 -0
- package/dist/cli/commands/help.js.map +1 -0
- package/dist/cli/commands/history.d.ts +9 -0
- package/dist/cli/commands/history.d.ts.map +1 -0
- package/dist/cli/commands/history.js +160 -0
- package/dist/cli/commands/history.js.map +1 -0
- package/dist/cli/commands/identity.d.ts +18 -0
- package/dist/cli/commands/identity.d.ts.map +1 -0
- package/dist/cli/commands/identity.js +698 -0
- package/dist/cli/commands/identity.js.map +1 -0
- package/dist/cli/commands/inbox.d.ts +20 -0
- package/dist/cli/commands/inbox.d.ts.map +1 -0
- package/dist/cli/commands/inbox.js +493 -0
- package/dist/cli/commands/inbox.js.map +1 -0
- package/dist/cli/commands/init.d.ts +20 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +144 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/install.d.ts +9 -0
- package/dist/cli/commands/install.d.ts.map +1 -0
- package/dist/cli/commands/install.js +200 -0
- package/dist/cli/commands/install.js.map +1 -0
- package/dist/cli/commands/library.d.ts +12 -0
- package/dist/cli/commands/library.d.ts.map +1 -0
- package/dist/cli/commands/library.js +665 -0
- package/dist/cli/commands/library.js.map +1 -0
- package/dist/cli/commands/message.d.ts +11 -0
- package/dist/cli/commands/message.d.ts.map +1 -0
- package/dist/cli/commands/message.js +608 -0
- package/dist/cli/commands/message.js.map +1 -0
- package/dist/cli/commands/plan.d.ts +17 -0
- package/dist/cli/commands/plan.d.ts.map +1 -0
- package/dist/cli/commands/plan.js +698 -0
- package/dist/cli/commands/plan.js.map +1 -0
- package/dist/cli/commands/playbook.d.ts +12 -0
- package/dist/cli/commands/playbook.d.ts.map +1 -0
- package/dist/cli/commands/playbook.js +730 -0
- package/dist/cli/commands/playbook.js.map +1 -0
- package/dist/cli/commands/reset.d.ts +12 -0
- package/dist/cli/commands/reset.d.ts.map +1 -0
- package/dist/cli/commands/reset.js +306 -0
- package/dist/cli/commands/reset.js.map +1 -0
- package/dist/cli/commands/serve.d.ts +11 -0
- package/dist/cli/commands/serve.d.ts.map +1 -0
- package/dist/cli/commands/serve.js +106 -0
- package/dist/cli/commands/serve.js.map +1 -0
- package/dist/cli/commands/stats.d.ts +8 -0
- package/dist/cli/commands/stats.d.ts.map +1 -0
- package/dist/cli/commands/stats.js +82 -0
- package/dist/cli/commands/stats.js.map +1 -0
- package/dist/cli/commands/sync.d.ts +14 -0
- package/dist/cli/commands/sync.d.ts.map +1 -0
- package/dist/cli/commands/sync.js +370 -0
- package/dist/cli/commands/sync.js.map +1 -0
- package/dist/cli/commands/task.d.ts +25 -0
- package/dist/cli/commands/task.d.ts.map +1 -0
- package/dist/cli/commands/task.js +1153 -0
- package/dist/cli/commands/task.js.map +1 -0
- package/dist/cli/commands/team.d.ts +13 -0
- package/dist/cli/commands/team.d.ts.map +1 -0
- package/dist/cli/commands/team.js +471 -0
- package/dist/cli/commands/team.js.map +1 -0
- package/dist/cli/commands/workflow.d.ts +16 -0
- package/dist/cli/commands/workflow.d.ts.map +1 -0
- package/dist/cli/commands/workflow.js +753 -0
- package/dist/cli/commands/workflow.js.map +1 -0
- package/dist/cli/completion.d.ts +28 -0
- package/dist/cli/completion.d.ts.map +1 -0
- package/dist/cli/completion.js +295 -0
- package/dist/cli/completion.js.map +1 -0
- package/dist/cli/db.d.ts +38 -0
- package/dist/cli/db.d.ts.map +1 -0
- package/dist/cli/db.js +90 -0
- package/dist/cli/db.js.map +1 -0
- package/dist/cli/formatter.d.ts +87 -0
- package/dist/cli/formatter.d.ts.map +1 -0
- package/dist/cli/formatter.js +464 -0
- package/dist/cli/formatter.js.map +1 -0
- package/dist/cli/index.d.ts +33 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +38 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/parser.d.ts +45 -0
- package/dist/cli/parser.d.ts.map +1 -0
- package/dist/cli/parser.js +256 -0
- package/dist/cli/parser.js.map +1 -0
- package/dist/cli/plugin-loader.d.ts +39 -0
- package/dist/cli/plugin-loader.d.ts.map +1 -0
- package/dist/cli/plugin-loader.js +165 -0
- package/dist/cli/plugin-loader.js.map +1 -0
- package/dist/cli/plugin-registry.d.ts +50 -0
- package/dist/cli/plugin-registry.d.ts.map +1 -0
- package/dist/cli/plugin-registry.js +206 -0
- package/dist/cli/plugin-registry.js.map +1 -0
- package/dist/cli/plugin-types.d.ts +106 -0
- package/dist/cli/plugin-types.d.ts.map +1 -0
- package/dist/cli/plugin-types.js +103 -0
- package/dist/cli/plugin-types.js.map +1 -0
- package/dist/cli/runner.d.ts +35 -0
- package/dist/cli/runner.d.ts.map +1 -0
- package/dist/cli/runner.js +340 -0
- package/dist/cli/runner.js.map +1 -0
- package/dist/cli/suggest.d.ts +15 -0
- package/dist/cli/suggest.d.ts.map +1 -0
- package/dist/cli/suggest.js +49 -0
- package/dist/cli/suggest.js.map +1 -0
- package/dist/cli/types.d.ts +138 -0
- package/dist/cli/types.d.ts.map +1 -0
- package/dist/cli/types.js +63 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/config/config.d.ts +86 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +348 -0
- package/dist/config/config.js.map +1 -0
- package/dist/config/defaults.d.ts +66 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +114 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/duration.d.ts +75 -0
- package/dist/config/duration.d.ts.map +1 -0
- package/dist/config/duration.js +190 -0
- package/dist/config/duration.js.map +1 -0
- package/dist/config/env.d.ts +67 -0
- package/dist/config/env.d.ts.map +1 -0
- package/dist/config/env.js +207 -0
- package/dist/config/env.js.map +1 -0
- package/dist/config/file.d.ts +97 -0
- package/dist/config/file.d.ts.map +1 -0
- package/dist/config/file.js +365 -0
- package/dist/config/file.js.map +1 -0
- package/dist/config/index.d.ts +35 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +41 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/merge.d.ts +53 -0
- package/dist/config/merge.d.ts.map +1 -0
- package/dist/config/merge.js +226 -0
- package/dist/config/merge.js.map +1 -0
- package/dist/config/types.d.ts +257 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +72 -0
- package/dist/config/types.js.map +1 -0
- package/dist/config/validation.d.ts +55 -0
- package/dist/config/validation.d.ts.map +1 -0
- package/dist/config/validation.js +251 -0
- package/dist/config/validation.js.map +1 -0
- package/dist/http/index.d.ts +8 -0
- package/dist/http/index.d.ts.map +1 -0
- package/dist/http/index.js +12 -0
- package/dist/http/index.js.map +1 -0
- package/dist/http/sync-handlers.d.ts +162 -0
- package/dist/http/sync-handlers.d.ts.map +1 -0
- package/dist/http/sync-handlers.js +271 -0
- package/dist/http/sync-handlers.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +69 -0
- package/dist/index.js.map +1 -0
- package/dist/server/index.d.ts +34 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +3329 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/static.d.ts +18 -0
- package/dist/server/static.d.ts.map +1 -0
- package/dist/server/static.js +71 -0
- package/dist/server/static.js.map +1 -0
- package/dist/server/ws/broadcaster.d.ts +8 -0
- package/dist/server/ws/broadcaster.d.ts.map +1 -0
- package/dist/server/ws/broadcaster.js +7 -0
- package/dist/server/ws/broadcaster.js.map +1 -0
- package/dist/server/ws/handler.d.ts +55 -0
- package/dist/server/ws/handler.d.ts.map +1 -0
- package/dist/server/ws/handler.js +160 -0
- package/dist/server/ws/handler.js.map +1 -0
- package/dist/services/blocked-cache.d.ts +297 -0
- package/dist/services/blocked-cache.d.ts.map +1 -0
- package/dist/services/blocked-cache.js +755 -0
- package/dist/services/blocked-cache.js.map +1 -0
- package/dist/services/dependency.d.ts +205 -0
- package/dist/services/dependency.d.ts.map +1 -0
- package/dist/services/dependency.js +566 -0
- package/dist/services/dependency.js.map +1 -0
- package/dist/services/embeddings/fusion.d.ts +33 -0
- package/dist/services/embeddings/fusion.d.ts.map +1 -0
- package/dist/services/embeddings/fusion.js +34 -0
- package/dist/services/embeddings/fusion.js.map +1 -0
- package/dist/services/embeddings/index.d.ts +12 -0
- package/dist/services/embeddings/index.d.ts.map +1 -0
- package/dist/services/embeddings/index.js +10 -0
- package/dist/services/embeddings/index.js.map +1 -0
- package/dist/services/embeddings/local-provider.d.ts +31 -0
- package/dist/services/embeddings/local-provider.d.ts.map +1 -0
- package/dist/services/embeddings/local-provider.js +80 -0
- package/dist/services/embeddings/local-provider.js.map +1 -0
- package/dist/services/embeddings/service.d.ts +76 -0
- package/dist/services/embeddings/service.d.ts.map +1 -0
- package/dist/services/embeddings/service.js +153 -0
- package/dist/services/embeddings/service.js.map +1 -0
- package/dist/services/embeddings/types.d.ts +70 -0
- package/dist/services/embeddings/types.d.ts.map +1 -0
- package/dist/services/embeddings/types.js +8 -0
- package/dist/services/embeddings/types.js.map +1 -0
- package/dist/services/id-length-cache.d.ts +156 -0
- package/dist/services/id-length-cache.d.ts.map +1 -0
- package/dist/services/id-length-cache.js +197 -0
- package/dist/services/id-length-cache.js.map +1 -0
- package/dist/services/inbox.d.ts +147 -0
- package/dist/services/inbox.d.ts.map +1 -0
- package/dist/services/inbox.js +428 -0
- package/dist/services/inbox.js.map +1 -0
- package/dist/services/index.d.ts +10 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +10 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/priority-service.d.ts +145 -0
- package/dist/services/priority-service.d.ts.map +1 -0
- package/dist/services/priority-service.js +272 -0
- package/dist/services/priority-service.js.map +1 -0
- package/dist/services/search-utils.d.ts +47 -0
- package/dist/services/search-utils.d.ts.map +1 -0
- package/dist/services/search-utils.js +83 -0
- package/dist/services/search-utils.js.map +1 -0
- package/dist/sync/hash.d.ts +48 -0
- package/dist/sync/hash.d.ts.map +1 -0
- package/dist/sync/hash.js +136 -0
- package/dist/sync/hash.js.map +1 -0
- package/dist/sync/index.d.ts +11 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +16 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/merge.d.ts +80 -0
- package/dist/sync/merge.d.ts.map +1 -0
- package/dist/sync/merge.js +310 -0
- package/dist/sync/merge.js.map +1 -0
- package/dist/sync/serialization.d.ts +132 -0
- package/dist/sync/serialization.d.ts.map +1 -0
- package/dist/sync/serialization.js +306 -0
- package/dist/sync/serialization.js.map +1 -0
- package/dist/sync/service.d.ts +102 -0
- package/dist/sync/service.d.ts.map +1 -0
- package/dist/sync/service.js +493 -0
- package/dist/sync/service.js.map +1 -0
- package/dist/sync/types.d.ts +275 -0
- package/dist/sync/types.d.ts.map +1 -0
- package/dist/sync/types.js +76 -0
- package/dist/sync/types.js.map +1 -0
- package/dist/systems/identity.d.ts +479 -0
- package/dist/systems/identity.d.ts.map +1 -0
- package/dist/systems/identity.js +817 -0
- package/dist/systems/identity.js.map +1 -0
- package/dist/systems/index.d.ts +8 -0
- package/dist/systems/index.d.ts.map +1 -0
- package/dist/systems/index.js +29 -0
- package/dist/systems/index.js.map +1 -0
- package/package.json +121 -0
- package/web/assets/charts-vendor-D1YcbGux.js +55 -0
- package/web/assets/dnd-vendor-DmxE-_ZH.js +5 -0
- package/web/assets/editor-vendor-BxraAWts.js +279 -0
- package/web/assets/index-B77vv208.js +341 -0
- package/web/assets/index-CF_XnVLh.css +1 -0
- package/web/assets/router-vendor-BCKpRBrB.js +41 -0
- package/web/assets/ui-vendor-DUahGnbT.js +45 -0
- package/web/assets/utils-vendor-CfYKiENT.js +813 -0
- package/web/favicon.ico +0 -0
- package/web/index.html +23 -0
- package/web/logo.png +0 -0
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blocked Cache Service
|
|
3
|
+
*
|
|
4
|
+
* Maintains a materialized view of blocked elements for efficient ready-work queries.
|
|
5
|
+
* The blocked_cache table provides ~25x speedup for large datasets by avoiding
|
|
6
|
+
* recursive dependency checks on every query.
|
|
7
|
+
*
|
|
8
|
+
* Blocking Rules:
|
|
9
|
+
* - `blocks` dependency: Blocked element waits for blocker to close
|
|
10
|
+
* - `parent-child` dependency: Blocked element (child) inherits blocked state from blocker (parent) (transitive)
|
|
11
|
+
* - `awaits` dependency: Blocked element waits until gate is satisfied (timer, approval, etc.)
|
|
12
|
+
*
|
|
13
|
+
* Cache Invalidation Triggers:
|
|
14
|
+
* - Blocking dependency added/removed
|
|
15
|
+
* - Element status changes (especially closing)
|
|
16
|
+
* - Gate satisfaction changes
|
|
17
|
+
* - Parent blocking state changes
|
|
18
|
+
*/
|
|
19
|
+
import { DependencyType as DT, GateType, isValidAwaitsMetadata } from '@stoneforge/core';
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// Types
|
|
22
|
+
// ============================================================================
|
|
23
|
+
/**
|
|
24
|
+
* Status values that indicate an element is "done" and doesn't block others
|
|
25
|
+
*/
|
|
26
|
+
const COMPLETED_STATUSES = ['closed', 'completed', 'tombstone'];
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// BlockedCacheService Class
|
|
29
|
+
// ============================================================================
|
|
30
|
+
/**
|
|
31
|
+
* Service for managing the blocked elements cache
|
|
32
|
+
*/
|
|
33
|
+
export class BlockedCacheService {
|
|
34
|
+
db;
|
|
35
|
+
statusCallback;
|
|
36
|
+
constructor(db) {
|
|
37
|
+
this.db = db;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Set the callback for automatic status transitions
|
|
41
|
+
* This allows the service to notify when elements should be blocked/unblocked
|
|
42
|
+
*/
|
|
43
|
+
setStatusTransitionCallback(callback) {
|
|
44
|
+
this.statusCallback = callback;
|
|
45
|
+
}
|
|
46
|
+
// --------------------------------------------------------------------------
|
|
47
|
+
// Query Operations
|
|
48
|
+
// --------------------------------------------------------------------------
|
|
49
|
+
/**
|
|
50
|
+
* Check if an element is blocked
|
|
51
|
+
*
|
|
52
|
+
* @param elementId - Element to check
|
|
53
|
+
* @returns Blocking info if blocked, null if not blocked
|
|
54
|
+
*/
|
|
55
|
+
isBlocked(elementId) {
|
|
56
|
+
const row = this.db.queryOne('SELECT * FROM blocked_cache WHERE element_id = ?', [elementId]);
|
|
57
|
+
if (!row) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
elementId: row.element_id,
|
|
62
|
+
blockedBy: row.blocked_by,
|
|
63
|
+
reason: row.reason ?? 'Blocked by dependency',
|
|
64
|
+
previousStatus: row.previous_status,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get all blocked elements
|
|
69
|
+
*
|
|
70
|
+
* @returns Array of blocking info for all blocked elements
|
|
71
|
+
*/
|
|
72
|
+
getAllBlocked() {
|
|
73
|
+
const rows = this.db.query('SELECT * FROM blocked_cache');
|
|
74
|
+
return rows.map((row) => ({
|
|
75
|
+
elementId: row.element_id,
|
|
76
|
+
blockedBy: row.blocked_by,
|
|
77
|
+
reason: row.reason ?? 'Blocked by dependency',
|
|
78
|
+
previousStatus: row.previous_status,
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Get all elements blocked by a specific element
|
|
83
|
+
*
|
|
84
|
+
* @param blockerId - The element causing blocks
|
|
85
|
+
* @returns Array of element IDs blocked by this element
|
|
86
|
+
*/
|
|
87
|
+
getBlockedBy(blockerId) {
|
|
88
|
+
const rows = this.db.query('SELECT element_id FROM blocked_cache WHERE blocked_by = ?', [blockerId]);
|
|
89
|
+
return rows.map((row) => row.element_id);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Count blocked elements
|
|
93
|
+
*/
|
|
94
|
+
count() {
|
|
95
|
+
const row = this.db.queryOne('SELECT COUNT(*) as count FROM blocked_cache');
|
|
96
|
+
return row?.count ?? 0;
|
|
97
|
+
}
|
|
98
|
+
// --------------------------------------------------------------------------
|
|
99
|
+
// Cache Maintenance
|
|
100
|
+
// --------------------------------------------------------------------------
|
|
101
|
+
/**
|
|
102
|
+
* Add a blocking entry to the cache
|
|
103
|
+
*
|
|
104
|
+
* @param elementId - Element being blocked
|
|
105
|
+
* @param blockedBy - Element causing the block
|
|
106
|
+
* @param reason - Human-readable reason
|
|
107
|
+
* @param previousStatus - The element's status before becoming blocked
|
|
108
|
+
*/
|
|
109
|
+
addBlocked(elementId, blockedBy, reason, previousStatus) {
|
|
110
|
+
this.db.run(`INSERT OR REPLACE INTO blocked_cache (element_id, blocked_by, reason, previous_status)
|
|
111
|
+
VALUES (?, ?, ?, ?)`, [elementId, blockedBy, reason, previousStatus ?? null]);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Remove a blocking entry from the cache
|
|
115
|
+
*
|
|
116
|
+
* @param elementId - Element to unblock
|
|
117
|
+
*/
|
|
118
|
+
removeBlocked(elementId) {
|
|
119
|
+
this.db.run('DELETE FROM blocked_cache WHERE element_id = ?', [elementId]);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Clear all entries from the cache
|
|
123
|
+
*/
|
|
124
|
+
clear() {
|
|
125
|
+
this.db.run('DELETE FROM blocked_cache');
|
|
126
|
+
}
|
|
127
|
+
// --------------------------------------------------------------------------
|
|
128
|
+
// Blocking State Computation
|
|
129
|
+
// --------------------------------------------------------------------------
|
|
130
|
+
/**
|
|
131
|
+
* Check if an element is in a completed state (doesn't block others)
|
|
132
|
+
*
|
|
133
|
+
* @param elementId - Element to check
|
|
134
|
+
* @returns true if element is completed/closed/tombstone
|
|
135
|
+
*/
|
|
136
|
+
isElementCompleted(elementId) {
|
|
137
|
+
const row = this.db.queryOne('SELECT id, data, deleted_at FROM elements WHERE id = ?', [elementId]);
|
|
138
|
+
// Element doesn't exist - treat as non-blocking (external reference)
|
|
139
|
+
if (!row) {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
// Deleted (tombstone)
|
|
143
|
+
if (row.deleted_at) {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
// Check status in data
|
|
147
|
+
try {
|
|
148
|
+
const data = JSON.parse(row.data);
|
|
149
|
+
const status = data.status;
|
|
150
|
+
return COMPLETED_STATUSES.includes(status);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Check if a blocker element is in a completed state (no longer blocking)
|
|
158
|
+
*
|
|
159
|
+
* @param blockerId - The blocker element to check
|
|
160
|
+
* @returns true if blocker is completed/closed/tombstone (no longer blocking)
|
|
161
|
+
*/
|
|
162
|
+
isBlockerCompleted(blockerId) {
|
|
163
|
+
return this.isElementCompleted(blockerId);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Get the current status of an element
|
|
167
|
+
*
|
|
168
|
+
* @param elementId - Element to check
|
|
169
|
+
* @returns The element's status, or null if not found or no status
|
|
170
|
+
*/
|
|
171
|
+
getElementStatus(elementId) {
|
|
172
|
+
const row = this.db.queryOne('SELECT data FROM elements WHERE id = ?', [elementId]);
|
|
173
|
+
if (!row) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
const data = JSON.parse(row.data);
|
|
178
|
+
return data.status ?? null;
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Check if element is a task (has status field)
|
|
186
|
+
*/
|
|
187
|
+
isTask(elementId) {
|
|
188
|
+
const row = this.db.queryOne('SELECT type FROM elements WHERE id = ?', [elementId]);
|
|
189
|
+
return row?.type === 'task';
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Check if element is a plan
|
|
193
|
+
*/
|
|
194
|
+
isPlan(elementId) {
|
|
195
|
+
const row = this.db.queryOne('SELECT type FROM elements WHERE id = ?', [elementId]);
|
|
196
|
+
return row?.type === 'plan';
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Check if an awaits gate is satisfied
|
|
200
|
+
*
|
|
201
|
+
* @param metadata - The awaits dependency metadata
|
|
202
|
+
* @param options - Gate check options
|
|
203
|
+
* @returns true if gate is satisfied (not blocking)
|
|
204
|
+
*/
|
|
205
|
+
isGateSatisfied(metadata, options = {}) {
|
|
206
|
+
const now = options.currentTime ?? new Date();
|
|
207
|
+
switch (metadata.gateType) {
|
|
208
|
+
case GateType.TIMER:
|
|
209
|
+
// Timer gate: satisfied when current time >= waitUntil
|
|
210
|
+
const waitUntil = new Date(metadata.waitUntil);
|
|
211
|
+
return now >= waitUntil;
|
|
212
|
+
case GateType.APPROVAL:
|
|
213
|
+
// Approval gate: satisfied when enough approvers have approved
|
|
214
|
+
const required = metadata.approvalCount ?? metadata.requiredApprovers.length;
|
|
215
|
+
const current = metadata.currentApprovers?.length ?? 0;
|
|
216
|
+
return current >= required;
|
|
217
|
+
case GateType.EXTERNAL:
|
|
218
|
+
// External gates are satisfied when explicitly marked via API
|
|
219
|
+
return metadata.satisfied === true;
|
|
220
|
+
case GateType.WEBHOOK:
|
|
221
|
+
// Webhook gates are satisfied when explicitly marked via callback
|
|
222
|
+
return metadata.satisfied === true;
|
|
223
|
+
default:
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Compute the blocking state for a single element
|
|
229
|
+
*
|
|
230
|
+
* @param elementId - Element to check
|
|
231
|
+
* @param options - Gate check options
|
|
232
|
+
* @returns Blocking info if blocked, null if not blocked
|
|
233
|
+
*/
|
|
234
|
+
computeBlockingState(elementId, options = {}) {
|
|
235
|
+
// All blocking types now use consistent direction:
|
|
236
|
+
// blocked_id = element that is waiting, blocker_id = element doing the blocking
|
|
237
|
+
const blockingDeps = this.db.query(`SELECT * FROM dependencies
|
|
238
|
+
WHERE blocked_id = ? AND type IN (?, ?, ?)`, [elementId, DT.BLOCKS, DT.PARENT_CHILD, DT.AWAITS]);
|
|
239
|
+
for (const dep of blockingDeps) {
|
|
240
|
+
const blockerId = dep.blocker_id;
|
|
241
|
+
const type = dep.type;
|
|
242
|
+
switch (type) {
|
|
243
|
+
case DT.BLOCKS:
|
|
244
|
+
// Check if blocker is completed
|
|
245
|
+
if (!this.isBlockerCompleted(blockerId)) {
|
|
246
|
+
return {
|
|
247
|
+
elementId,
|
|
248
|
+
blockedBy: blockerId,
|
|
249
|
+
reason: `Blocked by ${blockerId} (blocks dependency)`,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
break;
|
|
253
|
+
case DT.PARENT_CHILD:
|
|
254
|
+
// Check if parent is blocked (transitive)
|
|
255
|
+
const parentBlocked = this.isBlocked(blockerId);
|
|
256
|
+
if (parentBlocked) {
|
|
257
|
+
return {
|
|
258
|
+
elementId,
|
|
259
|
+
blockedBy: blockerId,
|
|
260
|
+
reason: `Blocked by parent ${blockerId} (parent is blocked)`,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
// For task-task hierarchy: child is blocked until parent completes
|
|
264
|
+
// For task-plan hierarchy: tasks in a plan are NOT blocked by the plan's status
|
|
265
|
+
// Plans are collections, not blocking parents
|
|
266
|
+
if (!this.isPlan(blockerId) && !this.isBlockerCompleted(blockerId)) {
|
|
267
|
+
return {
|
|
268
|
+
elementId,
|
|
269
|
+
blockedBy: blockerId,
|
|
270
|
+
reason: `Blocked by parent ${blockerId} (parent not completed)`,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
break;
|
|
274
|
+
case DT.AWAITS:
|
|
275
|
+
// Check if gate is satisfied
|
|
276
|
+
if (dep.metadata) {
|
|
277
|
+
try {
|
|
278
|
+
const metadata = JSON.parse(dep.metadata);
|
|
279
|
+
if (isValidAwaitsMetadata(metadata)) {
|
|
280
|
+
if (!this.isGateSatisfied(metadata, options)) {
|
|
281
|
+
return {
|
|
282
|
+
elementId,
|
|
283
|
+
blockedBy: blockerId,
|
|
284
|
+
reason: `Blocked by gate (${metadata.gateType})`,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
// Invalid metadata, treat as blocking
|
|
291
|
+
return {
|
|
292
|
+
elementId,
|
|
293
|
+
blockedBy: blockerId,
|
|
294
|
+
reason: 'Blocked by gate (invalid metadata)',
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
// --------------------------------------------------------------------------
|
|
304
|
+
// Invalidation
|
|
305
|
+
// --------------------------------------------------------------------------
|
|
306
|
+
/**
|
|
307
|
+
* Update blocking state for an element after a dependency change
|
|
308
|
+
*
|
|
309
|
+
* Handles automatic status transitions:
|
|
310
|
+
* - When element becomes blocked: saves current status and triggers BLOCKED transition
|
|
311
|
+
* - When element becomes unblocked: restores previous status
|
|
312
|
+
*
|
|
313
|
+
* @param elementId - Element whose dependencies changed
|
|
314
|
+
* @param options - Gate check options
|
|
315
|
+
*/
|
|
316
|
+
invalidateElement(elementId, options = {}) {
|
|
317
|
+
// Check current cached state
|
|
318
|
+
const wasBlocked = this.isBlocked(elementId);
|
|
319
|
+
// Compute new blocking state
|
|
320
|
+
const shouldBeBlocked = this.computeBlockingState(elementId, options);
|
|
321
|
+
// Case 1: Becoming blocked (wasn't blocked, now should be)
|
|
322
|
+
if (!wasBlocked && shouldBeBlocked) {
|
|
323
|
+
// Only handle automatic status transitions for tasks
|
|
324
|
+
if (this.isTask(elementId) && this.statusCallback) {
|
|
325
|
+
const currentStatus = this.getElementStatus(elementId);
|
|
326
|
+
// Only trigger automatic block if not already in a terminal/deferred state
|
|
327
|
+
// and not already blocked
|
|
328
|
+
if (currentStatus && currentStatus !== 'blocked' &&
|
|
329
|
+
currentStatus !== 'closed' && currentStatus !== 'tombstone' &&
|
|
330
|
+
currentStatus !== 'deferred') {
|
|
331
|
+
this.addBlocked(shouldBeBlocked.elementId, shouldBeBlocked.blockedBy, shouldBeBlocked.reason, currentStatus);
|
|
332
|
+
this.statusCallback.onBlock(elementId, currentStatus);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
// Non-task or no callback: just update cache (no previous status to preserve)
|
|
337
|
+
this.addBlocked(shouldBeBlocked.elementId, shouldBeBlocked.blockedBy, shouldBeBlocked.reason, null);
|
|
338
|
+
}
|
|
339
|
+
// Case 2: Becoming unblocked (was blocked, now shouldn't be)
|
|
340
|
+
else if (wasBlocked && !shouldBeBlocked) {
|
|
341
|
+
// Get the status to restore
|
|
342
|
+
const statusToRestore = wasBlocked.previousStatus;
|
|
343
|
+
// Remove from cache first
|
|
344
|
+
this.removeBlocked(elementId);
|
|
345
|
+
// Only handle automatic status transitions for tasks
|
|
346
|
+
if (this.isTask(elementId) && this.statusCallback && statusToRestore) {
|
|
347
|
+
// Only restore if currently blocked
|
|
348
|
+
const currentStatus = this.getElementStatus(elementId);
|
|
349
|
+
if (currentStatus === 'blocked') {
|
|
350
|
+
this.statusCallback.onUnblock(elementId, statusToRestore);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
// Case 3: Still blocked but by different element (update cache)
|
|
355
|
+
else if (wasBlocked && shouldBeBlocked) {
|
|
356
|
+
// Keep the original previousStatus, just update blocker/reason
|
|
357
|
+
this.addBlocked(shouldBeBlocked.elementId, shouldBeBlocked.blockedBy, shouldBeBlocked.reason, wasBlocked.previousStatus);
|
|
358
|
+
}
|
|
359
|
+
// Case 4: Was not blocked, still not blocked - no action needed
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Update blocking state for all elements that depend on a changed element
|
|
363
|
+
* Called when an element's status changes (especially when completing)
|
|
364
|
+
*
|
|
365
|
+
* @param changedId - Element whose status changed
|
|
366
|
+
* @param options - Gate check options
|
|
367
|
+
*/
|
|
368
|
+
invalidateDependents(changedId, options = {}) {
|
|
369
|
+
// All blocking types: when a blocker changes, invalidate all blocked elements
|
|
370
|
+
const deps = this.db.query(`SELECT DISTINCT blocked_id, type FROM dependencies
|
|
371
|
+
WHERE blocker_id = ? AND type IN (?, ?, ?)`, [changedId, DT.BLOCKS, DT.PARENT_CHILD, DT.AWAITS]);
|
|
372
|
+
for (const dep of deps) {
|
|
373
|
+
const blockedId = dep.blocked_id;
|
|
374
|
+
this.invalidateElement(blockedId, options);
|
|
375
|
+
// For parent-child, also invalidate children (transitive)
|
|
376
|
+
if (dep.type === DT.PARENT_CHILD) {
|
|
377
|
+
this.invalidateChildren(blockedId, options);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Recursively invalidate children of an element
|
|
383
|
+
* Used for transitive parent-child blocking
|
|
384
|
+
*
|
|
385
|
+
* @param parentId - Parent element
|
|
386
|
+
* @param options - Gate check options
|
|
387
|
+
* @param visited - Set of already visited elements (cycle prevention)
|
|
388
|
+
*/
|
|
389
|
+
invalidateChildren(parentId, options = {}, visited = new Set()) {
|
|
390
|
+
if (visited.has(parentId)) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
visited.add(parentId);
|
|
394
|
+
// Find all elements that have this as a parent
|
|
395
|
+
const children = this.db.query(`SELECT blocked_id FROM dependencies
|
|
396
|
+
WHERE blocker_id = ? AND type = ?`, [parentId, DT.PARENT_CHILD]);
|
|
397
|
+
for (const child of children) {
|
|
398
|
+
const childId = child.blocked_id;
|
|
399
|
+
this.invalidateElement(childId, options);
|
|
400
|
+
this.invalidateChildren(childId, options, visited);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// --------------------------------------------------------------------------
|
|
404
|
+
// Gate Satisfaction
|
|
405
|
+
// --------------------------------------------------------------------------
|
|
406
|
+
/**
|
|
407
|
+
* Result of a gate satisfaction operation
|
|
408
|
+
*/
|
|
409
|
+
/**
|
|
410
|
+
* Mark an external or webhook gate as satisfied.
|
|
411
|
+
* Updates the dependency metadata and recomputes blocking state.
|
|
412
|
+
*
|
|
413
|
+
* @param blockedId - Element that has the awaits dependency
|
|
414
|
+
* @param blockerId - Blocker element ID of the awaits dependency
|
|
415
|
+
* @param actor - Entity marking the gate as satisfied
|
|
416
|
+
* @param options - Gate check options
|
|
417
|
+
* @returns True if gate was found and satisfied, false if not found
|
|
418
|
+
*/
|
|
419
|
+
satisfyGate(blockedId, blockerId, actor, options = {}) {
|
|
420
|
+
// Find the awaits dependency
|
|
421
|
+
const dep = this.db.queryOne(`SELECT * FROM dependencies WHERE blocked_id = ? AND blocker_id = ? AND type = ?`, [blockedId, blockerId, DT.AWAITS]);
|
|
422
|
+
if (!dep || !dep.metadata) {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
// Parse and validate metadata
|
|
426
|
+
let metadata;
|
|
427
|
+
try {
|
|
428
|
+
metadata = JSON.parse(dep.metadata);
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
// Check gate type - only external and webhook can be satisfied this way
|
|
434
|
+
const gateType = metadata.gateType;
|
|
435
|
+
if (gateType !== GateType.EXTERNAL && gateType !== GateType.WEBHOOK) {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
// Already satisfied?
|
|
439
|
+
if (metadata.satisfied === true) {
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
442
|
+
// Mark as satisfied
|
|
443
|
+
metadata.satisfied = true;
|
|
444
|
+
metadata.satisfiedAt = new Date().toISOString();
|
|
445
|
+
metadata.satisfiedBy = actor;
|
|
446
|
+
// Update the dependency metadata in the database
|
|
447
|
+
this.db.run(`UPDATE dependencies SET metadata = ? WHERE blocked_id = ? AND blocker_id = ? AND type = ?`, [JSON.stringify(metadata), blockedId, blockerId, DT.AWAITS]);
|
|
448
|
+
// Recompute blocking state for the blocked element
|
|
449
|
+
this.invalidateElement(blockedId, options);
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Record an approval for an approval gate.
|
|
454
|
+
* Updates the dependency metadata with the new approver.
|
|
455
|
+
*
|
|
456
|
+
* @param blockedId - Element that has the awaits dependency
|
|
457
|
+
* @param blockerId - Blocker element ID of the awaits dependency
|
|
458
|
+
* @param approver - Entity recording their approval
|
|
459
|
+
* @param options - Gate check options
|
|
460
|
+
* @returns Object indicating success and current approval count
|
|
461
|
+
*/
|
|
462
|
+
recordApproval(blockedId, blockerId, approver, options = {}) {
|
|
463
|
+
// Find the awaits dependency
|
|
464
|
+
const dep = this.db.queryOne(`SELECT * FROM dependencies WHERE blocked_id = ? AND blocker_id = ? AND type = ?`, [blockedId, blockerId, DT.AWAITS]);
|
|
465
|
+
if (!dep || !dep.metadata) {
|
|
466
|
+
return { success: false, currentCount: 0, requiredCount: 0, satisfied: false };
|
|
467
|
+
}
|
|
468
|
+
// Parse and validate metadata
|
|
469
|
+
let metadata;
|
|
470
|
+
try {
|
|
471
|
+
metadata = JSON.parse(dep.metadata);
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
return { success: false, currentCount: 0, requiredCount: 0, satisfied: false };
|
|
475
|
+
}
|
|
476
|
+
// Check gate type - only approval gates support this
|
|
477
|
+
if (metadata.gateType !== GateType.APPROVAL) {
|
|
478
|
+
return { success: false, currentCount: 0, requiredCount: 0, satisfied: false };
|
|
479
|
+
}
|
|
480
|
+
// Get required approvers and count
|
|
481
|
+
const requiredApprovers = metadata.requiredApprovers;
|
|
482
|
+
const requiredCount = metadata.approvalCount ?? requiredApprovers.length;
|
|
483
|
+
// Check if approver is in the required list
|
|
484
|
+
if (!requiredApprovers.includes(approver)) {
|
|
485
|
+
return { success: false, currentCount: 0, requiredCount, satisfied: false };
|
|
486
|
+
}
|
|
487
|
+
// Initialize or get current approvers
|
|
488
|
+
const currentApprovers = metadata.currentApprovers ?? [];
|
|
489
|
+
// Check if already approved
|
|
490
|
+
if (currentApprovers.includes(approver)) {
|
|
491
|
+
return {
|
|
492
|
+
success: true,
|
|
493
|
+
currentCount: currentApprovers.length,
|
|
494
|
+
requiredCount,
|
|
495
|
+
satisfied: currentApprovers.length >= requiredCount,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
// Add the approval
|
|
499
|
+
currentApprovers.push(approver);
|
|
500
|
+
metadata.currentApprovers = currentApprovers;
|
|
501
|
+
// Update the dependency metadata in the database
|
|
502
|
+
this.db.run(`UPDATE dependencies SET metadata = ? WHERE blocked_id = ? AND blocker_id = ? AND type = ?`, [JSON.stringify(metadata), blockedId, blockerId, DT.AWAITS]);
|
|
503
|
+
// Recompute blocking state for the blocked element
|
|
504
|
+
this.invalidateElement(blockedId, options);
|
|
505
|
+
const satisfied = currentApprovers.length >= requiredCount;
|
|
506
|
+
return {
|
|
507
|
+
success: true,
|
|
508
|
+
currentCount: currentApprovers.length,
|
|
509
|
+
requiredCount,
|
|
510
|
+
satisfied,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Remove an approval from an approval gate.
|
|
515
|
+
*
|
|
516
|
+
* @param blockedId - Element that has the awaits dependency
|
|
517
|
+
* @param blockerId - Blocker element ID of the awaits dependency
|
|
518
|
+
* @param approver - Entity removing their approval
|
|
519
|
+
* @param options - Gate check options
|
|
520
|
+
* @returns Object indicating success and current approval count
|
|
521
|
+
*/
|
|
522
|
+
removeApproval(blockedId, blockerId, approver, options = {}) {
|
|
523
|
+
// Find the awaits dependency
|
|
524
|
+
const dep = this.db.queryOne(`SELECT * FROM dependencies WHERE blocked_id = ? AND blocker_id = ? AND type = ?`, [blockedId, blockerId, DT.AWAITS]);
|
|
525
|
+
if (!dep || !dep.metadata) {
|
|
526
|
+
return { success: false, currentCount: 0, requiredCount: 0, satisfied: false };
|
|
527
|
+
}
|
|
528
|
+
// Parse and validate metadata
|
|
529
|
+
let metadata;
|
|
530
|
+
try {
|
|
531
|
+
metadata = JSON.parse(dep.metadata);
|
|
532
|
+
}
|
|
533
|
+
catch {
|
|
534
|
+
return { success: false, currentCount: 0, requiredCount: 0, satisfied: false };
|
|
535
|
+
}
|
|
536
|
+
// Check gate type
|
|
537
|
+
if (metadata.gateType !== GateType.APPROVAL) {
|
|
538
|
+
return { success: false, currentCount: 0, requiredCount: 0, satisfied: false };
|
|
539
|
+
}
|
|
540
|
+
// Get required approvers and count
|
|
541
|
+
const requiredApprovers = metadata.requiredApprovers;
|
|
542
|
+
const requiredCount = metadata.approvalCount ?? requiredApprovers.length;
|
|
543
|
+
// Get current approvers
|
|
544
|
+
const currentApprovers = metadata.currentApprovers ?? [];
|
|
545
|
+
// Check if approver is in the list
|
|
546
|
+
const index = currentApprovers.indexOf(approver);
|
|
547
|
+
if (index === -1) {
|
|
548
|
+
return {
|
|
549
|
+
success: true,
|
|
550
|
+
currentCount: currentApprovers.length,
|
|
551
|
+
requiredCount,
|
|
552
|
+
satisfied: currentApprovers.length >= requiredCount,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
// Remove the approval
|
|
556
|
+
currentApprovers.splice(index, 1);
|
|
557
|
+
metadata.currentApprovers = currentApprovers;
|
|
558
|
+
// Update the dependency metadata in the database
|
|
559
|
+
this.db.run(`UPDATE dependencies SET metadata = ? WHERE blocked_id = ? AND blocker_id = ? AND type = ?`, [JSON.stringify(metadata), blockedId, blockerId, DT.AWAITS]);
|
|
560
|
+
// Recompute blocking state for the blocked element
|
|
561
|
+
this.invalidateElement(blockedId, options);
|
|
562
|
+
const satisfied = currentApprovers.length >= requiredCount;
|
|
563
|
+
return {
|
|
564
|
+
success: true,
|
|
565
|
+
currentCount: currentApprovers.length,
|
|
566
|
+
requiredCount,
|
|
567
|
+
satisfied,
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
// --------------------------------------------------------------------------
|
|
571
|
+
// Full Rebuild
|
|
572
|
+
// --------------------------------------------------------------------------
|
|
573
|
+
/**
|
|
574
|
+
* Rebuild the entire blocked cache from scratch
|
|
575
|
+
*
|
|
576
|
+
* This is useful for:
|
|
577
|
+
* - Initial population after migration
|
|
578
|
+
* - Recovery from cache corruption
|
|
579
|
+
* - Periodic consistency checks
|
|
580
|
+
*
|
|
581
|
+
* The rebuild processes elements in topological order (parents before children)
|
|
582
|
+
* to ensure transitive blocking is computed correctly.
|
|
583
|
+
*
|
|
584
|
+
* @param options - Gate check options
|
|
585
|
+
* @returns Rebuild statistics
|
|
586
|
+
*/
|
|
587
|
+
rebuild(options = {}) {
|
|
588
|
+
const startTime = Date.now();
|
|
589
|
+
// Clear existing cache
|
|
590
|
+
this.clear();
|
|
591
|
+
// Get all elements that could potentially be blocked
|
|
592
|
+
// All blocking types use blocked_id as the waiting element
|
|
593
|
+
const blocksBlocked = this.db.query(`SELECT DISTINCT blocked_id as element_id FROM dependencies WHERE type = ?`, [DT.BLOCKS]);
|
|
594
|
+
const parentChildBlocked = this.db.query(`SELECT DISTINCT blocked_id as element_id FROM dependencies WHERE type = ?`, [DT.PARENT_CHILD]);
|
|
595
|
+
const awaitsBlocked = this.db.query(`SELECT DISTINCT blocked_id as element_id FROM dependencies WHERE type = ?`, [DT.AWAITS]);
|
|
596
|
+
// Combine all potentially blocked elements
|
|
597
|
+
const allElements = new Set();
|
|
598
|
+
for (const e of blocksBlocked)
|
|
599
|
+
allElements.add(e.element_id);
|
|
600
|
+
for (const e of parentChildBlocked)
|
|
601
|
+
allElements.add(e.element_id);
|
|
602
|
+
for (const e of awaitsBlocked)
|
|
603
|
+
allElements.add(e.element_id);
|
|
604
|
+
let elementsChecked = 0;
|
|
605
|
+
let elementsBlocked = 0;
|
|
606
|
+
// Process elements in dependency order (BFS from roots)
|
|
607
|
+
// This ensures parents are processed before children for transitive blocking
|
|
608
|
+
const processed = new Set();
|
|
609
|
+
const queue = [];
|
|
610
|
+
// First pass: Find parent relationships for topological ordering
|
|
611
|
+
const parentOf = new Map(); // child -> parents
|
|
612
|
+
for (const elementId of allElements) {
|
|
613
|
+
const parents = this.db.query(`SELECT blocker_id FROM dependencies
|
|
614
|
+
WHERE blocked_id = ? AND type = ?`, [elementId, DT.PARENT_CHILD]);
|
|
615
|
+
parentOf.set(elementId, parents.map((p) => p.blocker_id).filter((p) => allElements.has(p)));
|
|
616
|
+
}
|
|
617
|
+
// Start with elements that have no parents in our set
|
|
618
|
+
for (const elementId of allElements) {
|
|
619
|
+
const parents = parentOf.get(elementId) ?? [];
|
|
620
|
+
if (parents.length === 0) {
|
|
621
|
+
queue.push(elementId);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
// Process in order
|
|
625
|
+
while (queue.length > 0) {
|
|
626
|
+
const elementId = queue.shift();
|
|
627
|
+
if (processed.has(elementId)) {
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
// Check if all parents are processed
|
|
631
|
+
const parents = parentOf.get(elementId) ?? [];
|
|
632
|
+
const allParentsProcessed = parents.every((p) => processed.has(p));
|
|
633
|
+
if (!allParentsProcessed) {
|
|
634
|
+
// Put back in queue for later
|
|
635
|
+
queue.push(elementId);
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
processed.add(elementId);
|
|
639
|
+
elementsChecked++;
|
|
640
|
+
// Compute blocking state
|
|
641
|
+
const blocking = this.computeBlockingState(elementId, options);
|
|
642
|
+
if (blocking) {
|
|
643
|
+
this.addBlocked(blocking.elementId, blocking.blockedBy, blocking.reason);
|
|
644
|
+
elementsBlocked++;
|
|
645
|
+
}
|
|
646
|
+
// Add children to queue
|
|
647
|
+
const children = this.db.query(`SELECT blocked_id FROM dependencies
|
|
648
|
+
WHERE blocker_id = ? AND type = ?`, [elementId, DT.PARENT_CHILD]);
|
|
649
|
+
for (const child of children) {
|
|
650
|
+
if (!processed.has(child.blocked_id)) {
|
|
651
|
+
queue.push(child.blocked_id);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
// Handle any remaining elements (shouldn't happen if graph is consistent)
|
|
656
|
+
for (const elementId of allElements) {
|
|
657
|
+
if (!processed.has(elementId)) {
|
|
658
|
+
elementsChecked++;
|
|
659
|
+
const blocking = this.computeBlockingState(elementId, options);
|
|
660
|
+
if (blocking) {
|
|
661
|
+
this.addBlocked(blocking.elementId, blocking.blockedBy, blocking.reason);
|
|
662
|
+
elementsBlocked++;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
const durationMs = Date.now() - startTime;
|
|
667
|
+
return {
|
|
668
|
+
elementsChecked,
|
|
669
|
+
elementsBlocked,
|
|
670
|
+
durationMs,
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
// --------------------------------------------------------------------------
|
|
674
|
+
// Dependency Event Handlers
|
|
675
|
+
// --------------------------------------------------------------------------
|
|
676
|
+
/**
|
|
677
|
+
* Handle a blocking dependency being added
|
|
678
|
+
*
|
|
679
|
+
* @param blockedId - Element that is waiting/blocked
|
|
680
|
+
* @param blockerId - Element doing the blocking
|
|
681
|
+
* @param type - Type of dependency
|
|
682
|
+
* @param metadata - Dependency metadata (for awaits)
|
|
683
|
+
* @param options - Gate check options
|
|
684
|
+
*/
|
|
685
|
+
onDependencyAdded(blockedId, blockerId, type, _metadata, options = {}) {
|
|
686
|
+
// Only handle blocking dependency types
|
|
687
|
+
if (type !== DT.BLOCKS && type !== DT.PARENT_CHILD && type !== DT.AWAITS) {
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
// All blocking types: blockedId is the waiting element
|
|
691
|
+
this.invalidateElement(blockedId, options);
|
|
692
|
+
// For parent-child, also invalidate all children (transitive)
|
|
693
|
+
if (type === DT.PARENT_CHILD) {
|
|
694
|
+
this.invalidateChildren(blockedId, options);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Handle a blocking dependency being removed
|
|
699
|
+
*
|
|
700
|
+
* @param blockedId - Element that had a blocking dependency removed
|
|
701
|
+
* @param blockerId - Element that was doing the blocking
|
|
702
|
+
* @param type - Type of dependency that was removed
|
|
703
|
+
* @param options - Gate check options
|
|
704
|
+
*/
|
|
705
|
+
onDependencyRemoved(blockedId, blockerId, type, options = {}) {
|
|
706
|
+
// Only handle blocking dependency types
|
|
707
|
+
if (type !== DT.BLOCKS && type !== DT.PARENT_CHILD && type !== DT.AWAITS) {
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
// All blocking types: blockedId is the waiting element
|
|
711
|
+
this.invalidateElement(blockedId, options);
|
|
712
|
+
// For parent-child, also invalidate all children
|
|
713
|
+
if (type === DT.PARENT_CHILD) {
|
|
714
|
+
this.invalidateChildren(blockedId, options);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Handle an element's status changing
|
|
719
|
+
*
|
|
720
|
+
* @param elementId - Element whose status changed
|
|
721
|
+
* @param oldStatus - Previous status
|
|
722
|
+
* @param newStatus - New status
|
|
723
|
+
* @param options - Gate check options
|
|
724
|
+
*/
|
|
725
|
+
onStatusChanged(elementId, oldStatus, newStatus, options = {}) {
|
|
726
|
+
const wasCompleted = COMPLETED_STATUSES.includes(oldStatus);
|
|
727
|
+
const isNowCompleted = COMPLETED_STATUSES.includes(newStatus);
|
|
728
|
+
// If completion status changed, invalidate all dependents
|
|
729
|
+
if (wasCompleted !== isNowCompleted) {
|
|
730
|
+
this.invalidateDependents(elementId, options);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Handle an element being deleted
|
|
735
|
+
*
|
|
736
|
+
* @param elementId - Element that was deleted
|
|
737
|
+
* @param options - Gate check options
|
|
738
|
+
*/
|
|
739
|
+
onElementDeleted(elementId, options = {}) {
|
|
740
|
+
// Remove from cache if blocked
|
|
741
|
+
this.removeBlocked(elementId);
|
|
742
|
+
// Invalidate dependents (deletion is like completion)
|
|
743
|
+
this.invalidateDependents(elementId, options);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
// ============================================================================
|
|
747
|
+
// Factory Function
|
|
748
|
+
// ============================================================================
|
|
749
|
+
/**
|
|
750
|
+
* Create a new BlockedCacheService instance
|
|
751
|
+
*/
|
|
752
|
+
export function createBlockedCacheService(db) {
|
|
753
|
+
return new BlockedCacheService(db);
|
|
754
|
+
}
|
|
755
|
+
//# sourceMappingURL=blocked-cache.js.map
|