@unson/brainbase-mcp 0.2.4 → 0.3.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/README.md +17 -12
- package/dist/canonical-edge-builder.d.ts +11 -0
- package/dist/canonical-edge-builder.js +35 -0
- package/dist/canonical-graph.d.ts +9 -0
- package/dist/canonical-graph.js +174 -0
- package/dist/cli.js +134 -32
- package/dist/connected-onboarding.js +45 -11
- package/dist/entity-resolution.d.ts +111 -0
- package/dist/entity-resolution.js +381 -0
- package/dist/graph-diagnosis.d.ts +24 -0
- package/dist/graph-diagnosis.js +129 -0
- package/dist/guided-onboarding.js +4 -4
- package/dist/import-extract.d.ts +8 -3
- package/dist/import-extract.js +142 -17
- package/dist/onboarding.js +5 -5
- package/dist/ontology-migration.d.ts +30 -0
- package/dist/ontology-migration.js +339 -0
- package/dist/ontology-ssot.js +2 -2
- package/dist/ontology.d.ts +101 -3
- package/dist/ontology.js +64 -8
- package/dist/operationalization.js +6 -6
- package/dist/projects.d.ts +3 -1
- package/dist/projects.js +40 -0
- package/dist/relation-registry.d.ts +11 -0
- package/dist/relation-registry.js +66 -0
- package/dist/server.d.ts +86 -2
- package/dist/server.js +158 -9
- package/dist/skills.js +3 -3
- package/dist/ssot.d.ts +16 -0
- package/dist/ssot.js +68 -18
- package/dist/templates.d.ts +110 -2
- package/dist/templates.js +74 -5
- package/dist/tools.d.ts +6 -2
- package/dist/tools.js +184 -147
- package/dist/types.d.ts +52 -1
- package/package.json +4 -3
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { canonicalEdgeId, isActiveAt, validateCanonicalGraph } from './canonical-graph.js';
|
|
4
|
+
import { getCanonicalRelation } from './relation-registry.js';
|
|
5
|
+
export async function diagnoseGraph(dataDir, asOf = new Date().toISOString()) {
|
|
6
|
+
let graph;
|
|
7
|
+
let rawGraph;
|
|
8
|
+
try {
|
|
9
|
+
rawGraph = await readFile(join(dataDir, 'graph.json'), 'utf8');
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
return emptyDiagnosis('unavailable', asOf, [{ class: 'unavailable', detail: error instanceof Error ? error.message : String(error) }]);
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
graph = JSON.parse(rawGraph);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return emptyDiagnosis('invalid', asOf, [{ class: 'invalid', detail: 'graph.json is not valid JSON.' }]);
|
|
19
|
+
}
|
|
20
|
+
const schemaVersion = typeof graph?.version === 'number' ? graph.version : null;
|
|
21
|
+
const entities = Array.isArray(graph?.entities) ? graph.entities : [];
|
|
22
|
+
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
23
|
+
const issues = [];
|
|
24
|
+
if (schemaVersion === 1)
|
|
25
|
+
issues.push({ class: 'migration', detail: 'Graph v1 must be migrated before ID-edge retrieval is available.' });
|
|
26
|
+
const entityIds = new Set();
|
|
27
|
+
let duplicateEntities = 0;
|
|
28
|
+
for (const entity of entities) {
|
|
29
|
+
if (typeof entity?.id !== 'string')
|
|
30
|
+
continue;
|
|
31
|
+
if (entityIds.has(entity.id))
|
|
32
|
+
duplicateEntities += 1;
|
|
33
|
+
entityIds.add(entity.id);
|
|
34
|
+
}
|
|
35
|
+
if (duplicateEntities > 0)
|
|
36
|
+
issues.push({ class: 'duplicate', detail: `${duplicateEntities} duplicate entity IDs` });
|
|
37
|
+
const edgeIds = new Set();
|
|
38
|
+
const tuples = new Set();
|
|
39
|
+
let danglingEdges = 0;
|
|
40
|
+
let invalidEdges = 0;
|
|
41
|
+
let duplicateEdges = 0;
|
|
42
|
+
let activeEdges = 0;
|
|
43
|
+
const entityById = new Map(entities.filter((entity) => typeof entity?.id === 'string').map((entity) => [entity.id, entity]));
|
|
44
|
+
for (const edge of edges) {
|
|
45
|
+
const tuple = JSON.stringify([edge?.fromId, edge?.relation, edge?.toId]);
|
|
46
|
+
const duplicate = edgeIds.has(edge?.id) || tuples.has(tuple);
|
|
47
|
+
if (duplicate) {
|
|
48
|
+
duplicateEdges += 1;
|
|
49
|
+
issues.push({ class: 'duplicate', recordId: String(edge?.id ?? ''), detail: 'Duplicate edge ID or tuple.' });
|
|
50
|
+
}
|
|
51
|
+
if (typeof edge?.id === 'string')
|
|
52
|
+
edgeIds.add(edge.id);
|
|
53
|
+
tuples.add(tuple);
|
|
54
|
+
const from = entityById.get(edge?.fromId);
|
|
55
|
+
const to = entityById.get(edge?.toId);
|
|
56
|
+
if (!from || !to) {
|
|
57
|
+
danglingEdges += 1;
|
|
58
|
+
issues.push({ class: 'dangling', recordId: String(edge?.id ?? ''), detail: 'Edge endpoint does not resolve to an entity ID.' });
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
let valid = true;
|
|
62
|
+
try {
|
|
63
|
+
const relation = getCanonicalRelation(edge.relation);
|
|
64
|
+
valid = from.type === relation.from && to.type === relation.to && edge.id === canonicalEdgeId(edge);
|
|
65
|
+
if (valid && !duplicate && isActiveAt(edge, asOf) && isActiveAt(from, asOf) && isActiveAt(to, asOf))
|
|
66
|
+
activeEdges += 1;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
valid = false;
|
|
70
|
+
}
|
|
71
|
+
if (!valid) {
|
|
72
|
+
invalidEdges += 1;
|
|
73
|
+
issues.push({ class: 'invalid', recordId: String(edge?.id ?? ''), detail: 'Edge relation, endpoint types, stable ID, or validity interval is invalid.' });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const { projections, unresolvedRecords } = await diagnoseLegacyViews(dataDir, entities);
|
|
77
|
+
if (projections > 0)
|
|
78
|
+
issues.push({ class: 'projection', detail: `${projections} legacy records are projections of canonical entities.` });
|
|
79
|
+
if (unresolvedRecords > 0)
|
|
80
|
+
issues.push({ class: 'unresolved', detail: `${unresolvedRecords} legacy records do not resolve to a canonical entity ID.` });
|
|
81
|
+
let structurallyInvalid = false;
|
|
82
|
+
try {
|
|
83
|
+
validateCanonicalGraph(graph);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
structurallyInvalid = true;
|
|
87
|
+
}
|
|
88
|
+
const status = schemaVersion === 1 ? 'migration_required' : structurallyInvalid ? 'invalid' : issues.length > 0 ? 'issues' : 'healthy';
|
|
89
|
+
return { status, schemaVersion, ontology: graph?.ontology ?? null, asOf, migrationRequired: schemaVersion === 1,
|
|
90
|
+
counts: { entities: entities.length, edges: edges.length, activeEdges, danglingEdges, invalidEdges, duplicateEdges, duplicateEntities, unresolvedRecords, projections }, issues };
|
|
91
|
+
}
|
|
92
|
+
async function diagnoseLegacyViews(dataDir, entities) {
|
|
93
|
+
const personKeys = canonicalKeys(entities.filter((entity) => entity.type === 'person'));
|
|
94
|
+
const decisionKeys = canonicalKeys(entities.filter((entity) => entity.type === 'decision'));
|
|
95
|
+
let projections = 0;
|
|
96
|
+
let unresolvedRecords = 0;
|
|
97
|
+
try {
|
|
98
|
+
const relationships = JSON.parse(await readFile(join(dataDir, 'relationships.json'), 'utf8')).relationships;
|
|
99
|
+
for (const record of Array.isArray(relationships) ? relationships : [])
|
|
100
|
+
personKeys.has(normalize(String(record.person ?? ''))) ? projections += 1 : unresolvedRecords += 1;
|
|
101
|
+
}
|
|
102
|
+
catch { /* loadPersonalOs reports sidecar errors */ }
|
|
103
|
+
try {
|
|
104
|
+
for (const row of (await readFile(join(dataDir, 'decisions.jsonl'), 'utf8')).split(/\r?\n/u).filter(Boolean)) {
|
|
105
|
+
const record = JSON.parse(row);
|
|
106
|
+
decisionKeys.has(String(record.id ?? '')) || decisionKeys.has(normalize(String(record.title ?? ''))) ? projections += 1 : unresolvedRecords += 1;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch { /* loadPersonalOs reports sidecar errors */ }
|
|
110
|
+
return { projections, unresolvedRecords };
|
|
111
|
+
}
|
|
112
|
+
function canonicalKeys(entities) {
|
|
113
|
+
const candidates = new Map();
|
|
114
|
+
for (const entity of entities) {
|
|
115
|
+
const id = String(entity.id ?? '');
|
|
116
|
+
for (const key of [id, normalize(String(entity.name ?? '')), ...(Array.isArray(entity.aliases) ? entity.aliases.map((alias) => normalize(String(alias))) : [])]) {
|
|
117
|
+
if (!key)
|
|
118
|
+
continue;
|
|
119
|
+
const matches = candidates.get(key) ?? new Set();
|
|
120
|
+
matches.add(id);
|
|
121
|
+
candidates.set(key, matches);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return new Set([...candidates].filter(([, matches]) => matches.size === 1).map(([key]) => key));
|
|
125
|
+
}
|
|
126
|
+
function normalize(value) { return value.normalize('NFKC').replace(/\s+/g, '').toLowerCase(); }
|
|
127
|
+
function emptyDiagnosis(status, asOf, issues) {
|
|
128
|
+
return { status, schemaVersion: null, ontology: null, asOf, migrationRequired: false, counts: { entities: 0, edges: 0, activeEdges: 0, danglingEdges: 0, invalidEdges: 0, duplicateEdges: 0, duplicateEntities: 0, unresolvedRecords: 0, projections: 0 }, issues };
|
|
129
|
+
}
|
|
@@ -70,7 +70,7 @@ export function buildGuidedFirstRun(input) {
|
|
|
70
70
|
'--dir', input.dataDir,
|
|
71
71
|
'--scenario', firstValueScenario
|
|
72
72
|
]);
|
|
73
|
-
const actualAgentPrompt = `Brainbaseのget_context
|
|
73
|
+
const actualAgentPrompt = `Brainbaseのresolve_entity、get_context、searchを使い、保存済み文脈を根拠に「${firstValueScenario}」へ回答してください。使った正規ID、関係、前提、未確認事項を分けてください。`;
|
|
74
74
|
const projectDryRunCommand = project
|
|
75
75
|
? projectCommand(input.dataDir, project.project.name, input.project, project.project.sources, false)
|
|
76
76
|
: undefined;
|
|
@@ -132,7 +132,7 @@ export function buildGuidedFirstRun(input) {
|
|
|
132
132
|
},
|
|
133
133
|
{
|
|
134
134
|
id: 'doctor',
|
|
135
|
-
title: 'doctor と MCP get_context/search で確認する',
|
|
135
|
+
title: 'doctor と MCP resolve_entity/get_context/search で確認する',
|
|
136
136
|
when: 'MCP設定を実configへmergeし、対象エージェントを再起動した後',
|
|
137
137
|
command: operationalization.pending.find((item) => item.id === 'verification')?.command ?? command(['brainbase', 'doctor', '--dir', input.dataDir])
|
|
138
138
|
},
|
|
@@ -202,9 +202,9 @@ export function buildGuidedFirstRun(input) {
|
|
|
202
202
|
],
|
|
203
203
|
completionCheck: [
|
|
204
204
|
'CLIサンプルは接続前プレビューであり、初回価値の達成証拠にしない。',
|
|
205
|
-
'新しい実エージェントがBrainbase MCPのget_context/searchを使って現実の依頼へ回答する。',
|
|
205
|
+
'新しい実エージェントがBrainbase MCPのresolve_entity/get_context/searchを使って現実の依頼へ回答する。',
|
|
206
206
|
'実回答を見た本人が役立ったかを判断する。',
|
|
207
|
-
'完了報告には、公開skills、ohayo/oyasumi/retro、MCP実config merge、source allowlist/import/candidate review、MCP get_context/search確認の未完了タスクを必ず出す。',
|
|
207
|
+
'完了報告には、公開skills、ohayo/oyasumi/retro、MCP実config merge、source allowlist/import/candidate review、MCP resolve_entity/get_context/search確認の未完了タスクを必ず出す。',
|
|
208
208
|
`${targetLabel(input.target)} のMCP設定に Brainbase が実登録されている。`,
|
|
209
209
|
'最初のプロジェクトが get_context/search で見える。',
|
|
210
210
|
'外部ソースは許可範囲、保存先、正本化前レビューの流れが説明できる。'
|
package/dist/import-extract.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type CanonicalWriteSet } from './canonical-edge-builder.js';
|
|
2
|
+
import type { CanonicalEdge, CanonicalEntity, DecisionRecord, PersonalKgEntry, RelationshipRecord } from './types.js';
|
|
2
3
|
export type SourceProvider = 'gmail' | 'calendar' | 'drive' | 'local';
|
|
3
4
|
export interface GmailSourceRecord {
|
|
4
5
|
kind: 'gmail';
|
|
@@ -86,14 +87,18 @@ export interface ExtractOptions {
|
|
|
86
87
|
export declare function extractCandidates(records: SourceRecord[], options?: ExtractOptions): ExtractedCandidate[];
|
|
87
88
|
export declare function buildExtractedCandidateSet(candidates: ExtractedCandidate[], dataDir: string): ExtractedCandidateSet;
|
|
88
89
|
export interface ApplyInput {
|
|
89
|
-
graphEntities:
|
|
90
|
+
graphEntities: CanonicalEntity[];
|
|
91
|
+
graphEdges: CanonicalEdge[];
|
|
90
92
|
relationships: RelationshipRecord[];
|
|
91
93
|
personalKg: PersonalKgEntry[];
|
|
92
94
|
decisions: DecisionRecord[];
|
|
93
95
|
ownerName?: string;
|
|
96
|
+
provenanceSourceKind?: 'import' | 'onboarding';
|
|
94
97
|
}
|
|
95
98
|
export interface ApplyResult {
|
|
96
|
-
graphEntities:
|
|
99
|
+
graphEntities: CanonicalEntity[];
|
|
100
|
+
graphEdges: CanonicalEdge[];
|
|
101
|
+
canonicalWrites: CanonicalWriteSet;
|
|
97
102
|
relationships: RelationshipRecord[];
|
|
98
103
|
personalKgAdditions: PersonalKgEntry[];
|
|
99
104
|
decisionAdditions: DecisionRecord[];
|
package/dist/import-extract.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildCanonicalEdge } from './canonical-edge-builder.js';
|
|
1
2
|
const PROVIDER_SOURCE_PATH = {
|
|
2
3
|
gmail: 'sources/gmail/threads.jsonl',
|
|
3
4
|
calendar: 'sources/calendar/events.jsonl',
|
|
@@ -306,13 +307,31 @@ export function loadApplyCandidates(raw) {
|
|
|
306
307
|
}
|
|
307
308
|
export function planApply(candidates, selection, base, now) {
|
|
308
309
|
const graphEntities = [...base.graphEntities];
|
|
310
|
+
const graphEdges = [...base.graphEdges];
|
|
311
|
+
const canonicalWrites = { entities: [], edges: [] };
|
|
309
312
|
const relationships = [...base.relationships];
|
|
310
313
|
const personalKgAdditions = [];
|
|
311
314
|
const decisionAdditions = [];
|
|
312
315
|
const applied = [];
|
|
313
316
|
const skipped = [];
|
|
314
317
|
let ownerName = base.ownerName;
|
|
315
|
-
|
|
318
|
+
const provenanceSourceKind = base.provenanceSourceKind ?? 'import';
|
|
319
|
+
const selectedCandidates = candidates.filter((candidate) => selection.all || selection.ids.has(candidate.id));
|
|
320
|
+
const availableProjectIds = new Set(graphEntities.filter((entity) => entity.type === 'project').map((entity) => entity.id));
|
|
321
|
+
const availableDecisionIds = new Set(graphEntities.filter((entity) => entity.type === 'decision').map((entity) => entity.id));
|
|
322
|
+
for (const candidate of selectedCandidates) {
|
|
323
|
+
if (candidate.kind === 'project') {
|
|
324
|
+
const name = payloadString(candidate.payload, 'name');
|
|
325
|
+
if (name)
|
|
326
|
+
availableProjectIds.add(`project-${stableHash(name)}`);
|
|
327
|
+
}
|
|
328
|
+
if (candidate.kind === 'decision') {
|
|
329
|
+
const text = payloadString(candidate.payload, 'decision') || payloadString(candidate.payload, 'text');
|
|
330
|
+
if (text)
|
|
331
|
+
availableDecisionIds.add(`decision-${stableHash(text)}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
for (const candidate of [...candidates].sort((left, right) => left.id.localeCompare(right.id, 'en'))) {
|
|
316
335
|
if (!selection.all && !selection.ids.has(candidate.id)) {
|
|
317
336
|
skipped.push({ id: candidate.id, reason: 'not selected' });
|
|
318
337
|
continue;
|
|
@@ -326,8 +345,8 @@ export function planApply(candidates, selection, base, now) {
|
|
|
326
345
|
break;
|
|
327
346
|
}
|
|
328
347
|
ownerName = name;
|
|
329
|
-
|
|
330
|
-
personalKgAdditions.push({ id: `self-${stableHash(name)}`, type: 'self', text: `I am ${name}.`, tags: ['self'], source: candidate.id, updatedAt: now });
|
|
348
|
+
writeEntity(graphEntities, canonicalWrites.entities, { id: 'self', type: 'person', name, summary: 'Owner of this local Brainbase Personal OS.', tags: ['self'] });
|
|
349
|
+
personalKgAdditions.push(reuseById(base.personalKg, { id: `self-${stableHash(name)}`, type: 'self', text: `I am ${name}.`, tags: ['self'], source: candidate.id, updatedAt: now }));
|
|
331
350
|
applied.push({ id: candidate.id, kind: candidate.kind, summary: name, canonicalIds: ['self', `self-${stableHash(name)}`] });
|
|
332
351
|
break;
|
|
333
352
|
}
|
|
@@ -338,19 +357,39 @@ export function planApply(candidates, selection, base, now) {
|
|
|
338
357
|
skipped.push({ id: candidate.id, reason: 'person candidate missing name' });
|
|
339
358
|
break;
|
|
340
359
|
}
|
|
360
|
+
const projectId = payloadString(payload, 'projectId');
|
|
361
|
+
if (!projectId) {
|
|
362
|
+
skipped.push({ id: candidate.id, reason: 'unresolved_project_reference: explicit payload.projectId is required' });
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
if (!availableProjectIds.has(projectId)) {
|
|
366
|
+
skipped.push({ id: candidate.id, reason: `unresolved_project_reference: ${projectId} is not a canonical or selected project` });
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
341
369
|
const context = payloadString(payload, 'context') || payloadString(payload, 'email') || 'Imported from onboarding sources.';
|
|
342
370
|
const role = payloadString(payload, 'role');
|
|
343
|
-
|
|
371
|
+
const personId = `person-${stableHash(person)}`;
|
|
372
|
+
writeEntity(graphEntities, canonicalWrites.entities, { id: personId, type: 'person', name: person, summary: context, tags: ['relationship'] });
|
|
373
|
+
const edge = buildCanonicalEdge({
|
|
374
|
+
fromId: personId,
|
|
375
|
+
relation: 'participates_in',
|
|
376
|
+
toId: projectId,
|
|
377
|
+
role: role || undefined,
|
|
378
|
+
context,
|
|
379
|
+
provenance: { sourceKind: provenanceSourceKind, sourceId: candidate.id }
|
|
380
|
+
});
|
|
381
|
+
writeEdge(graphEdges, canonicalWrites.edges, edge);
|
|
382
|
+
const relationshipId = `relationship-${stableHash(`${person}|${context}`)}`;
|
|
344
383
|
if (candidate.kind === 'relationship' && !relationships.some((relationship) => relationship.person === person && relationship.context === context)) {
|
|
345
|
-
relationships.push({ id:
|
|
384
|
+
relationships.push({ id: relationshipId, person, role: role || undefined, context, tags: ['relationship'], updatedAt: now });
|
|
346
385
|
}
|
|
347
386
|
applied.push({
|
|
348
387
|
id: candidate.id,
|
|
349
388
|
kind: candidate.kind,
|
|
350
389
|
summary: person,
|
|
351
390
|
canonicalIds: candidate.kind === 'relationship'
|
|
352
|
-
? [
|
|
353
|
-
: [
|
|
391
|
+
? [personId, edge.id, relationshipId]
|
|
392
|
+
: [personId, edge.id]
|
|
354
393
|
});
|
|
355
394
|
break;
|
|
356
395
|
}
|
|
@@ -360,7 +399,7 @@ export function planApply(candidates, selection, base, now) {
|
|
|
360
399
|
skipped.push({ id: candidate.id, reason: 'org candidate missing name' });
|
|
361
400
|
break;
|
|
362
401
|
}
|
|
363
|
-
|
|
402
|
+
writeEntity(graphEntities, canonicalWrites.entities, { id: `org-${stableHash(name)}`, type: 'org', name, summary: 'Imported from onboarding sources.', tags: ['org'] });
|
|
364
403
|
applied.push({ id: candidate.id, kind: candidate.kind, summary: name, canonicalIds: [`org-${stableHash(name)}`] });
|
|
365
404
|
break;
|
|
366
405
|
}
|
|
@@ -370,8 +409,8 @@ export function planApply(candidates, selection, base, now) {
|
|
|
370
409
|
skipped.push({ id: candidate.id, reason: 'project candidate missing name' });
|
|
371
410
|
break;
|
|
372
411
|
}
|
|
373
|
-
|
|
374
|
-
personalKgAdditions.push({ id: `work-${stableHash(name)}`, type: 'work', text: name, tags: ['work'], source: candidate.id, updatedAt: now });
|
|
412
|
+
writeEntity(graphEntities, canonicalWrites.entities, { id: `project-${stableHash(name)}`, type: 'project', name, summary: 'Imported from onboarding sources.', tags: ['work'] });
|
|
413
|
+
personalKgAdditions.push(reuseById(base.personalKg, { id: `work-${stableHash(name)}`, type: 'work', text: name, tags: ['work'], source: candidate.id, updatedAt: now }));
|
|
375
414
|
applied.push({ id: candidate.id, kind: candidate.kind, summary: name, canonicalIds: [`project-${stableHash(name)}`, `work-${stableHash(name)}`] });
|
|
376
415
|
break;
|
|
377
416
|
}
|
|
@@ -381,7 +420,7 @@ export function planApply(candidates, selection, base, now) {
|
|
|
381
420
|
skipped.push({ id: candidate.id, reason: 'value candidate missing text' });
|
|
382
421
|
break;
|
|
383
422
|
}
|
|
384
|
-
personalKgAdditions.push({ id: `value-${stableHash(text)}`, type: 'value', text, tags: ['onboarding'], source: candidate.id, updatedAt: now });
|
|
423
|
+
personalKgAdditions.push(reuseById(base.personalKg, { id: `value-${stableHash(text)}`, type: 'value', text, tags: ['onboarding'], source: candidate.id, updatedAt: now }));
|
|
385
424
|
applied.push({ id: candidate.id, kind: candidate.kind, summary: text, canonicalIds: [`value-${stableHash(text)}`] });
|
|
386
425
|
break;
|
|
387
426
|
}
|
|
@@ -391,7 +430,7 @@ export function planApply(candidates, selection, base, now) {
|
|
|
391
430
|
skipped.push({ id: candidate.id, reason: 'next_action candidate missing text' });
|
|
392
431
|
break;
|
|
393
432
|
}
|
|
394
|
-
personalKgAdditions.push({ id: `next-action-${stableHash(text)}`, type: 'work', text, tags: ['next-action'], source: candidate.id, updatedAt: now });
|
|
433
|
+
personalKgAdditions.push(reuseById(base.personalKg, { id: `next-action-${stableHash(text)}`, type: 'work', text, tags: ['next-action'], source: candidate.id, updatedAt: now }));
|
|
395
434
|
applied.push({ id: candidate.id, kind: candidate.kind, summary: text, canonicalIds: [`next-action-${stableHash(text)}`] });
|
|
396
435
|
break;
|
|
397
436
|
}
|
|
@@ -401,14 +440,30 @@ export function planApply(candidates, selection, base, now) {
|
|
|
401
440
|
skipped.push({ id: candidate.id, reason: 'decision candidate missing decision text' });
|
|
402
441
|
break;
|
|
403
442
|
}
|
|
443
|
+
const projectId = payloadString(payload, 'projectId');
|
|
444
|
+
if (!projectId) {
|
|
445
|
+
skipped.push({ id: candidate.id, reason: 'unresolved_project_reference: explicit payload.projectId is required' });
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
if (!availableProjectIds.has(projectId)) {
|
|
449
|
+
skipped.push({ id: candidate.id, reason: `unresolved_project_reference: ${projectId} is not a canonical or selected project` });
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
404
452
|
const topic = payloadString(payload, 'topic');
|
|
405
453
|
const effectiveAt = payloadString(payload, 'effectiveAt');
|
|
406
454
|
const rationale = payloadString(payload, 'rationale');
|
|
407
455
|
const supersedes = readStringArray(payload, ['supersedes']);
|
|
456
|
+
const unresolvedSupersedes = supersedes.find((id) => !availableDecisionIds.has(id));
|
|
457
|
+
if (unresolvedSupersedes) {
|
|
458
|
+
skipped.push({ id: candidate.id, reason: `unresolved_decision_reference: ${unresolvedSupersedes} is not a canonical or selected decision` });
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
408
461
|
const tags = readStringArray(payload, ['tags']);
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
462
|
+
const decisionId = `decision-${stableHash(text)}`;
|
|
463
|
+
const title = payloadString(payload, 'title') || 'Promoted decision principle';
|
|
464
|
+
decisionAdditions.push(reuseById(base.decisions, {
|
|
465
|
+
id: decisionId,
|
|
466
|
+
title,
|
|
412
467
|
decision: text,
|
|
413
468
|
topic: topic || undefined,
|
|
414
469
|
supersedes: supersedes.length > 0 ? supersedes : undefined,
|
|
@@ -416,15 +471,45 @@ export function planApply(candidates, selection, base, now) {
|
|
|
416
471
|
rationale: rationale || undefined,
|
|
417
472
|
tags: tags.length > 0 ? tags : ['principle'],
|
|
418
473
|
updatedAt: now
|
|
474
|
+
}));
|
|
475
|
+
writeEntity(graphEntities, canonicalWrites.entities, {
|
|
476
|
+
id: decisionId,
|
|
477
|
+
type: 'decision',
|
|
478
|
+
name: title,
|
|
479
|
+
summary: text,
|
|
480
|
+
tags: tags.length > 0 ? tags : ['principle'],
|
|
481
|
+
validFrom: effectiveAt || undefined
|
|
482
|
+
});
|
|
483
|
+
const governs = buildCanonicalEdge({
|
|
484
|
+
fromId: decisionId,
|
|
485
|
+
relation: 'governs',
|
|
486
|
+
toId: projectId,
|
|
487
|
+
validFrom: effectiveAt || undefined,
|
|
488
|
+
provenance: { sourceKind: provenanceSourceKind, sourceId: candidate.id }
|
|
419
489
|
});
|
|
420
|
-
|
|
490
|
+
writeEdge(graphEdges, canonicalWrites.edges, governs);
|
|
491
|
+
const canonicalIds = [decisionId, governs.id];
|
|
492
|
+
for (const supersededId of supersedes) {
|
|
493
|
+
if (!availableDecisionIds.has(supersededId))
|
|
494
|
+
continue;
|
|
495
|
+
const supersedesEdge = buildCanonicalEdge({
|
|
496
|
+
fromId: decisionId,
|
|
497
|
+
relation: 'supersedes',
|
|
498
|
+
toId: supersededId,
|
|
499
|
+
validFrom: effectiveAt || undefined,
|
|
500
|
+
provenance: { sourceKind: provenanceSourceKind, sourceId: candidate.id }
|
|
501
|
+
});
|
|
502
|
+
writeEdge(graphEdges, canonicalWrites.edges, supersedesEdge);
|
|
503
|
+
canonicalIds.push(supersedesEdge.id);
|
|
504
|
+
}
|
|
505
|
+
applied.push({ id: candidate.id, kind: candidate.kind, summary: text, canonicalIds });
|
|
421
506
|
break;
|
|
422
507
|
}
|
|
423
508
|
default:
|
|
424
509
|
skipped.push({ id: candidate.id, reason: `unsupported candidate kind: ${candidate.kind}` });
|
|
425
510
|
}
|
|
426
511
|
}
|
|
427
|
-
return { graphEntities, relationships, personalKgAdditions, decisionAdditions, ownerName, applied, skipped };
|
|
512
|
+
return { graphEntities, graphEdges, canonicalWrites, relationships, personalKgAdditions, decisionAdditions, ownerName, applied, skipped };
|
|
428
513
|
}
|
|
429
514
|
// ---------------------------------------------------------------------------
|
|
430
515
|
// Helpers
|
|
@@ -545,12 +630,52 @@ function payloadString(payload, key) {
|
|
|
545
630
|
function upsertEntity(entities, entity) {
|
|
546
631
|
const index = entities.findIndex((candidate) => candidate.id === entity.id);
|
|
547
632
|
if (index >= 0) {
|
|
633
|
+
assertCompatibleCanonicalIdentity(entities[index], entity);
|
|
548
634
|
entities[index] = { ...entities[index], ...entity };
|
|
549
635
|
}
|
|
550
636
|
else {
|
|
551
637
|
entities.push(entity);
|
|
552
638
|
}
|
|
553
639
|
}
|
|
640
|
+
function assertCompatibleCanonicalIdentity(existing, proposed) {
|
|
641
|
+
const existingIdentity = canonicalIdentity(existing);
|
|
642
|
+
const proposedIdentity = canonicalIdentity(proposed);
|
|
643
|
+
if (existingIdentity === proposedIdentity)
|
|
644
|
+
return;
|
|
645
|
+
const labels = [canonicalIdentityLabel(existing), canonicalIdentityLabel(proposed)].sort((left, right) => left.localeCompare(right, 'en'));
|
|
646
|
+
throw new Error(`canonical_id_collision: ${proposed.id} maps to distinct semantic identities: ${labels.join(' / ')}`);
|
|
647
|
+
}
|
|
648
|
+
function canonicalIdentity(entity) {
|
|
649
|
+
return JSON.stringify(entity.type === 'decision'
|
|
650
|
+
? { type: entity.type, decision: entity.summary ?? entity.name }
|
|
651
|
+
: { type: entity.type, name: entity.name });
|
|
652
|
+
}
|
|
653
|
+
function canonicalIdentityLabel(entity) {
|
|
654
|
+
return entity.type === 'decision' ? entity.summary ?? entity.name : entity.name;
|
|
655
|
+
}
|
|
656
|
+
function writeEntity(entities, writes, entity) {
|
|
657
|
+
upsertEntity(entities, entity);
|
|
658
|
+
upsertEntity(writes, entity);
|
|
659
|
+
}
|
|
660
|
+
function writeEdge(edges, writes, edge) {
|
|
661
|
+
upsertRecord(edges, edge);
|
|
662
|
+
upsertRecord(writes, edge);
|
|
663
|
+
}
|
|
664
|
+
function upsertRecord(records, record) {
|
|
665
|
+
const index = records.findIndex((candidate) => candidate.id === record.id);
|
|
666
|
+
if (index >= 0)
|
|
667
|
+
records[index] = record;
|
|
668
|
+
else
|
|
669
|
+
records.push(record);
|
|
670
|
+
}
|
|
671
|
+
function reuseById(existing, addition) {
|
|
672
|
+
const found = existing.find((item) => item.id === addition.id);
|
|
673
|
+
if (!found)
|
|
674
|
+
return addition;
|
|
675
|
+
return JSON.stringify({ ...found, updatedAt: undefined }) === JSON.stringify({ ...addition, updatedAt: undefined })
|
|
676
|
+
? found
|
|
677
|
+
: addition;
|
|
678
|
+
}
|
|
554
679
|
function truncate(value, max) {
|
|
555
680
|
if (value.length <= max) {
|
|
556
681
|
return value;
|
package/dist/onboarding.js
CHANGED
|
@@ -45,7 +45,7 @@ export function buildAgentOnboardingProtocol() {
|
|
|
45
45
|
questions: [
|
|
46
46
|
'Optionally run brainbase onboard:demo to preview which saved context should be used.',
|
|
47
47
|
'Install the MCP config after user approval and restart the selected agent.',
|
|
48
|
-
'Send the real request through that agent and require get_context/search evidence.',
|
|
48
|
+
'Send the real request through that agent and require resolve_entity/get_context/search evidence.',
|
|
49
49
|
'Ask the user whether the actual answer was useful. A CLI sample or synthetic evaluator is not onboarding completion.'
|
|
50
50
|
]
|
|
51
51
|
},
|
|
@@ -73,7 +73,7 @@ export function buildAgentOnboardingProtocol() {
|
|
|
73
73
|
'brainbase onboard:seed --name "<name>" --value "<what should not be re-explained>" --project "<current project>" --relationship "<person>|<role>|<context>"',
|
|
74
74
|
'brainbase onboard:projects --name "<project>" --goal "<goal>" --status "<status>" --role "<your role>"',
|
|
75
75
|
'brainbase onboard:install --target codex --dry-run',
|
|
76
|
-
'Restart the selected agent, then send the real request and require Brainbase get_context/search.',
|
|
76
|
+
'Restart the selected agent, then send the real request and require Brainbase resolve_entity/get_context/search.',
|
|
77
77
|
'Ask the user whether the actual answer was useful.',
|
|
78
78
|
'brainbase onboard:demo --scenario "<optional local preview>"',
|
|
79
79
|
'brainbase onboard:skills --target codex',
|
|
@@ -86,9 +86,9 @@ export function buildAgentOnboardingProtocol() {
|
|
|
86
86
|
'Personal OS directory exists.',
|
|
87
87
|
'The approved self, work, and person context are saved locally.',
|
|
88
88
|
'The CLI sample, if used, is labeled as a preview rather than an onboarding completion signal.',
|
|
89
|
-
'The completion report lists public skills, ohayo/oyasumi/retro routines, real MCP config merge, source allowlists, and MCP get_context/search verification as unfinished operationalization work.',
|
|
89
|
+
'The completion report lists public skills, ohayo/oyasumi/retro routines, real MCP config merge, source allowlists, and MCP resolve_entity/get_context/search verification as unfinished operationalization work.',
|
|
90
90
|
'The selected MCP client has a Brainbase config snippet merged into its real config after user approval.',
|
|
91
|
-
'A fresh agent session used Brainbase get_context/search for the real request.',
|
|
91
|
+
'A fresh agent session used Brainbase resolve_entity/get_context/search for the real request.',
|
|
92
92
|
'The user personally confirmed that the actual agent answer was useful.',
|
|
93
93
|
'Source diagnosis is optional follow-up work, but source allowlist/import/candidate review must be explicitly completed or deferred.'
|
|
94
94
|
]
|
|
@@ -123,7 +123,7 @@ export function buildValueDemo(input) {
|
|
|
123
123
|
dataDir: input.os.dataDir,
|
|
124
124
|
firstValueReady: ready
|
|
125
125
|
});
|
|
126
|
-
const actualAgentPrompt = `Brainbaseのget_context
|
|
126
|
+
const actualAgentPrompt = `Brainbaseのresolve_entity、get_context、searchを使い、保存済み文脈を根拠に「${scenario}」へ回答してください。使った正規ID、関係、前提、未確認事項を分けてください。`;
|
|
127
127
|
return {
|
|
128
128
|
goal: '実エージェント接続前に、保存した文脈から作れる出力をプレビューする。',
|
|
129
129
|
ready,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { DecisionRecord, GraphFile, GraphFileV2, RelationshipsFile } from './types.js';
|
|
2
|
+
export type MigrationIssueCode = 'ambiguous_person' | 'unresolved_person' | 'missing_project_evidence' | 'missing_project_endpoint' | 'ambiguous_decision_project' | 'missing_decision_endpoint' | 'decision_supersedes_self' | 'decision_supersedes_cycle' | 'unsupported_relationship_entity' | 'expected_input_digest_required' | 'input_digest_mismatch';
|
|
3
|
+
export interface MigrationIssue {
|
|
4
|
+
code: MigrationIssueCode;
|
|
5
|
+
recordId: string;
|
|
6
|
+
detail: string;
|
|
7
|
+
}
|
|
8
|
+
export interface CanonicalGraphMigrationInput {
|
|
9
|
+
graph: GraphFile;
|
|
10
|
+
relationships?: RelationshipsFile;
|
|
11
|
+
decisions?: DecisionRecord[];
|
|
12
|
+
}
|
|
13
|
+
export interface CanonicalGraphMigrationPlan {
|
|
14
|
+
status: 'migration_required' | 'up_to_date' | 'blocked';
|
|
15
|
+
graph: GraphFileV2;
|
|
16
|
+
issues: MigrationIssue[];
|
|
17
|
+
inputDigest: string;
|
|
18
|
+
outputDigest: string;
|
|
19
|
+
changes: {
|
|
20
|
+
entitiesAdded: number;
|
|
21
|
+
edgesAdded: number;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Produces a side-effect-free v1 -> v2 migration preview.
|
|
26
|
+
*
|
|
27
|
+
* Only explicit IDs and unique exact-name matches are promoted. Free-form
|
|
28
|
+
* context is never interpreted as a canonical relationship.
|
|
29
|
+
*/
|
|
30
|
+
export declare function planCanonicalGraphMigration(input: CanonicalGraphMigrationInput): CanonicalGraphMigrationPlan;
|