@unson/brainbase-mcp 0.2.4 → 0.3.1

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/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 = graphSchema.parse(await readJson(join(dataDir, 'graph.json')));
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`;
@@ -1,4 +1,112 @@
1
- import type { GraphFile, RelationshipsFile } from './types.js';
2
- export declare const emptyGraph: GraphFile;
1
+ import type { GraphFileV2, RelationshipsFile } from './types.js';
2
+ export declare const canonicalGraphOntologyRelease: {
3
+ manifest: {
4
+ id: "brainbase-personal-os";
5
+ version: "2.0.0";
6
+ ontology: {
7
+ readonly version: "2.0.0";
8
+ readonly effectiveAt: "2026-08-17T00:00:00.000Z";
9
+ readonly compatibility: "read-compatible-write-gated";
10
+ readonly name: "Brainbase Portable Ontology Kernel";
11
+ readonly description: "A local-first semantic contract for canonical Graph entities and ID-based edges.";
12
+ readonly domains: {
13
+ readonly types: {
14
+ readonly concepts: readonly [{
15
+ readonly id: "person";
16
+ readonly meaning: "A human represented in the local Graph.";
17
+ readonly usageConditions: readonly ["Use only for a human identity approved for the canonical local SSOT."];
18
+ }, {
19
+ readonly id: "org";
20
+ readonly meaning: "An organization represented in the local Graph.";
21
+ readonly usageConditions: readonly ["Use for a named organizational actor, not for a project or product."];
22
+ }, {
23
+ readonly id: "project";
24
+ readonly meaning: "A bounded body of work represented in the local Graph.";
25
+ readonly usageConditions: readonly ["Use when the entity has a bounded work objective; do not use it as an organization alias."];
26
+ }, {
27
+ readonly id: "relationship";
28
+ readonly meaning: "A contextual connection to a person.";
29
+ readonly usageConditions: readonly ["The person field must resolve to a canonical person entity by name."];
30
+ }, {
31
+ readonly id: "decision";
32
+ readonly meaning: "A durable choice that may explicitly supersede another choice.";
33
+ readonly usageConditions: readonly ["Use for an explicit durable choice; replacement requires a supersedes Decision ID."];
34
+ }];
35
+ };
36
+ readonly relations: {
37
+ readonly vocabulary: {
38
+ id: import("./types.js").CoreRelation;
39
+ source: import("./types.js").CanonicalEntityKind;
40
+ target: import("./types.js").CanonicalEntityKind;
41
+ }[];
42
+ };
43
+ readonly constraints: {
44
+ readonly rules: readonly [{
45
+ readonly id: "ONT-ENTITY-ID-UNIQUE";
46
+ readonly severity: "error";
47
+ readonly meaning: "Graph entity IDs must be unique.";
48
+ }, {
49
+ readonly id: "ONT-RELATIONSHIP-ID-UNIQUE";
50
+ readonly severity: "error";
51
+ readonly meaning: "Relationship IDs must be unique.";
52
+ }, {
53
+ readonly id: "ONT-DECISION-ID-UNIQUE";
54
+ readonly severity: "error";
55
+ readonly meaning: "Decision IDs must be unique.";
56
+ }, {
57
+ readonly id: "ONT-RELATIONSHIP-PERSON-RESOLVES";
58
+ readonly severity: "warning";
59
+ readonly meaning: "A relationship person should resolve to a canonical person entity by name.";
60
+ }, {
61
+ readonly id: "ONT-DECISION-SUPERSEDES-EXISTS";
62
+ readonly severity: "error";
63
+ readonly meaning: "A supersedes reference must resolve to an existing decision.";
64
+ }, {
65
+ readonly id: "ONT-DECISION-SUPERSEDES-SELF";
66
+ readonly severity: "error";
67
+ readonly meaning: "A decision must not supersede itself.";
68
+ }, {
69
+ readonly id: "ONT-DECISION-SUPERSEDES-CYCLE";
70
+ readonly severity: "error";
71
+ readonly meaning: "Decision supersession edges must not form a cycle.";
72
+ }];
73
+ };
74
+ readonly inference: {
75
+ readonly rules: readonly [{
76
+ readonly id: "ONT-INFER-EXPLICIT-SUPERSESSION";
77
+ readonly meaning: "Only an explicit supersedes edge makes an older decision inactive.";
78
+ }, {
79
+ readonly id: "ONT-INFER-SAME-TOPIC-CONFLICT";
80
+ readonly meaning: "Multiple active decisions on the same explicit topic are reported as a conflict.";
81
+ }];
82
+ };
83
+ readonly evolution: {
84
+ readonly compatibility: readonly [{
85
+ readonly fromVersion: "0.0.0";
86
+ readonly toVersion: "2.0.0";
87
+ readonly level: "read-compatible-write-gated";
88
+ readonly changes: readonly ["Adds a versioned public semantic contract.", "Adds canonical Graph v2 entities and ID-based edges governed by the Relation Registry."];
89
+ readonly migration: "Before enabling 2.0.0 writes, back up the Personal OS directory, run ontology:audit, preview ontology:migrate, then write using the preview expectedInputDigest.";
90
+ readonly rollback: "For an installation without a prior package, run npm uninstall -g @unson/brainbase-mcp, restore the captured MCP client configuration, and restart the client. Otherwise restore the pre-migration Personal OS backup and reinstall the recorded last known working package version.";
91
+ }, {
92
+ readonly fromVersion: "1.0.0";
93
+ readonly toVersion: "2.0.0";
94
+ readonly level: "read-compatible-write-gated";
95
+ readonly changes: readonly ["Adds canonical Graph v2 entities and ID-based edges.", "Binds the portable ontology release to the canonical Relation Registry."];
96
+ readonly migration: "Run ontology:audit --ontology-version 1.0.0, preview ontology:migrate, then write using the preview expectedInputDigest.";
97
+ readonly rollback: "Restore the pre-migration Personal OS backup; the immutable 1.0.0 interpretation remains available for historical reads.";
98
+ }];
99
+ };
100
+ };
101
+ };
102
+ relationRegistry: Readonly<Record<import("./types.js").CoreRelation, Readonly<import("./relation-registry.js").CanonicalRelationDefinition>>>;
103
+ };
104
+ binding: {
105
+ id: "brainbase-personal-os";
106
+ version: "2.0.0";
107
+ releaseDigest: string;
108
+ };
109
+ };
110
+ export declare const emptyGraph: GraphFileV2;
3
111
  export declare const emptyRelationships: RelationshipsFile;
4
112
  export declare const schemaTemplates: Record<string, unknown>;
package/dist/templates.js CHANGED
@@ -1,21 +1,61 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { portableOntology } from './ontology.js';
3
+ import { canonicalRelationRegistry } from './relation-registry.js';
4
+ const ontologyReleaseManifest = deepFreeze({
5
+ id: 'brainbase-personal-os',
6
+ version: portableOntology.version,
7
+ ontology: portableOntology,
8
+ relationRegistry: canonicalRelationRegistry
9
+ });
10
+ const ontologyReleaseDigest = `sha256:${createHash('sha256')
11
+ .update(JSON.stringify(ontologyReleaseManifest))
12
+ .digest('hex')}`;
13
+ export const canonicalGraphOntologyRelease = deepFreeze({
14
+ manifest: ontologyReleaseManifest,
15
+ binding: {
16
+ id: ontologyReleaseManifest.id,
17
+ version: ontologyReleaseManifest.version,
18
+ releaseDigest: ontologyReleaseDigest
19
+ }
20
+ });
1
21
  export const emptyGraph = {
2
- version: 1,
22
+ version: 2,
23
+ ontology: canonicalGraphOntologyRelease.binding,
3
24
  owner: {},
4
- entities: []
25
+ entities: [],
26
+ edges: []
5
27
  };
6
28
  export const emptyRelationships = {
7
29
  version: 1,
8
30
  relationships: []
9
31
  };
32
+ function deepFreeze(value) {
33
+ if (value && typeof value === 'object' && !Object.isFrozen(value)) {
34
+ Object.freeze(value);
35
+ for (const nested of Object.values(value))
36
+ deepFreeze(nested);
37
+ }
38
+ return value;
39
+ }
10
40
  export const schemaTemplates = {
11
41
  'graph.schema.json': {
12
42
  type: 'object',
13
- required: ['version', 'entities'],
43
+ required: ['version', 'ontology', 'entities', 'edges'],
14
44
  properties: {
15
- version: { const: 1 },
45
+ version: { const: 2 },
46
+ ontology: {
47
+ type: 'object',
48
+ required: ['id', 'version', 'releaseDigest'],
49
+ properties: {
50
+ id: { const: 'brainbase-personal-os' },
51
+ version: { type: 'string', minLength: 1 },
52
+ releaseDigest: { type: 'string', minLength: 1 }
53
+ }
54
+ },
16
55
  owner: {
17
56
  type: 'object',
18
57
  properties: {
58
+ id: { type: 'string' },
19
59
  name: { type: 'string' },
20
60
  summary: { type: 'string' }
21
61
  }
@@ -27,13 +67,42 @@ export const schemaTemplates = {
27
67
  required: ['id', 'type', 'name'],
28
68
  properties: {
29
69
  id: { type: 'string', minLength: 1 },
30
- type: { enum: ['person', 'org', 'project', 'relationship'] },
70
+ type: { enum: ['person', 'org', 'project', 'decision'] },
31
71
  name: { type: 'string', minLength: 1 },
72
+ aliases: { type: 'array', items: { type: 'string', minLength: 1 } },
32
73
  summary: { type: 'string' },
33
74
  tags: { type: 'array', items: { type: 'string' } },
34
75
  metadata: { type: 'object' }
35
76
  }
36
77
  }
78
+ },
79
+ edges: {
80
+ type: 'array',
81
+ items: {
82
+ type: 'object',
83
+ required: ['id', 'fromId', 'relation', 'toId'],
84
+ properties: {
85
+ id: { type: 'string', minLength: 1 },
86
+ fromId: { type: 'string', minLength: 1 },
87
+ relation: {
88
+ enum: ['member_of', 'participates_in', 'accountable_for', 'owned_by', 'governs', 'supersedes']
89
+ },
90
+ toId: { type: 'string', minLength: 1 },
91
+ role: { type: 'string' },
92
+ context: { type: 'string' },
93
+ validFrom: { type: 'string', format: 'date-time' },
94
+ validTo: { type: 'string', format: 'date-time' },
95
+ provenance: {
96
+ type: 'object',
97
+ required: ['sourceKind'],
98
+ properties: {
99
+ sourceKind: { enum: ['user_approved', 'migration', 'import', 'onboarding'] },
100
+ sourceId: { type: 'string' },
101
+ evidenceHash: { type: 'string' }
102
+ }
103
+ }
104
+ }
105
+ }
37
106
  }
38
107
  }
39
108
  },
package/dist/tools.d.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  import type { EntityKind, PersonalOs, SearchResult } from './types.js';
2
- export declare function getContext(os: PersonalOs): Record<string, unknown>;
2
+ export interface RetrievalOptions {
3
+ project?: string;
4
+ asOf?: string;
5
+ }
6
+ export declare function getContext(os: PersonalOs, options?: RetrievalOptions): Record<string, unknown>;
3
7
  export declare function listEntities(os: PersonalOs, type?: EntityKind): Record<string, unknown>;
4
8
  export declare function searchPersonalKg(os: PersonalOs, query: string, limit?: number): SearchResult[];
5
- export declare function searchAll(os: PersonalOs, query: string, limit?: number): SearchResult[];
9
+ export declare function searchAll(os: PersonalOs, query: string, limit?: number, options?: RetrievalOptions): SearchResult[];
6
10
  export declare function onboardingStatus(os: PersonalOs): Record<string, unknown>;