claude-memory-layer 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/commands/memory-forget.md +42 -0
- package/.claude-plugin/commands/memory-history.md +34 -0
- package/.claude-plugin/commands/memory-import.md +56 -0
- package/.claude-plugin/commands/memory-list.md +37 -0
- package/.claude-plugin/commands/memory-search.md +36 -0
- package/.claude-plugin/commands/memory-stats.md +34 -0
- package/.claude-plugin/hooks.json +59 -0
- package/.claude-plugin/plugin.json +24 -0
- package/.history/package_20260201112328.json +45 -0
- package/.history/package_20260201113602.json +45 -0
- package/.history/package_20260201113713.json +45 -0
- package/.history/package_20260201114110.json +45 -0
- package/Memo.txt +558 -0
- package/README.md +520 -0
- package/context.md +636 -0
- package/dist/.claude-plugin/commands/memory-forget.md +42 -0
- package/dist/.claude-plugin/commands/memory-history.md +34 -0
- package/dist/.claude-plugin/commands/memory-import.md +56 -0
- package/dist/.claude-plugin/commands/memory-list.md +37 -0
- package/dist/.claude-plugin/commands/memory-search.md +36 -0
- package/dist/.claude-plugin/commands/memory-stats.md +34 -0
- package/dist/.claude-plugin/hooks.json +59 -0
- package/dist/.claude-plugin/plugin.json +24 -0
- package/dist/cli/index.js +3539 -0
- package/dist/cli/index.js.map +7 -0
- package/dist/core/index.js +4408 -0
- package/dist/core/index.js.map +7 -0
- package/dist/hooks/session-end.js +2971 -0
- package/dist/hooks/session-end.js.map +7 -0
- package/dist/hooks/session-start.js +2969 -0
- package/dist/hooks/session-start.js.map +7 -0
- package/dist/hooks/stop.js +3123 -0
- package/dist/hooks/stop.js.map +7 -0
- package/dist/hooks/user-prompt-submit.js +2960 -0
- package/dist/hooks/user-prompt-submit.js.map +7 -0
- package/dist/services/memory-service.js +2931 -0
- package/dist/services/memory-service.js.map +7 -0
- package/package.json +45 -0
- package/plan.md +1642 -0
- package/scripts/build.ts +102 -0
- package/spec.md +624 -0
- package/specs/citations-system/context.md +243 -0
- package/specs/citations-system/plan.md +495 -0
- package/specs/citations-system/spec.md +371 -0
- package/specs/endless-mode/context.md +305 -0
- package/specs/endless-mode/plan.md +620 -0
- package/specs/endless-mode/spec.md +455 -0
- package/specs/entity-edge-model/context.md +401 -0
- package/specs/entity-edge-model/plan.md +459 -0
- package/specs/entity-edge-model/spec.md +391 -0
- package/specs/evidence-aligner-v2/context.md +401 -0
- package/specs/evidence-aligner-v2/plan.md +303 -0
- package/specs/evidence-aligner-v2/spec.md +312 -0
- package/specs/mcp-desktop-integration/context.md +278 -0
- package/specs/mcp-desktop-integration/plan.md +550 -0
- package/specs/mcp-desktop-integration/spec.md +494 -0
- package/specs/post-tool-use-hook/context.md +319 -0
- package/specs/post-tool-use-hook/plan.md +469 -0
- package/specs/post-tool-use-hook/spec.md +364 -0
- package/specs/private-tags/context.md +288 -0
- package/specs/private-tags/plan.md +412 -0
- package/specs/private-tags/spec.md +345 -0
- package/specs/progressive-disclosure/context.md +346 -0
- package/specs/progressive-disclosure/plan.md +663 -0
- package/specs/progressive-disclosure/spec.md +415 -0
- package/specs/task-entity-system/context.md +297 -0
- package/specs/task-entity-system/plan.md +301 -0
- package/specs/task-entity-system/spec.md +314 -0
- package/specs/vector-outbox-v2/context.md +470 -0
- package/specs/vector-outbox-v2/plan.md +562 -0
- package/specs/vector-outbox-v2/spec.md +466 -0
- package/specs/web-viewer-ui/context.md +384 -0
- package/specs/web-viewer-ui/plan.md +797 -0
- package/specs/web-viewer-ui/spec.md +516 -0
- package/src/cli/index.ts +570 -0
- package/src/core/canonical-key.ts +186 -0
- package/src/core/citation-generator.ts +63 -0
- package/src/core/consolidated-store.ts +279 -0
- package/src/core/consolidation-worker.ts +384 -0
- package/src/core/context-formatter.ts +276 -0
- package/src/core/continuity-manager.ts +336 -0
- package/src/core/edge-repo.ts +324 -0
- package/src/core/embedder.ts +124 -0
- package/src/core/entity-repo.ts +342 -0
- package/src/core/event-store.ts +672 -0
- package/src/core/evidence-aligner.ts +635 -0
- package/src/core/graduation.ts +365 -0
- package/src/core/index.ts +32 -0
- package/src/core/matcher.ts +210 -0
- package/src/core/metadata-extractor.ts +203 -0
- package/src/core/privacy/filter.ts +179 -0
- package/src/core/privacy/index.ts +20 -0
- package/src/core/privacy/tag-parser.ts +145 -0
- package/src/core/progressive-retriever.ts +415 -0
- package/src/core/retriever.ts +235 -0
- package/src/core/task/blocker-resolver.ts +325 -0
- package/src/core/task/index.ts +9 -0
- package/src/core/task/task-matcher.ts +238 -0
- package/src/core/task/task-projector.ts +345 -0
- package/src/core/task/task-resolver.ts +414 -0
- package/src/core/types.ts +841 -0
- package/src/core/vector-outbox.ts +295 -0
- package/src/core/vector-store.ts +182 -0
- package/src/core/vector-worker.ts +488 -0
- package/src/core/working-set-store.ts +244 -0
- package/src/hooks/post-tool-use.ts +127 -0
- package/src/hooks/session-end.ts +78 -0
- package/src/hooks/session-start.ts +57 -0
- package/src/hooks/stop.ts +78 -0
- package/src/hooks/user-prompt-submit.ts +54 -0
- package/src/mcp/handlers.ts +212 -0
- package/src/mcp/index.ts +47 -0
- package/src/mcp/tools.ts +78 -0
- package/src/server/api/citations.ts +101 -0
- package/src/server/api/events.ts +101 -0
- package/src/server/api/index.ts +18 -0
- package/src/server/api/search.ts +98 -0
- package/src/server/api/sessions.ts +111 -0
- package/src/server/api/stats.ts +97 -0
- package/src/server/index.ts +91 -0
- package/src/services/memory-service.ts +626 -0
- package/src/services/session-history-importer.ts +367 -0
- package/tests/canonical-key.test.ts +101 -0
- package/tests/evidence-aligner.test.ts +152 -0
- package/tests/matcher.test.ts +112 -0
- package/tsconfig.json +24 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local Embedding Generator using @xenova/transformers
|
|
3
|
+
* AXIOMMIND Principle 7: Standard JSON format for vectors
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { pipeline, Pipeline } from '@xenova/transformers';
|
|
7
|
+
|
|
8
|
+
export interface EmbeddingResult {
|
|
9
|
+
vector: number[];
|
|
10
|
+
model: string;
|
|
11
|
+
dimensions: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class Embedder {
|
|
15
|
+
private pipeline: Pipeline | null = null;
|
|
16
|
+
private readonly modelName: string;
|
|
17
|
+
private initialized = false;
|
|
18
|
+
|
|
19
|
+
constructor(modelName: string = 'Xenova/all-MiniLM-L6-v2') {
|
|
20
|
+
this.modelName = modelName;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Initialize the embedding pipeline
|
|
25
|
+
*/
|
|
26
|
+
async initialize(): Promise<void> {
|
|
27
|
+
if (this.initialized) return;
|
|
28
|
+
|
|
29
|
+
this.pipeline = await pipeline('feature-extraction', this.modelName);
|
|
30
|
+
this.initialized = true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Generate embedding for a single text
|
|
35
|
+
*/
|
|
36
|
+
async embed(text: string): Promise<EmbeddingResult> {
|
|
37
|
+
await this.initialize();
|
|
38
|
+
|
|
39
|
+
if (!this.pipeline) {
|
|
40
|
+
throw new Error('Embedding pipeline not initialized');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const output = await this.pipeline(text, {
|
|
44
|
+
pooling: 'mean',
|
|
45
|
+
normalize: true
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const vector = Array.from(output.data as Float32Array);
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
vector,
|
|
52
|
+
model: this.modelName,
|
|
53
|
+
dimensions: vector.length
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Generate embeddings for multiple texts in batch
|
|
59
|
+
*/
|
|
60
|
+
async embedBatch(texts: string[]): Promise<EmbeddingResult[]> {
|
|
61
|
+
await this.initialize();
|
|
62
|
+
|
|
63
|
+
if (!this.pipeline) {
|
|
64
|
+
throw new Error('Embedding pipeline not initialized');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const results: EmbeddingResult[] = [];
|
|
68
|
+
|
|
69
|
+
// Process in batches of 32 for memory efficiency
|
|
70
|
+
const batchSize = 32;
|
|
71
|
+
for (let i = 0; i < texts.length; i += batchSize) {
|
|
72
|
+
const batch = texts.slice(i, i + batchSize);
|
|
73
|
+
|
|
74
|
+
for (const text of batch) {
|
|
75
|
+
const output = await this.pipeline(text, {
|
|
76
|
+
pooling: 'mean',
|
|
77
|
+
normalize: true
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const vector = Array.from(output.data as Float32Array);
|
|
81
|
+
|
|
82
|
+
results.push({
|
|
83
|
+
vector,
|
|
84
|
+
model: this.modelName,
|
|
85
|
+
dimensions: vector.length
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return results;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get embedding dimensions for the current model
|
|
95
|
+
*/
|
|
96
|
+
async getDimensions(): Promise<number> {
|
|
97
|
+
const result = await this.embed('test');
|
|
98
|
+
return result.dimensions;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Check if embedder is ready
|
|
103
|
+
*/
|
|
104
|
+
isReady(): boolean {
|
|
105
|
+
return this.initialized && this.pipeline !== null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get model name
|
|
110
|
+
*/
|
|
111
|
+
getModelName(): string {
|
|
112
|
+
return this.modelName;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Singleton instance for reuse
|
|
117
|
+
let defaultEmbedder: Embedder | null = null;
|
|
118
|
+
|
|
119
|
+
export function getDefaultEmbedder(): Embedder {
|
|
120
|
+
if (!defaultEmbedder) {
|
|
121
|
+
defaultEmbedder = new Embedder();
|
|
122
|
+
}
|
|
123
|
+
return defaultEmbedder;
|
|
124
|
+
}
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entity Repository - CRUD operations for Task/Condition/Artifact entities
|
|
3
|
+
* AXIOMMIND Principle 5: Task is Entity
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Database } from 'duckdb';
|
|
7
|
+
import { randomUUID } from 'crypto';
|
|
8
|
+
import type {
|
|
9
|
+
Entity,
|
|
10
|
+
EntityType,
|
|
11
|
+
EntityStage,
|
|
12
|
+
EntityStatus,
|
|
13
|
+
EntityAlias,
|
|
14
|
+
TaskCurrentJson
|
|
15
|
+
} from './types.js';
|
|
16
|
+
import { makeEntityCanonicalKey } from './canonical-key.js';
|
|
17
|
+
|
|
18
|
+
export interface CreateEntityInput {
|
|
19
|
+
entityType: EntityType;
|
|
20
|
+
title: string;
|
|
21
|
+
currentJson: Record<string, unknown>;
|
|
22
|
+
project?: string;
|
|
23
|
+
stage?: EntityStage;
|
|
24
|
+
status?: EntityStatus;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface UpdateEntityInput {
|
|
28
|
+
currentJson?: Record<string, unknown>;
|
|
29
|
+
stage?: EntityStage;
|
|
30
|
+
status?: EntityStatus;
|
|
31
|
+
searchText?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class EntityRepo {
|
|
35
|
+
constructor(private db: Database) {}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create a new entity
|
|
39
|
+
*/
|
|
40
|
+
async create(input: CreateEntityInput): Promise<Entity> {
|
|
41
|
+
const entityId = randomUUID();
|
|
42
|
+
const canonicalKey = makeEntityCanonicalKey(input.entityType, input.title, {
|
|
43
|
+
project: input.project
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const titleNorm = input.title.toLowerCase().trim();
|
|
47
|
+
const searchText = `${input.title} ${JSON.stringify(input.currentJson)}`;
|
|
48
|
+
|
|
49
|
+
const now = new Date();
|
|
50
|
+
|
|
51
|
+
await this.db.run(
|
|
52
|
+
`INSERT INTO entities (
|
|
53
|
+
entity_id, entity_type, canonical_key, title, stage, status,
|
|
54
|
+
current_json, title_norm, search_text, created_at, updated_at
|
|
55
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
56
|
+
[
|
|
57
|
+
entityId,
|
|
58
|
+
input.entityType,
|
|
59
|
+
canonicalKey,
|
|
60
|
+
input.title,
|
|
61
|
+
input.stage ?? 'raw',
|
|
62
|
+
input.status ?? 'active',
|
|
63
|
+
JSON.stringify(input.currentJson),
|
|
64
|
+
titleNorm,
|
|
65
|
+
searchText,
|
|
66
|
+
now.toISOString(),
|
|
67
|
+
now.toISOString()
|
|
68
|
+
]
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// Create primary alias
|
|
72
|
+
await this.db.run(
|
|
73
|
+
`INSERT INTO entity_aliases (entity_type, canonical_key, entity_id, is_primary)
|
|
74
|
+
VALUES (?, ?, ?, TRUE)
|
|
75
|
+
ON CONFLICT (entity_type, canonical_key) DO NOTHING`,
|
|
76
|
+
[input.entityType, canonicalKey, entityId]
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
entityId,
|
|
81
|
+
entityType: input.entityType,
|
|
82
|
+
canonicalKey,
|
|
83
|
+
title: input.title,
|
|
84
|
+
stage: input.stage ?? 'raw',
|
|
85
|
+
status: input.status ?? 'active',
|
|
86
|
+
currentJson: input.currentJson,
|
|
87
|
+
titleNorm,
|
|
88
|
+
searchText,
|
|
89
|
+
createdAt: now,
|
|
90
|
+
updatedAt: now
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Find entity by ID
|
|
96
|
+
*/
|
|
97
|
+
async findById(entityId: string): Promise<Entity | null> {
|
|
98
|
+
const rows = await this.db.all<Array<Record<string, unknown>>>(
|
|
99
|
+
`SELECT * FROM entities WHERE entity_id = ?`,
|
|
100
|
+
[entityId]
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
if (rows.length === 0) return null;
|
|
104
|
+
return this.rowToEntity(rows[0]);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Find entity by canonical key
|
|
109
|
+
*/
|
|
110
|
+
async findByCanonicalKey(
|
|
111
|
+
entityType: EntityType,
|
|
112
|
+
canonicalKey: string
|
|
113
|
+
): Promise<Entity | null> {
|
|
114
|
+
const rows = await this.db.all<Array<Record<string, unknown>>>(
|
|
115
|
+
`SELECT * FROM entities
|
|
116
|
+
WHERE entity_type = ? AND canonical_key = ?`,
|
|
117
|
+
[entityType, canonicalKey]
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
if (rows.length === 0) return null;
|
|
121
|
+
return this.rowToEntity(rows[0]);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Find or create entity by title (idempotent)
|
|
126
|
+
*/
|
|
127
|
+
async findOrCreate(input: CreateEntityInput): Promise<{ entity: Entity; created: boolean }> {
|
|
128
|
+
const canonicalKey = makeEntityCanonicalKey(input.entityType, input.title, {
|
|
129
|
+
project: input.project
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const existing = await this.findByCanonicalKey(input.entityType, canonicalKey);
|
|
133
|
+
if (existing) {
|
|
134
|
+
return { entity: existing, created: false };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const entity = await this.create(input);
|
|
138
|
+
return { entity, created: true };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Update entity
|
|
143
|
+
*/
|
|
144
|
+
async update(entityId: string, input: UpdateEntityInput): Promise<Entity | null> {
|
|
145
|
+
const existing = await this.findById(entityId);
|
|
146
|
+
if (!existing) return null;
|
|
147
|
+
|
|
148
|
+
const updates: string[] = [];
|
|
149
|
+
const values: unknown[] = [];
|
|
150
|
+
|
|
151
|
+
if (input.currentJson !== undefined) {
|
|
152
|
+
updates.push('current_json = ?');
|
|
153
|
+
values.push(JSON.stringify(input.currentJson));
|
|
154
|
+
}
|
|
155
|
+
if (input.stage !== undefined) {
|
|
156
|
+
updates.push('stage = ?');
|
|
157
|
+
values.push(input.stage);
|
|
158
|
+
}
|
|
159
|
+
if (input.status !== undefined) {
|
|
160
|
+
updates.push('status = ?');
|
|
161
|
+
values.push(input.status);
|
|
162
|
+
}
|
|
163
|
+
if (input.searchText !== undefined) {
|
|
164
|
+
updates.push('search_text = ?');
|
|
165
|
+
values.push(input.searchText);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
updates.push('updated_at = ?');
|
|
169
|
+
values.push(new Date().toISOString());
|
|
170
|
+
|
|
171
|
+
values.push(entityId);
|
|
172
|
+
|
|
173
|
+
await this.db.run(
|
|
174
|
+
`UPDATE entities SET ${updates.join(', ')} WHERE entity_id = ?`,
|
|
175
|
+
values
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
return this.findById(entityId);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* List entities by type
|
|
183
|
+
*/
|
|
184
|
+
async listByType(
|
|
185
|
+
entityType: EntityType,
|
|
186
|
+
options?: { status?: EntityStatus; limit?: number; offset?: number }
|
|
187
|
+
): Promise<Entity[]> {
|
|
188
|
+
let query = `SELECT * FROM entities WHERE entity_type = ?`;
|
|
189
|
+
const params: unknown[] = [entityType];
|
|
190
|
+
|
|
191
|
+
if (options?.status) {
|
|
192
|
+
query += ` AND status = ?`;
|
|
193
|
+
params.push(options.status);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
query += ` ORDER BY updated_at DESC`;
|
|
197
|
+
|
|
198
|
+
if (options?.limit) {
|
|
199
|
+
query += ` LIMIT ?`;
|
|
200
|
+
params.push(options.limit);
|
|
201
|
+
}
|
|
202
|
+
if (options?.offset) {
|
|
203
|
+
query += ` OFFSET ?`;
|
|
204
|
+
params.push(options.offset);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const rows = await this.db.all<Array<Record<string, unknown>>>(query, params);
|
|
208
|
+
return rows.map(row => this.rowToEntity(row));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Search entities by text
|
|
213
|
+
*/
|
|
214
|
+
async search(
|
|
215
|
+
query: string,
|
|
216
|
+
options?: { entityType?: EntityType; limit?: number }
|
|
217
|
+
): Promise<Entity[]> {
|
|
218
|
+
const searchPattern = `%${query.toLowerCase()}%`;
|
|
219
|
+
|
|
220
|
+
let sql = `SELECT * FROM entities WHERE (title_norm LIKE ? OR search_text LIKE ?)`;
|
|
221
|
+
const params: unknown[] = [searchPattern, searchPattern];
|
|
222
|
+
|
|
223
|
+
if (options?.entityType) {
|
|
224
|
+
sql += ` AND entity_type = ?`;
|
|
225
|
+
params.push(options.entityType);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
sql += ` AND status = 'active' ORDER BY updated_at DESC`;
|
|
229
|
+
|
|
230
|
+
if (options?.limit) {
|
|
231
|
+
sql += ` LIMIT ?`;
|
|
232
|
+
params.push(options.limit);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const rows = await this.db.all<Array<Record<string, unknown>>>(sql, params);
|
|
236
|
+
return rows.map(row => this.rowToEntity(row));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Get tasks by status
|
|
241
|
+
*/
|
|
242
|
+
async getTasksByStatus(status: string): Promise<Entity[]> {
|
|
243
|
+
const rows = await this.db.all<Array<Record<string, unknown>>>(
|
|
244
|
+
`SELECT * FROM entities
|
|
245
|
+
WHERE entity_type = 'task'
|
|
246
|
+
AND json_extract(current_json, '$.status') = ?
|
|
247
|
+
AND status = 'active'
|
|
248
|
+
ORDER BY updated_at DESC`,
|
|
249
|
+
[status]
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
return rows.map(row => this.rowToEntity(row));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Get blocked tasks with their blockers
|
|
257
|
+
*/
|
|
258
|
+
async getBlockedTasksWithBlockers(): Promise<Array<{
|
|
259
|
+
task: Entity;
|
|
260
|
+
blockers: Array<{ entityId: string; entityType: string; title: string }>;
|
|
261
|
+
}>> {
|
|
262
|
+
const tasks = await this.getTasksByStatus('blocked');
|
|
263
|
+
|
|
264
|
+
const results: Array<{
|
|
265
|
+
task: Entity;
|
|
266
|
+
blockers: Array<{ entityId: string; entityType: string; title: string }>;
|
|
267
|
+
}> = [];
|
|
268
|
+
|
|
269
|
+
for (const task of tasks) {
|
|
270
|
+
const blockerEdges = await this.db.all<Array<Record<string, unknown>>>(
|
|
271
|
+
`SELECT e.dst_id, ent.entity_type, ent.title
|
|
272
|
+
FROM edges e
|
|
273
|
+
JOIN entities ent ON ent.entity_id = e.dst_id
|
|
274
|
+
WHERE e.src_id = ? AND e.rel_type = 'blocked_by'`,
|
|
275
|
+
[task.entityId]
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
results.push({
|
|
279
|
+
task,
|
|
280
|
+
blockers: blockerEdges.map(row => ({
|
|
281
|
+
entityId: row.dst_id as string,
|
|
282
|
+
entityType: row.entity_type as string,
|
|
283
|
+
title: row.title as string
|
|
284
|
+
}))
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return results;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Add alias for entity
|
|
293
|
+
*/
|
|
294
|
+
async addAlias(
|
|
295
|
+
entityType: EntityType,
|
|
296
|
+
canonicalKey: string,
|
|
297
|
+
entityId: string
|
|
298
|
+
): Promise<void> {
|
|
299
|
+
await this.db.run(
|
|
300
|
+
`INSERT INTO entity_aliases (entity_type, canonical_key, entity_id, is_primary)
|
|
301
|
+
VALUES (?, ?, ?, FALSE)
|
|
302
|
+
ON CONFLICT (entity_type, canonical_key) DO NOTHING`,
|
|
303
|
+
[entityType, canonicalKey, entityId]
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Find entity by alias
|
|
309
|
+
*/
|
|
310
|
+
async findByAlias(entityType: EntityType, canonicalKey: string): Promise<Entity | null> {
|
|
311
|
+
const rows = await this.db.all<Array<Record<string, unknown>>>(
|
|
312
|
+
`SELECT e.* FROM entities e
|
|
313
|
+
JOIN entity_aliases a ON e.entity_id = a.entity_id
|
|
314
|
+
WHERE a.entity_type = ? AND a.canonical_key = ?`,
|
|
315
|
+
[entityType, canonicalKey]
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
if (rows.length === 0) return null;
|
|
319
|
+
return this.rowToEntity(rows[0]);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Convert database row to Entity
|
|
324
|
+
*/
|
|
325
|
+
private rowToEntity(row: Record<string, unknown>): Entity {
|
|
326
|
+
return {
|
|
327
|
+
entityId: row.entity_id as string,
|
|
328
|
+
entityType: row.entity_type as EntityType,
|
|
329
|
+
canonicalKey: row.canonical_key as string,
|
|
330
|
+
title: row.title as string,
|
|
331
|
+
stage: row.stage as EntityStage,
|
|
332
|
+
status: row.status as EntityStatus,
|
|
333
|
+
currentJson: typeof row.current_json === 'string'
|
|
334
|
+
? JSON.parse(row.current_json)
|
|
335
|
+
: row.current_json as Record<string, unknown>,
|
|
336
|
+
titleNorm: row.title_norm as string | undefined,
|
|
337
|
+
searchText: row.search_text as string | undefined,
|
|
338
|
+
createdAt: new Date(row.created_at as string),
|
|
339
|
+
updatedAt: new Date(row.updated_at as string)
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
}
|