@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
package/dist/projects.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildCanonicalEdge } from './canonical-edge-builder.js';
|
|
1
2
|
export function parseProjectSource(value) {
|
|
2
3
|
const [areaRaw, labelRaw, refRaw] = value.split('|').map((part) => part.trim());
|
|
3
4
|
const area = parseProjectSourceArea(areaRaw);
|
|
@@ -74,6 +75,43 @@ export function buildProjectRegistrationPlan(input) {
|
|
|
74
75
|
tags: ['project', 'principle'],
|
|
75
76
|
updatedAt: now
|
|
76
77
|
}));
|
|
78
|
+
const decisionEntities = decisions.map((decision) => ({
|
|
79
|
+
id: decision.id,
|
|
80
|
+
type: 'decision',
|
|
81
|
+
name: decision.title,
|
|
82
|
+
summary: decision.decision,
|
|
83
|
+
tags: decision.tags,
|
|
84
|
+
metadata: {
|
|
85
|
+
rationale: decision.rationale,
|
|
86
|
+
projectId
|
|
87
|
+
}
|
|
88
|
+
}));
|
|
89
|
+
const canonicalEntities = [
|
|
90
|
+
{ ...projectEntity, type: 'project' },
|
|
91
|
+
...stakeholderEntities.map((entity) => ({ ...entity, type: 'person' })),
|
|
92
|
+
...decisionEntities
|
|
93
|
+
];
|
|
94
|
+
const participationEdges = stakeholders.map((stakeholder) => {
|
|
95
|
+
const personId = `person-${stableHash(stakeholder.person)}`;
|
|
96
|
+
return buildCanonicalEdge({
|
|
97
|
+
fromId: personId,
|
|
98
|
+
relation: 'participates_in',
|
|
99
|
+
toId: projectId,
|
|
100
|
+
role: stakeholder.role,
|
|
101
|
+
context: stakeholder.context,
|
|
102
|
+
provenance: {
|
|
103
|
+
sourceKind: 'onboarding',
|
|
104
|
+
sourceId: `relationship-${stableHash(`${name}|${stakeholder.person}|${stakeholder.context}`)}`
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
const governanceEdges = decisions.map((decision) => buildCanonicalEdge({
|
|
109
|
+
fromId: decision.id,
|
|
110
|
+
relation: 'governs',
|
|
111
|
+
toId: projectId,
|
|
112
|
+
context: decision.decision,
|
|
113
|
+
provenance: { sourceKind: 'onboarding', sourceId: decision.id }
|
|
114
|
+
}));
|
|
77
115
|
return {
|
|
78
116
|
goal: 'Review project registration before promoting it into canonical Brainbase SSOT.',
|
|
79
117
|
canonicalWrites: false,
|
|
@@ -91,6 +129,8 @@ export function buildProjectRegistrationPlan(input) {
|
|
|
91
129
|
stakeholders,
|
|
92
130
|
writes: {
|
|
93
131
|
graphEntities: [projectEntity, ...stakeholderEntities],
|
|
132
|
+
canonicalEntities,
|
|
133
|
+
canonicalEdges: [...participationEdges, ...governanceEdges],
|
|
94
134
|
relationships,
|
|
95
135
|
personalKg,
|
|
96
136
|
decisions
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CanonicalEntityKind, CoreRelation } from './types.js';
|
|
2
|
+
export interface CanonicalRelationDefinition {
|
|
3
|
+
id: CoreRelation;
|
|
4
|
+
from: CanonicalEntityKind;
|
|
5
|
+
to: CanonicalEntityKind;
|
|
6
|
+
meaning: string;
|
|
7
|
+
scopeTraversal: 'none' | 'project_direct' | 'project_transitive';
|
|
8
|
+
traversalDirection: 'none' | 'forward' | 'reverse';
|
|
9
|
+
}
|
|
10
|
+
export declare const canonicalRelationRegistry: Readonly<Record<CoreRelation, Readonly<CanonicalRelationDefinition>>>;
|
|
11
|
+
export declare function getCanonicalRelation(relation: string): Readonly<CanonicalRelationDefinition>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const definitions = {
|
|
2
|
+
member_of: {
|
|
3
|
+
id: 'member_of',
|
|
4
|
+
from: 'person',
|
|
5
|
+
to: 'org',
|
|
6
|
+
meaning: 'A person is a member of an organization.',
|
|
7
|
+
scopeTraversal: 'none',
|
|
8
|
+
traversalDirection: 'none'
|
|
9
|
+
},
|
|
10
|
+
participates_in: {
|
|
11
|
+
id: 'participates_in',
|
|
12
|
+
from: 'person',
|
|
13
|
+
to: 'project',
|
|
14
|
+
meaning: 'A person participates in a project.',
|
|
15
|
+
scopeTraversal: 'project_direct',
|
|
16
|
+
traversalDirection: 'forward'
|
|
17
|
+
},
|
|
18
|
+
accountable_for: {
|
|
19
|
+
id: 'accountable_for',
|
|
20
|
+
from: 'person',
|
|
21
|
+
to: 'project',
|
|
22
|
+
meaning: 'A person is accountable for a project outcome or decision.',
|
|
23
|
+
scopeTraversal: 'project_direct',
|
|
24
|
+
traversalDirection: 'forward'
|
|
25
|
+
},
|
|
26
|
+
owned_by: {
|
|
27
|
+
id: 'owned_by',
|
|
28
|
+
from: 'project',
|
|
29
|
+
to: 'org',
|
|
30
|
+
meaning: 'A project is owned by an organization.',
|
|
31
|
+
scopeTraversal: 'project_direct',
|
|
32
|
+
traversalDirection: 'reverse'
|
|
33
|
+
},
|
|
34
|
+
governs: {
|
|
35
|
+
id: 'governs',
|
|
36
|
+
from: 'decision',
|
|
37
|
+
to: 'project',
|
|
38
|
+
meaning: 'A durable decision or principle governs a project.',
|
|
39
|
+
scopeTraversal: 'project_direct',
|
|
40
|
+
traversalDirection: 'forward'
|
|
41
|
+
},
|
|
42
|
+
supersedes: {
|
|
43
|
+
id: 'supersedes',
|
|
44
|
+
from: 'decision',
|
|
45
|
+
to: 'decision',
|
|
46
|
+
meaning: 'A durable decision explicitly supersedes another decision.',
|
|
47
|
+
scopeTraversal: 'project_transitive',
|
|
48
|
+
traversalDirection: 'forward'
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
export const canonicalRelationRegistry = deepFreeze(definitions);
|
|
52
|
+
export function getCanonicalRelation(relation) {
|
|
53
|
+
const definition = canonicalRelationRegistry[relation];
|
|
54
|
+
if (!definition) {
|
|
55
|
+
throw new Error(`ONTOLOGY-RELATION-UNKNOWN: unsupported canonical relation ${JSON.stringify(relation)}`);
|
|
56
|
+
}
|
|
57
|
+
return definition;
|
|
58
|
+
}
|
|
59
|
+
function deepFreeze(value) {
|
|
60
|
+
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
61
|
+
Object.freeze(value);
|
|
62
|
+
for (const nested of Object.values(value))
|
|
63
|
+
deepFreeze(nested);
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}
|
package/dist/server.d.ts
CHANGED
|
@@ -8,6 +8,15 @@ export declare const toolDefinitions: readonly [{
|
|
|
8
8
|
readonly dataDir: {
|
|
9
9
|
readonly type: "string";
|
|
10
10
|
};
|
|
11
|
+
readonly project: {
|
|
12
|
+
readonly type: "string";
|
|
13
|
+
readonly description: "Optional canonical project ID, name, or alias.";
|
|
14
|
+
};
|
|
15
|
+
readonly as_of: {
|
|
16
|
+
readonly type: "string";
|
|
17
|
+
readonly format: "date-time";
|
|
18
|
+
readonly description: "RFC 3339 validity instant. Defaults to now.";
|
|
19
|
+
};
|
|
11
20
|
};
|
|
12
21
|
};
|
|
13
22
|
}, {
|
|
@@ -40,6 +49,15 @@ export declare const toolDefinitions: readonly [{
|
|
|
40
49
|
readonly limit: {
|
|
41
50
|
readonly type: "number";
|
|
42
51
|
};
|
|
52
|
+
readonly project: {
|
|
53
|
+
readonly type: "string";
|
|
54
|
+
readonly description: "Optional canonical project ID, name, or alias.";
|
|
55
|
+
};
|
|
56
|
+
readonly as_of: {
|
|
57
|
+
readonly type: "string";
|
|
58
|
+
readonly format: "date-time";
|
|
59
|
+
readonly description: "RFC 3339 validity instant. Defaults to now.";
|
|
60
|
+
};
|
|
43
61
|
};
|
|
44
62
|
};
|
|
45
63
|
}, {
|
|
@@ -380,7 +398,7 @@ export declare const toolDefinitions: readonly [{
|
|
|
380
398
|
readonly type: "string";
|
|
381
399
|
};
|
|
382
400
|
readonly ontologyVersion: {
|
|
383
|
-
readonly enum: readonly ["0.0.0", "1.0.0"];
|
|
401
|
+
readonly enum: readonly ["0.0.0", "1.0.0", "2.0.0"];
|
|
384
402
|
};
|
|
385
403
|
};
|
|
386
404
|
};
|
|
@@ -398,7 +416,7 @@ export declare const toolDefinitions: readonly [{
|
|
|
398
416
|
readonly format: "date-time";
|
|
399
417
|
};
|
|
400
418
|
readonly ontologyVersion: {
|
|
401
|
-
readonly enum: readonly ["0.0.0", "1.0.0"];
|
|
419
|
+
readonly enum: readonly ["0.0.0", "1.0.0", "2.0.0"];
|
|
402
420
|
};
|
|
403
421
|
};
|
|
404
422
|
};
|
|
@@ -413,6 +431,72 @@ export declare const toolDefinitions: readonly [{
|
|
|
413
431
|
};
|
|
414
432
|
};
|
|
415
433
|
};
|
|
434
|
+
}, {
|
|
435
|
+
readonly name: "resolve_entity";
|
|
436
|
+
readonly description: "Resolve mentions in text to canonical Graph v2 entity IDs and return a privacy-safe evidence receipt.";
|
|
437
|
+
readonly inputSchema: {
|
|
438
|
+
readonly type: "object";
|
|
439
|
+
readonly required: readonly ["text", "asOf"];
|
|
440
|
+
readonly additionalProperties: false;
|
|
441
|
+
readonly properties: {
|
|
442
|
+
readonly dataDir: {
|
|
443
|
+
readonly type: "string";
|
|
444
|
+
readonly description: "Optional Personal OS directory.";
|
|
445
|
+
};
|
|
446
|
+
readonly text: {
|
|
447
|
+
readonly type: "string";
|
|
448
|
+
readonly description: "Text whose entity mentions should be resolved. The text is hashed, not stored in the receipt.";
|
|
449
|
+
};
|
|
450
|
+
readonly asOf: {
|
|
451
|
+
readonly type: "string";
|
|
452
|
+
readonly format: "date-time";
|
|
453
|
+
readonly description: "RFC 3339 instant used for temporal entity and edge validity.";
|
|
454
|
+
};
|
|
455
|
+
readonly mentionSpans: {
|
|
456
|
+
readonly type: "array";
|
|
457
|
+
readonly items: {
|
|
458
|
+
readonly type: "object";
|
|
459
|
+
readonly required: readonly ["start", "end"];
|
|
460
|
+
readonly additionalProperties: false;
|
|
461
|
+
readonly properties: {
|
|
462
|
+
readonly start: {
|
|
463
|
+
readonly type: "integer";
|
|
464
|
+
readonly minimum: 0;
|
|
465
|
+
};
|
|
466
|
+
readonly end: {
|
|
467
|
+
readonly type: "integer";
|
|
468
|
+
readonly minimum: 1;
|
|
469
|
+
};
|
|
470
|
+
};
|
|
471
|
+
};
|
|
472
|
+
};
|
|
473
|
+
readonly projectScope: {
|
|
474
|
+
readonly type: "object";
|
|
475
|
+
readonly required: readonly ["projectIds"];
|
|
476
|
+
readonly additionalProperties: false;
|
|
477
|
+
readonly properties: {
|
|
478
|
+
readonly projectIds: {
|
|
479
|
+
readonly type: "array";
|
|
480
|
+
readonly minItems: 1;
|
|
481
|
+
readonly items: {
|
|
482
|
+
readonly type: "string";
|
|
483
|
+
readonly minLength: 1;
|
|
484
|
+
};
|
|
485
|
+
};
|
|
486
|
+
readonly policy: {
|
|
487
|
+
readonly enum: readonly ["strict", "prefer_project", "allow_global_fallback"];
|
|
488
|
+
};
|
|
489
|
+
};
|
|
490
|
+
};
|
|
491
|
+
readonly entityTypes: {
|
|
492
|
+
readonly type: "array";
|
|
493
|
+
readonly minItems: 1;
|
|
494
|
+
readonly items: {
|
|
495
|
+
readonly enum: readonly ["person", "org", "project", "decision"];
|
|
496
|
+
};
|
|
497
|
+
};
|
|
498
|
+
};
|
|
499
|
+
};
|
|
416
500
|
}];
|
|
417
501
|
export declare function callBrainbaseTool(name: string, rawArgs?: unknown): Promise<unknown>;
|
|
418
502
|
export declare function createServer(): Server;
|
package/dist/server.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
1
4
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
5
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
6
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
4
7
|
import { z } from 'zod';
|
|
5
8
|
import { ConnectedOnboardingRuntime } from './connected-onboarding.js';
|
|
9
|
+
import { validateCanonicalGraph } from './canonical-graph.js';
|
|
10
|
+
import { resolveText } from './entity-resolution.js';
|
|
6
11
|
import { resolveDataDir } from './paths.js';
|
|
7
12
|
import { auditPersonalOsDirectory } from './ontology-ssot.js';
|
|
8
13
|
import { getOntologyImpact, inferPersonalOs, portableOntology, resolveOntologyVersion } from './ontology.js';
|
|
@@ -15,7 +20,34 @@ const argsSchema = z.object({
|
|
|
15
20
|
type: z.enum(['person', 'org', 'project', 'relationship', 'decision']).optional(),
|
|
16
21
|
fromVersion: z.string().optional(),
|
|
17
22
|
ontologyVersion: z.string().optional(),
|
|
18
|
-
asOf: z.string().datetime({ offset: true }).optional()
|
|
23
|
+
asOf: z.string().datetime({ offset: true }).optional(),
|
|
24
|
+
project: z.string().min(1).optional(),
|
|
25
|
+
as_of: z.string().datetime({ offset: true }).optional()
|
|
26
|
+
});
|
|
27
|
+
const mentionSpanSchema = z.object({
|
|
28
|
+
start: z.number().int().nonnegative(),
|
|
29
|
+
end: z.number().int().positive()
|
|
30
|
+
}).strict().refine((span) => span.end > span.start, { message: 'mention span end must be after start' });
|
|
31
|
+
const resolveEntitySchema = z.object({
|
|
32
|
+
dataDir: z.string().optional(),
|
|
33
|
+
text: z.string(),
|
|
34
|
+
asOf: z.string().datetime({ offset: true }),
|
|
35
|
+
mentionSpans: z.array(mentionSpanSchema).optional(),
|
|
36
|
+
projectScope: z.object({
|
|
37
|
+
projectIds: z.array(z.string().min(1)).min(1),
|
|
38
|
+
policy: z.enum(['strict', 'prefer_project', 'allow_global_fallback']).optional()
|
|
39
|
+
}).strict().optional(),
|
|
40
|
+
entityTypes: z.array(z.enum(['person', 'org', 'project', 'decision'])).min(1).optional()
|
|
41
|
+
}).strict().superRefine((value, context) => {
|
|
42
|
+
for (const [index, span] of (value.mentionSpans ?? []).entries()) {
|
|
43
|
+
if (span.end > value.text.length) {
|
|
44
|
+
context.addIssue({
|
|
45
|
+
code: z.ZodIssueCode.custom,
|
|
46
|
+
path: ['mentionSpans', index, 'end'],
|
|
47
|
+
message: 'mention span must be within text'
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
19
51
|
});
|
|
20
52
|
const sourceInventorySchema = z.object({
|
|
21
53
|
id: z.string(),
|
|
@@ -89,7 +121,9 @@ export const toolDefinitions = [
|
|
|
89
121
|
inputSchema: {
|
|
90
122
|
type: 'object',
|
|
91
123
|
properties: {
|
|
92
|
-
dataDir: { type: 'string' }
|
|
124
|
+
dataDir: { type: 'string' },
|
|
125
|
+
project: { type: 'string', description: 'Optional canonical project ID, name, or alias.' },
|
|
126
|
+
as_of: { type: 'string', format: 'date-time', description: 'RFC 3339 validity instant. Defaults to now.' }
|
|
93
127
|
}
|
|
94
128
|
}
|
|
95
129
|
},
|
|
@@ -113,7 +147,9 @@ export const toolDefinitions = [
|
|
|
113
147
|
properties: {
|
|
114
148
|
dataDir: { type: 'string' },
|
|
115
149
|
query: { type: 'string' },
|
|
116
|
-
limit: { type: 'number' }
|
|
150
|
+
limit: { type: 'number' },
|
|
151
|
+
project: { type: 'string', description: 'Optional canonical project ID, name, or alias.' },
|
|
152
|
+
as_of: { type: 'string', format: 'date-time', description: 'RFC 3339 validity instant. Defaults to now.' }
|
|
117
153
|
}
|
|
118
154
|
}
|
|
119
155
|
},
|
|
@@ -230,7 +266,7 @@ export const toolDefinitions = [
|
|
|
230
266
|
type: 'object',
|
|
231
267
|
properties: {
|
|
232
268
|
dataDir: { type: 'string' },
|
|
233
|
-
ontologyVersion: { enum: ['0.0.0', '1.0.0'] }
|
|
269
|
+
ontologyVersion: { enum: ['0.0.0', '1.0.0', '2.0.0'] }
|
|
234
270
|
}
|
|
235
271
|
}
|
|
236
272
|
},
|
|
@@ -242,7 +278,7 @@ export const toolDefinitions = [
|
|
|
242
278
|
properties: {
|
|
243
279
|
dataDir: { type: 'string' },
|
|
244
280
|
asOf: { type: 'string', format: 'date-time' },
|
|
245
|
-
ontologyVersion: { enum: ['0.0.0', '1.0.0'] }
|
|
281
|
+
ontologyVersion: { enum: ['0.0.0', '1.0.0', '2.0.0'] }
|
|
246
282
|
}
|
|
247
283
|
}
|
|
248
284
|
},
|
|
@@ -255,12 +291,52 @@ export const toolDefinitions = [
|
|
|
255
291
|
fromVersion: { type: 'string' }
|
|
256
292
|
}
|
|
257
293
|
}
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
name: 'resolve_entity',
|
|
297
|
+
description: 'Resolve mentions in text to canonical Graph v2 entity IDs and return a privacy-safe evidence receipt.',
|
|
298
|
+
inputSchema: {
|
|
299
|
+
type: 'object',
|
|
300
|
+
required: ['text', 'asOf'],
|
|
301
|
+
additionalProperties: false,
|
|
302
|
+
properties: {
|
|
303
|
+
dataDir: { type: 'string', description: 'Optional Personal OS directory.' },
|
|
304
|
+
text: { type: 'string', description: 'Text whose entity mentions should be resolved. The text is hashed, not stored in the receipt.' },
|
|
305
|
+
asOf: { type: 'string', format: 'date-time', description: 'RFC 3339 instant used for temporal entity and edge validity.' },
|
|
306
|
+
mentionSpans: {
|
|
307
|
+
type: 'array',
|
|
308
|
+
items: {
|
|
309
|
+
type: 'object',
|
|
310
|
+
required: ['start', 'end'],
|
|
311
|
+
additionalProperties: false,
|
|
312
|
+
properties: { start: { type: 'integer', minimum: 0 }, end: { type: 'integer', minimum: 1 } }
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
projectScope: {
|
|
316
|
+
type: 'object',
|
|
317
|
+
required: ['projectIds'],
|
|
318
|
+
additionalProperties: false,
|
|
319
|
+
properties: {
|
|
320
|
+
projectIds: { type: 'array', minItems: 1, items: { type: 'string', minLength: 1 } },
|
|
321
|
+
policy: { enum: ['strict', 'prefer_project', 'allow_global_fallback'] }
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
entityTypes: {
|
|
325
|
+
type: 'array',
|
|
326
|
+
minItems: 1,
|
|
327
|
+
items: { enum: ['person', 'org', 'project', 'decision'] }
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
258
331
|
}
|
|
259
332
|
];
|
|
260
333
|
export async function callBrainbaseTool(name, rawArgs = {}) {
|
|
261
334
|
if (name in connectedSchemas) {
|
|
262
335
|
return callConnectedOnboardingTool(name, rawArgs);
|
|
263
336
|
}
|
|
337
|
+
if (name === 'resolve_entity') {
|
|
338
|
+
return callResolveEntityTool(rawArgs);
|
|
339
|
+
}
|
|
264
340
|
const args = argsSchema.parse(rawArgs ?? {});
|
|
265
341
|
const dataDir = resolveDataDir(args.dataDir);
|
|
266
342
|
switch (name) {
|
|
@@ -316,21 +392,25 @@ export async function callBrainbaseTool(name, rawArgs = {}) {
|
|
|
316
392
|
...portableOntology
|
|
317
393
|
};
|
|
318
394
|
case 'audit_ontology':
|
|
319
|
-
return auditPersonalOsDirectory(dataDir, {
|
|
395
|
+
return auditPersonalOsDirectory(dataDir, {
|
|
396
|
+
ontologyVersion: args.ontologyVersion === undefined
|
|
397
|
+
? undefined
|
|
398
|
+
: resolveOntologyVersion(args.ontologyVersion)
|
|
399
|
+
});
|
|
320
400
|
case 'ontology_impact':
|
|
321
401
|
return getOntologyImpact(args.fromVersion);
|
|
322
402
|
}
|
|
323
403
|
const os = await loadPersonalOs(dataDir);
|
|
324
404
|
switch (name) {
|
|
325
405
|
case 'get_context':
|
|
326
|
-
return getContext(os);
|
|
406
|
+
return getContext(os, { project: args.project, asOf: args.as_of ?? args.asOf });
|
|
327
407
|
case 'list_entities':
|
|
328
408
|
return listEntities(os, args.type);
|
|
329
409
|
case 'search':
|
|
330
410
|
if (!args.query) {
|
|
331
411
|
throw new Error('search requires query');
|
|
332
412
|
}
|
|
333
|
-
return { results: searchAll(os, args.query, args.limit) };
|
|
413
|
+
return { results: searchAll(os, args.query, args.limit, { project: args.project, asOf: args.as_of ?? args.asOf }) };
|
|
334
414
|
case 'search_personal_kg':
|
|
335
415
|
if (!args.query) {
|
|
336
416
|
throw new Error('search_personal_kg requires query');
|
|
@@ -341,12 +421,81 @@ export async function callBrainbaseTool(name, rawArgs = {}) {
|
|
|
341
421
|
case 'infer_decisions':
|
|
342
422
|
return inferPersonalOs(os, {
|
|
343
423
|
asOf: args.asOf,
|
|
344
|
-
ontologyVersion:
|
|
424
|
+
ontologyVersion: args.ontologyVersion === undefined
|
|
425
|
+
? undefined
|
|
426
|
+
: resolveOntologyVersion(args.ontologyVersion)
|
|
345
427
|
});
|
|
346
428
|
default:
|
|
347
429
|
throw new Error(`Unknown tool: ${name}`);
|
|
348
430
|
}
|
|
349
431
|
}
|
|
432
|
+
async function callResolveEntityTool(rawArgs) {
|
|
433
|
+
const args = resolveEntitySchema.parse(rawArgs ?? {});
|
|
434
|
+
const dataDir = resolveDataDir(args.dataDir);
|
|
435
|
+
const graphPath = join(dataDir, 'graph.json');
|
|
436
|
+
let serialized;
|
|
437
|
+
try {
|
|
438
|
+
serialized = await readFile(graphPath, 'utf8');
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
return blockedResolution(args, 'unavailable', 'graph_unavailable');
|
|
442
|
+
}
|
|
443
|
+
let graph;
|
|
444
|
+
try {
|
|
445
|
+
graph = JSON.parse(serialized);
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
return blockedResolution(args, 'invalid', 'graph_invalid');
|
|
449
|
+
}
|
|
450
|
+
try {
|
|
451
|
+
validateCanonicalGraph(graph);
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
return blockedResolution(args, 'invalid', 'graph_invalid');
|
|
455
|
+
}
|
|
456
|
+
if (isGraphVersion(graph, 1)) {
|
|
457
|
+
return {
|
|
458
|
+
status: 'migration_required',
|
|
459
|
+
graphSchemaVersion: 1,
|
|
460
|
+
requiredAction: 'migrate_graph_v2'
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
if (!isGraphVersion(graph, 2)) {
|
|
464
|
+
return blockedResolution(args, 'invalid', 'graph_invalid');
|
|
465
|
+
}
|
|
466
|
+
const receipt = resolveText({
|
|
467
|
+
text: args.text,
|
|
468
|
+
...(args.mentionSpans ? { mentionSpans: args.mentionSpans } : {}),
|
|
469
|
+
...(args.projectScope ? { projectScope: args.projectScope } : {}),
|
|
470
|
+
asOf: args.asOf,
|
|
471
|
+
...(args.entityTypes ? { entityTypes: args.entityTypes } : {}),
|
|
472
|
+
source: {
|
|
473
|
+
authority: 'local_graph',
|
|
474
|
+
status: 'complete',
|
|
475
|
+
revision: createHash('sha256').update(serialized).digest('hex'),
|
|
476
|
+
graph: graph
|
|
477
|
+
}
|
|
478
|
+
}).receipt;
|
|
479
|
+
return { status: 'verified', receipt };
|
|
480
|
+
}
|
|
481
|
+
function blockedResolution(args, sourceStatus, issueCode) {
|
|
482
|
+
const receipt = resolveText({
|
|
483
|
+
text: args.text,
|
|
484
|
+
...(args.mentionSpans ? { mentionSpans: args.mentionSpans } : {}),
|
|
485
|
+
...(args.projectScope ? { projectScope: args.projectScope } : {}),
|
|
486
|
+
asOf: args.asOf,
|
|
487
|
+
...(args.entityTypes ? { entityTypes: args.entityTypes } : {}),
|
|
488
|
+
source: {
|
|
489
|
+
authority: 'local_graph',
|
|
490
|
+
status: sourceStatus,
|
|
491
|
+
issues: [{ code: issueCode, message: issueCode }]
|
|
492
|
+
}
|
|
493
|
+
}).receipt;
|
|
494
|
+
return { status: 'unverified', receipt };
|
|
495
|
+
}
|
|
496
|
+
function isGraphVersion(graph, version) {
|
|
497
|
+
return Boolean(graph && typeof graph === 'object' && 'version' in graph && graph.version === version);
|
|
498
|
+
}
|
|
350
499
|
async function callConnectedOnboardingTool(name, rawArgs) {
|
|
351
500
|
const schema = connectedSchemas[name];
|
|
352
501
|
const args = schema.parse(rawArgs ?? {});
|
package/dist/skills.js
CHANGED
|
@@ -27,8 +27,8 @@ const SKILL_DEFINITIONS = {
|
|
|
27
27
|
'2. `brainbase onboard:start --target codex|claude|codecode` を実行し、本人、プロジェクト、関係者、判断基準の最小候補を確認する。',
|
|
28
28
|
'3. ユーザーが明示的に承認した事実だけを `brainbase onboard:seed` で保存する。',
|
|
29
29
|
'4. `brainbase onboard:install --target codex|claude|codecode --dry-run` でMCP設定を確認し、承認後に実設定へ反映して対象エージェントを再起動する。',
|
|
30
|
-
'5. 新しい実際のエージェントで、Brainbaseの `get_context
|
|
31
|
-
'6.
|
|
30
|
+
'5. 新しい実際のエージェントで、Brainbaseの `resolve_entity`、`get_context`、`search` を使うよう明示して最初の現実の依頼を実行する。',
|
|
31
|
+
'6. 実回答、使われた正規IDと関係、保存済み文脈、未確認事項をユーザーへ示し、本人に役立ったかを確認する。ここまでを10分以内の初回価値候補ジャーニーとして測る。',
|
|
32
32
|
'7. CLIの `onboard:demo` は必要なら接続前のプレビューに使えるが、CLIサンプルは初回価値の達成証拠にしない。',
|
|
33
33
|
'8. 本人が役立つと確認した後だけ、必要に応じてsource診断、追加候補、Skills、ルーティンへ進む。',
|
|
34
34
|
'9. 継続利用するプロジェクトの詳細が必要なら、本人確認後に `brainbase onboard:projects --write` で登録する。',
|
|
@@ -113,7 +113,7 @@ const SKILL_DEFINITIONS = {
|
|
|
113
113
|
'3. どの候補idを承認、却下、修正するかをユーザーに確認する。',
|
|
114
114
|
'4. まず `brainbase onboard:apply --from <candidate-file> --select <id>` でdry-runする。手入力の事実には `brainbase onboard:seed` を使う。',
|
|
115
115
|
'5. `--write` は明示的な承認後にだけ使い、canonical filesへ書き込む。',
|
|
116
|
-
'6. `brainbase doctor`
|
|
116
|
+
'6. `brainbase doctor` を実行し、Brainbase MCPの `resolve_entity`、`get_context`、`search` で承認済み事実と正規ID接続が見えることを確認する。',
|
|
117
117
|
'',
|
|
118
118
|
'## 安全ルール',
|
|
119
119
|
'',
|
package/dist/ssot.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CanonicalGraphMigrationPlan } from './ontology-migration.js';
|
|
1
2
|
import type { PersonalOs } from './types.js';
|
|
2
3
|
export declare function initializePersonalOs(dataDir: string): Promise<void>;
|
|
3
4
|
export declare function loadPersonalOs(dataDir: string): Promise<PersonalOs>;
|
|
@@ -11,3 +12,18 @@ export declare function mutatePersonalOsWithSidecar<T>(dataDir: string, sidecarP
|
|
|
11
12
|
sidecarContent: string;
|
|
12
13
|
result: T;
|
|
13
14
|
}>): Promise<T>;
|
|
15
|
+
export type CanonicalGraphMigrationExecution = CanonicalGraphMigrationPlan & {
|
|
16
|
+
expectedInputDigest: string;
|
|
17
|
+
written: boolean;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Plans or atomically applies the canonical Graph migration.
|
|
21
|
+
*
|
|
22
|
+
* The input is always recovered, read, and planned while holding the same
|
|
23
|
+
* lock used by every canonical SSOT writer. Omitting `write` is a byte-safe
|
|
24
|
+
* preview; a blocked or already-current plan is never committed.
|
|
25
|
+
*/
|
|
26
|
+
export declare function migrateCanonicalGraph(dataDir: string, options?: {
|
|
27
|
+
write?: boolean;
|
|
28
|
+
expectedInputDigest?: string;
|
|
29
|
+
}): Promise<CanonicalGraphMigrationExecution>;
|
package/dist/ssot.js
CHANGED
|
@@ -4,28 +4,14 @@ import { access, copyFile, mkdir, readFile, readdir, rename, rm, stat, writeFile
|
|
|
4
4
|
import { hostname } from 'node:os';
|
|
5
5
|
import { dirname, isAbsolute, join, relative, resolve, win32 } from 'node:path';
|
|
6
6
|
import { z } from 'zod';
|
|
7
|
+
import { validateCanonicalGraph } from './canonical-graph.js';
|
|
7
8
|
import { assertOntologyValid } from './ontology.js';
|
|
9
|
+
import { planCanonicalGraphMigration } from './ontology-migration.js';
|
|
8
10
|
import { emptyGraph, emptyRelationships, schemaTemplates } from './templates.js';
|
|
9
11
|
const canonicalFiles = ['graph.json', 'relationships.json', 'personal-kg.jsonl', 'decisions.jsonl'];
|
|
10
12
|
const lockName = '.brainbase-ssot.lock';
|
|
11
13
|
const stagingPrefix = '.brainbase-staging-';
|
|
12
14
|
const transactionPrefix = '.brainbase-transaction-';
|
|
13
|
-
const graphEntitySchema = z.object({
|
|
14
|
-
id: z.string().min(1),
|
|
15
|
-
type: z.enum(['person', 'org', 'project', 'relationship']),
|
|
16
|
-
name: z.string().min(1),
|
|
17
|
-
summary: z.string().optional(),
|
|
18
|
-
tags: z.array(z.string()).optional(),
|
|
19
|
-
metadata: z.record(z.unknown()).optional()
|
|
20
|
-
});
|
|
21
|
-
const graphSchema = z.object({
|
|
22
|
-
version: z.literal(1),
|
|
23
|
-
owner: z.object({
|
|
24
|
-
name: z.string().optional(),
|
|
25
|
-
summary: z.string().optional()
|
|
26
|
-
}).optional(),
|
|
27
|
-
entities: z.array(graphEntitySchema)
|
|
28
|
-
});
|
|
29
15
|
const personalKgSchema = z.object({
|
|
30
16
|
id: z.string().min(1),
|
|
31
17
|
type: z.enum(['self', 'work', 'relationship', 'value', 'judgment', 'experience', 'sns_context']),
|
|
@@ -112,8 +98,56 @@ export async function mutatePersonalOsWithSidecar(dataDir, sidecarPath, mutator)
|
|
|
112
98
|
return mutation.result;
|
|
113
99
|
});
|
|
114
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Plans or atomically applies the canonical Graph migration.
|
|
103
|
+
*
|
|
104
|
+
* The input is always recovered, read, and planned while holding the same
|
|
105
|
+
* lock used by every canonical SSOT writer. Omitting `write` is a byte-safe
|
|
106
|
+
* preview; a blocked or already-current plan is never committed.
|
|
107
|
+
*/
|
|
108
|
+
export async function migrateCanonicalGraph(dataDir, options = {}) {
|
|
109
|
+
return withSsotLock(dataDir, async () => {
|
|
110
|
+
await recoverTransactions(dataDir);
|
|
111
|
+
assertCompleteCanonicalSet(dataDir, await canonicalPresence(dataDir));
|
|
112
|
+
const current = await loadPersonalOsUnlocked(dataDir);
|
|
113
|
+
const plan = planCanonicalGraphMigration({
|
|
114
|
+
graph: current.graph,
|
|
115
|
+
relationships: current.relationships,
|
|
116
|
+
decisions: current.decisions
|
|
117
|
+
});
|
|
118
|
+
if (options.write && options.expectedInputDigest === undefined) {
|
|
119
|
+
return blockMigrationWrite(plan, {
|
|
120
|
+
code: 'expected_input_digest_required',
|
|
121
|
+
recordId: 'canonical-aggregate',
|
|
122
|
+
detail: 'MIGRATION-EXPECTED-INPUT-DIGEST-REQUIRED: preview first and pass its inputDigest before writing'
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
if (options.write && options.expectedInputDigest !== plan.inputDigest) {
|
|
126
|
+
return blockMigrationWrite(plan, {
|
|
127
|
+
code: 'input_digest_mismatch',
|
|
128
|
+
recordId: 'canonical-aggregate',
|
|
129
|
+
detail: `MIGRATION-INPUT-DIGEST-MISMATCH: expected ${options.expectedInputDigest}, replanned ${plan.inputDigest}`
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (!options.write || plan.status !== 'migration_required') {
|
|
133
|
+
return { ...plan, expectedInputDigest: plan.inputDigest, written: false };
|
|
134
|
+
}
|
|
135
|
+
const next = { ...current, graph: plan.graph };
|
|
136
|
+
await commitAggregate(dataDir, next, 'mutation');
|
|
137
|
+
return { ...plan, expectedInputDigest: plan.inputDigest, written: true };
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
function blockMigrationWrite(plan, issue) {
|
|
141
|
+
return {
|
|
142
|
+
...plan,
|
|
143
|
+
status: 'blocked',
|
|
144
|
+
issues: [...plan.issues, issue].sort((left, right) => (`${left.recordId}\u0000${left.code}`.localeCompare(`${right.recordId}\u0000${right.code}`, 'en'))),
|
|
145
|
+
expectedInputDigest: plan.inputDigest,
|
|
146
|
+
written: false
|
|
147
|
+
};
|
|
148
|
+
}
|
|
115
149
|
async function loadPersonalOsUnlocked(dataDir) {
|
|
116
|
-
const graph =
|
|
150
|
+
const graph = parseGraph(await readJson(join(dataDir, 'graph.json')));
|
|
117
151
|
const relationships = relationshipsSchema.parse(await readJson(join(dataDir, 'relationships.json')));
|
|
118
152
|
const personalKg = await readJsonl(join(dataDir, 'personal-kg.jsonl'), personalKgSchema, 'personal-kg.jsonl');
|
|
119
153
|
const decisions = await readJsonl(join(dataDir, 'decisions.jsonl'), decisionSchema, 'decisions.jsonl');
|
|
@@ -229,11 +263,27 @@ async function writeAggregate(targetDir, os) {
|
|
|
229
263
|
await writeFile(join(targetDir, 'decisions.jsonl'), serializeJsonl(os.decisions));
|
|
230
264
|
}
|
|
231
265
|
function validateAggregate(os) {
|
|
232
|
-
graphSchema.parse(os.graph);
|
|
233
266
|
relationshipsSchema.parse(os.relationships);
|
|
234
267
|
os.personalKg.forEach((entry) => personalKgSchema.parse(entry));
|
|
235
268
|
os.decisions.forEach((decision) => decisionSchema.parse(decision));
|
|
236
269
|
assertOntologyValid(os);
|
|
270
|
+
validateCanonicalGraph(os.graph);
|
|
271
|
+
}
|
|
272
|
+
function parseGraph(value) {
|
|
273
|
+
try {
|
|
274
|
+
validateCanonicalGraph(value);
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
// Duplicate entity IDs are a complete, readable snapshot whose ontology
|
|
278
|
+
// violation must remain available to audit/inference instead of being
|
|
279
|
+
// collapsed into a source-unavailable result. Writers still validate the
|
|
280
|
+
// aggregate strictly before commit.
|
|
281
|
+
if (!(error instanceof Error) || !error.message.startsWith('GRAPH-ENTITY-ID-UNIQUE')) {
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
284
|
+
validateCanonicalGraph(value, { allowDuplicateEntityIds: true });
|
|
285
|
+
}
|
|
286
|
+
return value;
|
|
237
287
|
}
|
|
238
288
|
function serializeJsonl(values) {
|
|
239
289
|
return values.length === 0 ? '' : `${values.map((value) => JSON.stringify(value)).join('\n')}\n`;
|