memorix 1.1.7 → 1.1.8
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/CHANGELOG.md +13 -0
- package/CLAUDE.md +6 -1
- package/README.md +21 -0
- package/README.zh-CN.md +21 -0
- package/TEAM.md +86 -86
- package/dist/cli/index.js +730 -150
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/static/index.html +201 -201
- package/dist/dashboard/static/style.css +3584 -3584
- package/dist/index.js +15 -8
- package/dist/index.js.map +1 -1
- package/dist/memcode-runtime/CHANGELOG.md +13 -0
- package/dist/memcode-runtime/package.json +4 -4
- package/dist/sdk.js +15 -8
- package/dist/sdk.js.map +1 -1
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +18 -0
- package/docs/DESIGN_DECISIONS.md +357 -357
- package/docs/SETUP.md +10 -0
- package/docs/dev-log/progress.txt +35 -26
- package/package.json +1 -1
- package/src/audit/index.ts +156 -156
- package/src/cli/commands/agent-integrations.ts +623 -0
- package/src/cli/commands/audit-list.ts +89 -89
- package/src/cli/commands/background.ts +659 -659
- package/src/cli/commands/cleanup.ts +255 -255
- package/src/cli/commands/doctor.ts +23 -0
- package/src/cli/commands/formation.ts +48 -48
- package/src/cli/commands/git-hook-install.ts +111 -111
- package/src/cli/commands/handoff.ts +66 -66
- package/src/cli/commands/hooks-status.ts +63 -63
- package/src/cli/commands/ingest-commit.ts +153 -153
- package/src/cli/commands/ingest-image.ts +73 -73
- package/src/cli/commands/ingest-log.ts +180 -180
- package/src/cli/commands/ingest.ts +44 -44
- package/src/cli/commands/integrate-shared.ts +15 -15
- package/src/cli/commands/lock.ts +96 -96
- package/src/cli/commands/message.ts +121 -121
- package/src/cli/commands/poll.ts +70 -70
- package/src/cli/commands/purge-all-memory.ts +85 -85
- package/src/cli/commands/purge-project-memory.ts +83 -83
- package/src/cli/commands/reasoning.ts +132 -132
- package/src/cli/commands/repair.ts +60 -0
- package/src/cli/commands/retention.ts +108 -108
- package/src/cli/commands/serve-shared.ts +118 -118
- package/src/cli/commands/setup.ts +3 -3
- package/src/cli/commands/skills.ts +123 -123
- package/src/cli/commands/task.ts +192 -192
- package/src/cli/commands/transfer.ts +73 -73
- package/src/cli/commands/uninstall-project-artifacts.ts +85 -85
- package/src/cli/index.ts +3 -1
- package/src/cli/tui/ChatView.tsx +234 -234
- package/src/cli/tui/CommandBar.tsx +312 -312
- package/src/cli/tui/ContextRail.tsx +118 -118
- package/src/cli/tui/HeaderBar.tsx +72 -72
- package/src/cli/tui/LogoBanner.tsx +51 -51
- package/src/cli/tui/Panels.tsx +632 -632
- package/src/cli/tui/Sidebar.tsx +179 -179
- package/src/cli/tui/chat-service.ts +742 -742
- package/src/cli/tui/data.ts +547 -547
- package/src/cli/tui/index.ts +41 -41
- package/src/cli/tui/markdown-render.tsx +371 -371
- package/src/cli/tui/theme.ts +178 -178
- package/src/cli/tui/use-mouse.ts +157 -157
- package/src/cli/tui/useNavigation.ts +56 -56
- package/src/cli/update-checker.ts +211 -211
- package/src/cli/version.ts +7 -7
- package/src/cli/workbench.ts +1 -1
- package/src/codegraph/context-pack.ts +5 -1
- package/src/codegraph/lite-provider.ts +5 -1
- package/src/codegraph/project-context.ts +5 -1
- package/src/compact/token-budget.ts +74 -74
- package/src/config/behavior.ts +59 -59
- package/src/dashboard/project-classification.ts +64 -64
- package/src/dashboard/static/index.html +201 -201
- package/src/dashboard/static/style.css +3584 -3584
- package/src/embedding/fastembed-provider.ts +142 -142
- package/src/embedding/transformers-provider.ts +111 -111
- package/src/git/extractor.ts +209 -209
- package/src/git/hooks-path.ts +85 -85
- package/src/git/noise-filter.ts +210 -210
- package/src/hooks/installers/index.ts +4 -4
- package/src/hooks/official-skills.ts +1 -1
- package/src/hooks/pattern-detector.ts +173 -173
- package/src/hooks/rules/memorix-agent-rules.md +2 -2
- package/src/hooks/significance-filter.ts +250 -250
- package/src/llm/memory-manager.ts +328 -328
- package/src/llm/provider.ts +885 -885
- package/src/llm/quality.ts +248 -248
- package/src/memory/attribution-guard.ts +249 -249
- package/src/memory/auto-relations.ts +107 -107
- package/src/memory/consolidation.ts +302 -302
- package/src/memory/disclosure-policy.ts +141 -141
- package/src/memory/entity-extractor.ts +197 -197
- package/src/memory/formation/evaluate.ts +217 -217
- package/src/memory/formation/extract.ts +361 -361
- package/src/memory/formation/index.ts +417 -417
- package/src/memory/formation/resolve.ts +344 -344
- package/src/memory/formation/types.ts +315 -315
- package/src/memory/freshness.ts +122 -122
- package/src/memory/graph.ts +197 -197
- package/src/memory/refs.ts +94 -94
- package/src/memory/retention.ts +433 -433
- package/src/memory/secret-filter.ts +79 -79
- package/src/memory/session.ts +523 -523
- package/src/multimodal/image-loader.ts +143 -143
- package/src/orchestrate/adapters/claude-stream.ts +192 -192
- package/src/orchestrate/adapters/claude.ts +111 -111
- package/src/orchestrate/adapters/codex-stream.ts +134 -134
- package/src/orchestrate/adapters/codex.ts +41 -41
- package/src/orchestrate/adapters/gemini-stream.ts +166 -166
- package/src/orchestrate/adapters/gemini.ts +42 -42
- package/src/orchestrate/adapters/index.ts +73 -73
- package/src/orchestrate/adapters/opencode-stream.ts +143 -143
- package/src/orchestrate/adapters/opencode.ts +47 -47
- package/src/orchestrate/adapters/spawn-helper.ts +286 -286
- package/src/orchestrate/adapters/types.ts +77 -77
- package/src/orchestrate/capability-router.ts +284 -284
- package/src/orchestrate/context-compact.ts +188 -188
- package/src/orchestrate/cost-tracker.ts +219 -219
- package/src/orchestrate/error-recovery.ts +191 -191
- package/src/orchestrate/evidence.ts +140 -140
- package/src/orchestrate/ledger.ts +110 -110
- package/src/orchestrate/memorix-bridge.ts +380 -380
- package/src/orchestrate/output-budget.ts +80 -80
- package/src/orchestrate/permission.ts +152 -152
- package/src/orchestrate/pipeline-trace.ts +131 -131
- package/src/orchestrate/prompt-builder.ts +155 -155
- package/src/orchestrate/ring-buffer.ts +37 -37
- package/src/orchestrate/task-graph.ts +389 -389
- package/src/orchestrate/verify-gate.ts +219 -219
- package/src/orchestrate/worktree.ts +232 -232
- package/src/project/aliases.ts +374 -374
- package/src/project/detector.ts +268 -268
- package/src/rules/adapters/claude-code.ts +99 -99
- package/src/rules/adapters/codex.ts +97 -97
- package/src/rules/adapters/copilot.ts +124 -124
- package/src/rules/adapters/cursor.ts +114 -114
- package/src/rules/adapters/kiro.ts +126 -126
- package/src/rules/adapters/trae.ts +56 -56
- package/src/rules/adapters/windsurf.ts +83 -83
- package/src/rules/syncer.ts +235 -235
- package/src/sdk.ts +327 -327
- package/src/search/intent-detector.ts +289 -289
- package/src/search/query-expansion.ts +52 -52
- package/src/server/formation-timeout.ts +27 -27
- package/src/skills/mini-skills.ts +386 -386
- package/src/store/chat-store.ts +119 -119
- package/src/store/file-lock.ts +100 -100
- package/src/store/graph-store.ts +249 -249
- package/src/store/mini-skill-store.ts +349 -349
- package/src/store/obs-store.ts +255 -255
- package/src/store/persistence-json.ts +212 -212
- package/src/store/persistence.ts +291 -291
- package/src/store/project-affinity.ts +195 -195
- package/src/store/session-store.ts +259 -259
- package/src/store/sqlite-store.ts +339 -339
- package/src/team/event-bus.ts +76 -76
- package/src/team/file-locks.ts +173 -173
- package/src/team/handoff.ts +167 -167
- package/src/team/messages.ts +203 -203
- package/src/team/poll.ts +132 -132
- package/src/team/tasks.ts +211 -211
- package/src/wiki/generator.ts +237 -237
- package/src/wiki/knowledge-graph.ts +334 -334
- package/src/wiki/types.ts +85 -85
- package/src/workspace/mcp-adapters/codex.ts +191 -191
- package/src/workspace/mcp-adapters/copilot.ts +105 -105
- package/src/workspace/mcp-adapters/cursor.ts +53 -53
- package/src/workspace/mcp-adapters/kiro.ts +64 -64
- package/src/workspace/mcp-adapters/opencode.ts +123 -123
- package/src/workspace/mcp-adapters/trae.ts +134 -134
- package/src/workspace/mcp-adapters/windsurf.ts +91 -91
- package/src/workspace/sanitizer.ts +60 -60
- package/src/workspace/workflow-sync.ts +131 -131
|
@@ -1,334 +1,334 @@
|
|
|
1
|
-
import type { Observation, MiniSkill } from '../types.js';
|
|
2
|
-
import type {
|
|
3
|
-
KnowledgeSourceRef,
|
|
4
|
-
SemanticNode,
|
|
5
|
-
SemanticEdge,
|
|
6
|
-
SemanticEdgeType,
|
|
7
|
-
KnowledgeGraphCluster,
|
|
8
|
-
KnowledgeGraphStats,
|
|
9
|
-
ProjectKnowledgeGraph,
|
|
10
|
-
} from './types.js';
|
|
11
|
-
import { isEligible } from './generator.js';
|
|
12
|
-
|
|
13
|
-
// -- Section definitions (must match generator.ts SECTION_DEFS) --
|
|
14
|
-
|
|
15
|
-
interface SectionDef {
|
|
16
|
-
id: string;
|
|
17
|
-
label: string;
|
|
18
|
-
typeMatch: (o: Observation) => boolean;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const SECTION_DEFS: SectionDef[] = [
|
|
22
|
-
{
|
|
23
|
-
id: 'core-decisions',
|
|
24
|
-
label: 'Core Decisions',
|
|
25
|
-
typeMatch: (o) => o.type === 'decision' || o.type === 'trade-off' || o.type === 'reasoning',
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
id: 'operational-knowledge',
|
|
29
|
-
label: 'Operational Knowledge',
|
|
30
|
-
typeMatch: (o) =>
|
|
31
|
-
o.type === 'how-it-works' ||
|
|
32
|
-
o.type === 'what-changed' ||
|
|
33
|
-
o.type === 'why-it-exists' ||
|
|
34
|
-
o.type === 'discovery' ||
|
|
35
|
-
o.type === 'session-request',
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
id: 'known-gotchas',
|
|
39
|
-
label: 'Known Gotchas',
|
|
40
|
-
typeMatch: (o) => o.type === 'gotcha' || o.type === 'problem-solution',
|
|
41
|
-
},
|
|
42
|
-
];
|
|
43
|
-
|
|
44
|
-
const GIT_SECTION: SectionDef = {
|
|
45
|
-
id: 'git-backed-facts',
|
|
46
|
-
label: 'Git-backed Facts',
|
|
47
|
-
typeMatch: (o) => o.source === 'git' && o.sourceDetail === 'git-ingest',
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const SKILLS_SECTION: SectionDef = {
|
|
51
|
-
id: 'promoted-skills',
|
|
52
|
-
label: 'Promoted Skills',
|
|
53
|
-
typeMatch: () => false, // skills handled separately
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const ALL_SECTIONS: SectionDef[] = [...SECTION_DEFS, GIT_SECTION, SKILLS_SECTION];
|
|
57
|
-
|
|
58
|
-
// -- Edge inference rules --
|
|
59
|
-
// Infer semantic edges between observations based on shared attributes.
|
|
60
|
-
// Rules (no LLM, deterministic):
|
|
61
|
-
// 1. supports: same entityName, different section -> source supports target
|
|
62
|
-
// 2. relates_to: same entityName, same section -> bidirectional
|
|
63
|
-
// 3. derived_from: mini-skill source obs -> skill node
|
|
64
|
-
// Explicit graph-store references are merged later as mentions edges.
|
|
65
|
-
|
|
66
|
-
function inferEdges(
|
|
67
|
-
nodes: SemanticNode[],
|
|
68
|
-
entityNameIndex: Map<string, SemanticNode[]>,
|
|
69
|
-
): SemanticEdge[] {
|
|
70
|
-
const edges: SemanticEdge[] = [];
|
|
71
|
-
const seen = new Set<string>();
|
|
72
|
-
const maxRelatesEdgesPerEntitySection = 8;
|
|
73
|
-
|
|
74
|
-
function addEdge(source: string, target: string, edgeType: SemanticEdgeType): void {
|
|
75
|
-
const key = `${source}:${edgeType}:${target}`;
|
|
76
|
-
if (seen.has(key)) return;
|
|
77
|
-
if (source === target) return;
|
|
78
|
-
seen.add(key);
|
|
79
|
-
edges.push({
|
|
80
|
-
id: `e_${edges.length}_${source}_${target}`,
|
|
81
|
-
source,
|
|
82
|
-
target,
|
|
83
|
-
edgeType,
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function rankNodesForEntity(a: SemanticNode, b: SemanticNode): number {
|
|
88
|
-
const evidenceDiff = (b.evidenceCount || 0) - (a.evidenceCount || 0);
|
|
89
|
-
if (evidenceDiff !== 0) return evidenceDiff;
|
|
90
|
-
return a.id.localeCompare(b.id);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// 1 & 2: Entity-name based edges
|
|
94
|
-
for (const [, group] of entityNameIndex) {
|
|
95
|
-
if (group.length < 2) continue;
|
|
96
|
-
|
|
97
|
-
const bySection = new Map<string, SemanticNode[]>();
|
|
98
|
-
for (const node of group) {
|
|
99
|
-
const sectionGroup = bySection.get(node.sectionId) || [];
|
|
100
|
-
sectionGroup.push(node);
|
|
101
|
-
bySection.set(node.sectionId, sectionGroup);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
for (const sectionGroup of bySection.values()) {
|
|
105
|
-
const ranked = [...sectionGroup].sort(rankNodesForEntity);
|
|
106
|
-
if (ranked.length === 2) {
|
|
107
|
-
addEdge(ranked[0].id, ranked[1].id, 'relates_to');
|
|
108
|
-
addEdge(ranked[1].id, ranked[0].id, 'relates_to');
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
const anchor = ranked[0];
|
|
112
|
-
for (const node of ranked.slice(1, maxRelatesEdgesPerEntitySection + 1)) {
|
|
113
|
-
addEdge(anchor.id, node.id, 'relates_to');
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const sectionAnchors = [...bySection.entries()]
|
|
118
|
-
.map(([sectionId, sectionGroup]) => ({
|
|
119
|
-
sectionId,
|
|
120
|
-
anchor: [...sectionGroup].sort(rankNodesForEntity)[0],
|
|
121
|
-
}))
|
|
122
|
-
.sort((a, b) => sectionPriority(a.sectionId) - sectionPriority(b.sectionId));
|
|
123
|
-
|
|
124
|
-
for (let i = 0; i < sectionAnchors.length; i++) {
|
|
125
|
-
for (let j = i + 1; j < sectionAnchors.length; j++) {
|
|
126
|
-
const from = sectionAnchors[i].anchor;
|
|
127
|
-
const to = sectionAnchors[j].anchor;
|
|
128
|
-
const edgeType = sectionPriority(sectionAnchors[i].sectionId) === sectionPriority(sectionAnchors[j].sectionId)
|
|
129
|
-
? 'relates_to'
|
|
130
|
-
: 'supports';
|
|
131
|
-
addEdge(from.id, to.id, edgeType);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return edges;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function sectionPriority(sectionId: string): number {
|
|
140
|
-
const order = ['core-decisions', 'known-gotchas', 'operational-knowledge', 'git-backed-facts', 'promoted-skills'];
|
|
141
|
-
const idx = order.indexOf(sectionId);
|
|
142
|
-
return idx >= 0 ? idx : order.length;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function sectionIdForObs(o: Observation): string {
|
|
146
|
-
// Git-backed facts take priority over type-based sections
|
|
147
|
-
if (GIT_SECTION.typeMatch(o)) return GIT_SECTION.id;
|
|
148
|
-
for (const def of SECTION_DEFS) {
|
|
149
|
-
if (def.typeMatch(o)) return def.id;
|
|
150
|
-
}
|
|
151
|
-
return 'operational-knowledge'; // fallback
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// -- Graph store entity/relation types --
|
|
155
|
-
|
|
156
|
-
export interface GraphStoreEntity {
|
|
157
|
-
name: string;
|
|
158
|
-
entityType: string;
|
|
159
|
-
observations: string[];
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export interface GraphStoreRelation {
|
|
163
|
-
from: string;
|
|
164
|
-
to: string;
|
|
165
|
-
relationType: string;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Map graph-store relationType to SemanticEdgeType
|
|
169
|
-
function mapRelationType(relationType: string): SemanticEdgeType {
|
|
170
|
-
const lower = relationType.toLowerCase();
|
|
171
|
-
if (lower.includes('support') || lower.includes('depend')) return 'supports';
|
|
172
|
-
if (lower.includes('relat') || lower.includes('connect') || lower.includes('associat')) return 'relates_to';
|
|
173
|
-
if (lower.includes('mention') || lower.includes('refer')) return 'mentions';
|
|
174
|
-
if (lower.includes('deriv') || lower.includes('origin') || lower.includes('source')) return 'derived_from';
|
|
175
|
-
return 'relates_to'; // default fallback
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// -- Main generator --
|
|
179
|
-
|
|
180
|
-
export interface GenerateGraphOptions {
|
|
181
|
-
projectId: string;
|
|
182
|
-
observations: Observation[];
|
|
183
|
-
miniSkills: MiniSkill[];
|
|
184
|
-
generatedAt?: string;
|
|
185
|
-
graphEntities?: GraphStoreEntity[];
|
|
186
|
-
graphRelations?: GraphStoreRelation[];
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export function generateKnowledgeGraph(options: GenerateGraphOptions): ProjectKnowledgeGraph {
|
|
190
|
-
const { projectId, observations, miniSkills, graphEntities, graphRelations } = options;
|
|
191
|
-
|
|
192
|
-
const eligible = observations.filter((o) => isEligible(o, projectId));
|
|
193
|
-
const scopedMiniSkills = miniSkills.filter((s) => s.projectId === projectId);
|
|
194
|
-
|
|
195
|
-
// Build nodes from eligible observations
|
|
196
|
-
const nodes: SemanticNode[] = [];
|
|
197
|
-
const entityNameIndex = new Map<string, SemanticNode[]>();
|
|
198
|
-
|
|
199
|
-
for (const o of eligible) {
|
|
200
|
-
const sectionId = sectionIdForObs(o);
|
|
201
|
-
const ref: KnowledgeSourceRef = {
|
|
202
|
-
kind: o.source === 'git' ? 'git' : 'observation',
|
|
203
|
-
id: `obs:${o.id}`,
|
|
204
|
-
title: o.title,
|
|
205
|
-
};
|
|
206
|
-
const node: SemanticNode = {
|
|
207
|
-
id: `obs:${o.id}`,
|
|
208
|
-
label: o.title,
|
|
209
|
-
nodeType: o.type,
|
|
210
|
-
sectionId,
|
|
211
|
-
entityName: o.entityName || undefined,
|
|
212
|
-
evidenceCount: (o.facts?.length ?? 0) + (o.concepts?.length ?? 0) + (o.filesModified?.length ?? 0),
|
|
213
|
-
summary: o.narrative?.slice(0, 200) || '',
|
|
214
|
-
refs: [ref],
|
|
215
|
-
};
|
|
216
|
-
nodes.push(node);
|
|
217
|
-
|
|
218
|
-
if (o.entityName) {
|
|
219
|
-
const group = entityNameIndex.get(o.entityName) || [];
|
|
220
|
-
group.push(node);
|
|
221
|
-
entityNameIndex.set(o.entityName, group);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Build nodes from mini-skills
|
|
226
|
-
for (const s of scopedMiniSkills) {
|
|
227
|
-
const ref: KnowledgeSourceRef = {
|
|
228
|
-
kind: 'mini-skill',
|
|
229
|
-
id: `skill:${s.id}`,
|
|
230
|
-
title: s.title,
|
|
231
|
-
};
|
|
232
|
-
const obsRefs: KnowledgeSourceRef[] = s.sourceObservationIds.map(
|
|
233
|
-
(oid) => ({ kind: 'observation' as const, id: `obs:${oid}` }),
|
|
234
|
-
);
|
|
235
|
-
const node: SemanticNode = {
|
|
236
|
-
id: `skill:${s.id}`,
|
|
237
|
-
label: s.title,
|
|
238
|
-
nodeType: 'mini-skill',
|
|
239
|
-
sectionId: 'promoted-skills',
|
|
240
|
-
entityName: s.sourceEntity || undefined,
|
|
241
|
-
evidenceCount: obsRefs.length,
|
|
242
|
-
summary: s.instruction?.slice(0, 200) || '',
|
|
243
|
-
refs: [ref, ...obsRefs],
|
|
244
|
-
};
|
|
245
|
-
nodes.push(node);
|
|
246
|
-
|
|
247
|
-
if (s.sourceEntity) {
|
|
248
|
-
const group = entityNameIndex.get(s.sourceEntity) || [];
|
|
249
|
-
group.push(node);
|
|
250
|
-
entityNameIndex.set(s.sourceEntity, group);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Infer edges from observation/skill semantics
|
|
255
|
-
const edges = inferEdges(nodes, entityNameIndex);
|
|
256
|
-
|
|
257
|
-
// Add derived_from edges: skill -> source obs
|
|
258
|
-
for (const s of scopedMiniSkills) {
|
|
259
|
-
const skillNodeId = `skill:${s.id}`;
|
|
260
|
-
for (const oid of s.sourceObservationIds) {
|
|
261
|
-
const obsNodeId = `obs:${oid}`;
|
|
262
|
-
if (nodes.some((n) => n.id === obsNodeId)) {
|
|
263
|
-
edges.push({
|
|
264
|
-
id: `e_${edges.length}_${skillNodeId}_${obsNodeId}`,
|
|
265
|
-
source: skillNodeId,
|
|
266
|
-
target: obsNodeId,
|
|
267
|
-
edgeType: 'derived_from',
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Merge graph-store relations into semantic edges
|
|
274
|
-
// Only include relations where both endpoints map to existing nodes
|
|
275
|
-
if (graphRelations && graphRelations.length > 0) {
|
|
276
|
-
const entityNodeMap = new Map<string, string>(); // entityName -> first matching nodeId
|
|
277
|
-
for (const n of nodes) {
|
|
278
|
-
if (n.entityName && !entityNodeMap.has(n.entityName)) {
|
|
279
|
-
entityNodeMap.set(n.entityName, n.id);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const seen = new Set(edges.map((e) => `${e.source}:${e.edgeType}:${e.target}`));
|
|
284
|
-
|
|
285
|
-
for (const rel of graphRelations) {
|
|
286
|
-
const srcId = entityNodeMap.get(rel.from);
|
|
287
|
-
const tgtId = entityNodeMap.get(rel.to);
|
|
288
|
-
if (!srcId || !tgtId) continue; // skip if either endpoint not in project scope
|
|
289
|
-
if (srcId === tgtId) continue;
|
|
290
|
-
const edgeType = mapRelationType(rel.relationType);
|
|
291
|
-
const key = `${srcId}:${edgeType}:${tgtId}`;
|
|
292
|
-
if (seen.has(key)) continue; // skip duplicates
|
|
293
|
-
seen.add(key);
|
|
294
|
-
edges.push({
|
|
295
|
-
id: `e_gr_${edges.length}_${srcId}_${tgtId}`,
|
|
296
|
-
source: srcId,
|
|
297
|
-
target: tgtId,
|
|
298
|
-
edgeType,
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Build clusters from sections
|
|
304
|
-
const sectionCounts: Record<string, number> = {};
|
|
305
|
-
for (const n of nodes) {
|
|
306
|
-
sectionCounts[n.sectionId] = (sectionCounts[n.sectionId] || 0) + 1;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const clusters: KnowledgeGraphCluster[] = ALL_SECTIONS
|
|
310
|
-
.filter((def) => (sectionCounts[def.id] ?? 0) > 0)
|
|
311
|
-
.map((def) => ({
|
|
312
|
-
id: `cluster:${def.id}`,
|
|
313
|
-
label: def.label,
|
|
314
|
-
sectionId: def.id,
|
|
315
|
-
nodeCount: sectionCounts[def.id] ?? 0,
|
|
316
|
-
}));
|
|
317
|
-
|
|
318
|
-
const stats: KnowledgeGraphStats = {
|
|
319
|
-
totalNodes: nodes.length,
|
|
320
|
-
totalEdges: edges.length,
|
|
321
|
-
clusterCount: clusters.length,
|
|
322
|
-
sectionCounts,
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
return {
|
|
326
|
-
title: 'Knowledge Graph',
|
|
327
|
-
projectId,
|
|
328
|
-
generatedAt: options.generatedAt ?? new Date().toISOString(),
|
|
329
|
-
nodes,
|
|
330
|
-
edges,
|
|
331
|
-
clusters,
|
|
332
|
-
stats,
|
|
333
|
-
};
|
|
334
|
-
}
|
|
1
|
+
import type { Observation, MiniSkill } from '../types.js';
|
|
2
|
+
import type {
|
|
3
|
+
KnowledgeSourceRef,
|
|
4
|
+
SemanticNode,
|
|
5
|
+
SemanticEdge,
|
|
6
|
+
SemanticEdgeType,
|
|
7
|
+
KnowledgeGraphCluster,
|
|
8
|
+
KnowledgeGraphStats,
|
|
9
|
+
ProjectKnowledgeGraph,
|
|
10
|
+
} from './types.js';
|
|
11
|
+
import { isEligible } from './generator.js';
|
|
12
|
+
|
|
13
|
+
// -- Section definitions (must match generator.ts SECTION_DEFS) --
|
|
14
|
+
|
|
15
|
+
interface SectionDef {
|
|
16
|
+
id: string;
|
|
17
|
+
label: string;
|
|
18
|
+
typeMatch: (o: Observation) => boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const SECTION_DEFS: SectionDef[] = [
|
|
22
|
+
{
|
|
23
|
+
id: 'core-decisions',
|
|
24
|
+
label: 'Core Decisions',
|
|
25
|
+
typeMatch: (o) => o.type === 'decision' || o.type === 'trade-off' || o.type === 'reasoning',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'operational-knowledge',
|
|
29
|
+
label: 'Operational Knowledge',
|
|
30
|
+
typeMatch: (o) =>
|
|
31
|
+
o.type === 'how-it-works' ||
|
|
32
|
+
o.type === 'what-changed' ||
|
|
33
|
+
o.type === 'why-it-exists' ||
|
|
34
|
+
o.type === 'discovery' ||
|
|
35
|
+
o.type === 'session-request',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 'known-gotchas',
|
|
39
|
+
label: 'Known Gotchas',
|
|
40
|
+
typeMatch: (o) => o.type === 'gotcha' || o.type === 'problem-solution',
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const GIT_SECTION: SectionDef = {
|
|
45
|
+
id: 'git-backed-facts',
|
|
46
|
+
label: 'Git-backed Facts',
|
|
47
|
+
typeMatch: (o) => o.source === 'git' && o.sourceDetail === 'git-ingest',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const SKILLS_SECTION: SectionDef = {
|
|
51
|
+
id: 'promoted-skills',
|
|
52
|
+
label: 'Promoted Skills',
|
|
53
|
+
typeMatch: () => false, // skills handled separately
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const ALL_SECTIONS: SectionDef[] = [...SECTION_DEFS, GIT_SECTION, SKILLS_SECTION];
|
|
57
|
+
|
|
58
|
+
// -- Edge inference rules --
|
|
59
|
+
// Infer semantic edges between observations based on shared attributes.
|
|
60
|
+
// Rules (no LLM, deterministic):
|
|
61
|
+
// 1. supports: same entityName, different section -> source supports target
|
|
62
|
+
// 2. relates_to: same entityName, same section -> bidirectional
|
|
63
|
+
// 3. derived_from: mini-skill source obs -> skill node
|
|
64
|
+
// Explicit graph-store references are merged later as mentions edges.
|
|
65
|
+
|
|
66
|
+
function inferEdges(
|
|
67
|
+
nodes: SemanticNode[],
|
|
68
|
+
entityNameIndex: Map<string, SemanticNode[]>,
|
|
69
|
+
): SemanticEdge[] {
|
|
70
|
+
const edges: SemanticEdge[] = [];
|
|
71
|
+
const seen = new Set<string>();
|
|
72
|
+
const maxRelatesEdgesPerEntitySection = 8;
|
|
73
|
+
|
|
74
|
+
function addEdge(source: string, target: string, edgeType: SemanticEdgeType): void {
|
|
75
|
+
const key = `${source}:${edgeType}:${target}`;
|
|
76
|
+
if (seen.has(key)) return;
|
|
77
|
+
if (source === target) return;
|
|
78
|
+
seen.add(key);
|
|
79
|
+
edges.push({
|
|
80
|
+
id: `e_${edges.length}_${source}_${target}`,
|
|
81
|
+
source,
|
|
82
|
+
target,
|
|
83
|
+
edgeType,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function rankNodesForEntity(a: SemanticNode, b: SemanticNode): number {
|
|
88
|
+
const evidenceDiff = (b.evidenceCount || 0) - (a.evidenceCount || 0);
|
|
89
|
+
if (evidenceDiff !== 0) return evidenceDiff;
|
|
90
|
+
return a.id.localeCompare(b.id);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 1 & 2: Entity-name based edges
|
|
94
|
+
for (const [, group] of entityNameIndex) {
|
|
95
|
+
if (group.length < 2) continue;
|
|
96
|
+
|
|
97
|
+
const bySection = new Map<string, SemanticNode[]>();
|
|
98
|
+
for (const node of group) {
|
|
99
|
+
const sectionGroup = bySection.get(node.sectionId) || [];
|
|
100
|
+
sectionGroup.push(node);
|
|
101
|
+
bySection.set(node.sectionId, sectionGroup);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const sectionGroup of bySection.values()) {
|
|
105
|
+
const ranked = [...sectionGroup].sort(rankNodesForEntity);
|
|
106
|
+
if (ranked.length === 2) {
|
|
107
|
+
addEdge(ranked[0].id, ranked[1].id, 'relates_to');
|
|
108
|
+
addEdge(ranked[1].id, ranked[0].id, 'relates_to');
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const anchor = ranked[0];
|
|
112
|
+
for (const node of ranked.slice(1, maxRelatesEdgesPerEntitySection + 1)) {
|
|
113
|
+
addEdge(anchor.id, node.id, 'relates_to');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const sectionAnchors = [...bySection.entries()]
|
|
118
|
+
.map(([sectionId, sectionGroup]) => ({
|
|
119
|
+
sectionId,
|
|
120
|
+
anchor: [...sectionGroup].sort(rankNodesForEntity)[0],
|
|
121
|
+
}))
|
|
122
|
+
.sort((a, b) => sectionPriority(a.sectionId) - sectionPriority(b.sectionId));
|
|
123
|
+
|
|
124
|
+
for (let i = 0; i < sectionAnchors.length; i++) {
|
|
125
|
+
for (let j = i + 1; j < sectionAnchors.length; j++) {
|
|
126
|
+
const from = sectionAnchors[i].anchor;
|
|
127
|
+
const to = sectionAnchors[j].anchor;
|
|
128
|
+
const edgeType = sectionPriority(sectionAnchors[i].sectionId) === sectionPriority(sectionAnchors[j].sectionId)
|
|
129
|
+
? 'relates_to'
|
|
130
|
+
: 'supports';
|
|
131
|
+
addEdge(from.id, to.id, edgeType);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return edges;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function sectionPriority(sectionId: string): number {
|
|
140
|
+
const order = ['core-decisions', 'known-gotchas', 'operational-knowledge', 'git-backed-facts', 'promoted-skills'];
|
|
141
|
+
const idx = order.indexOf(sectionId);
|
|
142
|
+
return idx >= 0 ? idx : order.length;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function sectionIdForObs(o: Observation): string {
|
|
146
|
+
// Git-backed facts take priority over type-based sections
|
|
147
|
+
if (GIT_SECTION.typeMatch(o)) return GIT_SECTION.id;
|
|
148
|
+
for (const def of SECTION_DEFS) {
|
|
149
|
+
if (def.typeMatch(o)) return def.id;
|
|
150
|
+
}
|
|
151
|
+
return 'operational-knowledge'; // fallback
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// -- Graph store entity/relation types --
|
|
155
|
+
|
|
156
|
+
export interface GraphStoreEntity {
|
|
157
|
+
name: string;
|
|
158
|
+
entityType: string;
|
|
159
|
+
observations: string[];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface GraphStoreRelation {
|
|
163
|
+
from: string;
|
|
164
|
+
to: string;
|
|
165
|
+
relationType: string;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Map graph-store relationType to SemanticEdgeType
|
|
169
|
+
function mapRelationType(relationType: string): SemanticEdgeType {
|
|
170
|
+
const lower = relationType.toLowerCase();
|
|
171
|
+
if (lower.includes('support') || lower.includes('depend')) return 'supports';
|
|
172
|
+
if (lower.includes('relat') || lower.includes('connect') || lower.includes('associat')) return 'relates_to';
|
|
173
|
+
if (lower.includes('mention') || lower.includes('refer')) return 'mentions';
|
|
174
|
+
if (lower.includes('deriv') || lower.includes('origin') || lower.includes('source')) return 'derived_from';
|
|
175
|
+
return 'relates_to'; // default fallback
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// -- Main generator --
|
|
179
|
+
|
|
180
|
+
export interface GenerateGraphOptions {
|
|
181
|
+
projectId: string;
|
|
182
|
+
observations: Observation[];
|
|
183
|
+
miniSkills: MiniSkill[];
|
|
184
|
+
generatedAt?: string;
|
|
185
|
+
graphEntities?: GraphStoreEntity[];
|
|
186
|
+
graphRelations?: GraphStoreRelation[];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function generateKnowledgeGraph(options: GenerateGraphOptions): ProjectKnowledgeGraph {
|
|
190
|
+
const { projectId, observations, miniSkills, graphEntities, graphRelations } = options;
|
|
191
|
+
|
|
192
|
+
const eligible = observations.filter((o) => isEligible(o, projectId));
|
|
193
|
+
const scopedMiniSkills = miniSkills.filter((s) => s.projectId === projectId);
|
|
194
|
+
|
|
195
|
+
// Build nodes from eligible observations
|
|
196
|
+
const nodes: SemanticNode[] = [];
|
|
197
|
+
const entityNameIndex = new Map<string, SemanticNode[]>();
|
|
198
|
+
|
|
199
|
+
for (const o of eligible) {
|
|
200
|
+
const sectionId = sectionIdForObs(o);
|
|
201
|
+
const ref: KnowledgeSourceRef = {
|
|
202
|
+
kind: o.source === 'git' ? 'git' : 'observation',
|
|
203
|
+
id: `obs:${o.id}`,
|
|
204
|
+
title: o.title,
|
|
205
|
+
};
|
|
206
|
+
const node: SemanticNode = {
|
|
207
|
+
id: `obs:${o.id}`,
|
|
208
|
+
label: o.title,
|
|
209
|
+
nodeType: o.type,
|
|
210
|
+
sectionId,
|
|
211
|
+
entityName: o.entityName || undefined,
|
|
212
|
+
evidenceCount: (o.facts?.length ?? 0) + (o.concepts?.length ?? 0) + (o.filesModified?.length ?? 0),
|
|
213
|
+
summary: o.narrative?.slice(0, 200) || '',
|
|
214
|
+
refs: [ref],
|
|
215
|
+
};
|
|
216
|
+
nodes.push(node);
|
|
217
|
+
|
|
218
|
+
if (o.entityName) {
|
|
219
|
+
const group = entityNameIndex.get(o.entityName) || [];
|
|
220
|
+
group.push(node);
|
|
221
|
+
entityNameIndex.set(o.entityName, group);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Build nodes from mini-skills
|
|
226
|
+
for (const s of scopedMiniSkills) {
|
|
227
|
+
const ref: KnowledgeSourceRef = {
|
|
228
|
+
kind: 'mini-skill',
|
|
229
|
+
id: `skill:${s.id}`,
|
|
230
|
+
title: s.title,
|
|
231
|
+
};
|
|
232
|
+
const obsRefs: KnowledgeSourceRef[] = s.sourceObservationIds.map(
|
|
233
|
+
(oid) => ({ kind: 'observation' as const, id: `obs:${oid}` }),
|
|
234
|
+
);
|
|
235
|
+
const node: SemanticNode = {
|
|
236
|
+
id: `skill:${s.id}`,
|
|
237
|
+
label: s.title,
|
|
238
|
+
nodeType: 'mini-skill',
|
|
239
|
+
sectionId: 'promoted-skills',
|
|
240
|
+
entityName: s.sourceEntity || undefined,
|
|
241
|
+
evidenceCount: obsRefs.length,
|
|
242
|
+
summary: s.instruction?.slice(0, 200) || '',
|
|
243
|
+
refs: [ref, ...obsRefs],
|
|
244
|
+
};
|
|
245
|
+
nodes.push(node);
|
|
246
|
+
|
|
247
|
+
if (s.sourceEntity) {
|
|
248
|
+
const group = entityNameIndex.get(s.sourceEntity) || [];
|
|
249
|
+
group.push(node);
|
|
250
|
+
entityNameIndex.set(s.sourceEntity, group);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Infer edges from observation/skill semantics
|
|
255
|
+
const edges = inferEdges(nodes, entityNameIndex);
|
|
256
|
+
|
|
257
|
+
// Add derived_from edges: skill -> source obs
|
|
258
|
+
for (const s of scopedMiniSkills) {
|
|
259
|
+
const skillNodeId = `skill:${s.id}`;
|
|
260
|
+
for (const oid of s.sourceObservationIds) {
|
|
261
|
+
const obsNodeId = `obs:${oid}`;
|
|
262
|
+
if (nodes.some((n) => n.id === obsNodeId)) {
|
|
263
|
+
edges.push({
|
|
264
|
+
id: `e_${edges.length}_${skillNodeId}_${obsNodeId}`,
|
|
265
|
+
source: skillNodeId,
|
|
266
|
+
target: obsNodeId,
|
|
267
|
+
edgeType: 'derived_from',
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Merge graph-store relations into semantic edges
|
|
274
|
+
// Only include relations where both endpoints map to existing nodes
|
|
275
|
+
if (graphRelations && graphRelations.length > 0) {
|
|
276
|
+
const entityNodeMap = new Map<string, string>(); // entityName -> first matching nodeId
|
|
277
|
+
for (const n of nodes) {
|
|
278
|
+
if (n.entityName && !entityNodeMap.has(n.entityName)) {
|
|
279
|
+
entityNodeMap.set(n.entityName, n.id);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const seen = new Set(edges.map((e) => `${e.source}:${e.edgeType}:${e.target}`));
|
|
284
|
+
|
|
285
|
+
for (const rel of graphRelations) {
|
|
286
|
+
const srcId = entityNodeMap.get(rel.from);
|
|
287
|
+
const tgtId = entityNodeMap.get(rel.to);
|
|
288
|
+
if (!srcId || !tgtId) continue; // skip if either endpoint not in project scope
|
|
289
|
+
if (srcId === tgtId) continue;
|
|
290
|
+
const edgeType = mapRelationType(rel.relationType);
|
|
291
|
+
const key = `${srcId}:${edgeType}:${tgtId}`;
|
|
292
|
+
if (seen.has(key)) continue; // skip duplicates
|
|
293
|
+
seen.add(key);
|
|
294
|
+
edges.push({
|
|
295
|
+
id: `e_gr_${edges.length}_${srcId}_${tgtId}`,
|
|
296
|
+
source: srcId,
|
|
297
|
+
target: tgtId,
|
|
298
|
+
edgeType,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Build clusters from sections
|
|
304
|
+
const sectionCounts: Record<string, number> = {};
|
|
305
|
+
for (const n of nodes) {
|
|
306
|
+
sectionCounts[n.sectionId] = (sectionCounts[n.sectionId] || 0) + 1;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const clusters: KnowledgeGraphCluster[] = ALL_SECTIONS
|
|
310
|
+
.filter((def) => (sectionCounts[def.id] ?? 0) > 0)
|
|
311
|
+
.map((def) => ({
|
|
312
|
+
id: `cluster:${def.id}`,
|
|
313
|
+
label: def.label,
|
|
314
|
+
sectionId: def.id,
|
|
315
|
+
nodeCount: sectionCounts[def.id] ?? 0,
|
|
316
|
+
}));
|
|
317
|
+
|
|
318
|
+
const stats: KnowledgeGraphStats = {
|
|
319
|
+
totalNodes: nodes.length,
|
|
320
|
+
totalEdges: edges.length,
|
|
321
|
+
clusterCount: clusters.length,
|
|
322
|
+
sectionCounts,
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
title: 'Knowledge Graph',
|
|
327
|
+
projectId,
|
|
328
|
+
generatedAt: options.generatedAt ?? new Date().toISOString(),
|
|
329
|
+
nodes,
|
|
330
|
+
edges,
|
|
331
|
+
clusters,
|
|
332
|
+
stats,
|
|
333
|
+
};
|
|
334
|
+
}
|