@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,339 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { canonicalEdgeId, validateCanonicalGraph } from './canonical-graph.js';
|
|
3
|
+
import { canonicalGraphOntologyRelease } from './templates.js';
|
|
4
|
+
/**
|
|
5
|
+
* Produces a side-effect-free v1 -> v2 migration preview.
|
|
6
|
+
*
|
|
7
|
+
* Only explicit IDs and unique exact-name matches are promoted. Free-form
|
|
8
|
+
* context is never interpreted as a canonical relationship.
|
|
9
|
+
*/
|
|
10
|
+
export function planCanonicalGraphMigration(input) {
|
|
11
|
+
validateCanonicalGraph(input.graph);
|
|
12
|
+
validateRelationshipIds(input.relationships?.relationships ?? []);
|
|
13
|
+
validateDecisionRecordIds(input.decisions ?? []);
|
|
14
|
+
const inputDigest = digest(normalizeMigrationInput(input));
|
|
15
|
+
const issues = [];
|
|
16
|
+
if (input.graph.version === 2) {
|
|
17
|
+
const graph = structuredClone(input.graph);
|
|
18
|
+
validateGraphSupersession(graph, issues);
|
|
19
|
+
return {
|
|
20
|
+
status: issues.length === 0 ? 'up_to_date' : 'blocked',
|
|
21
|
+
graph,
|
|
22
|
+
issues: issues.sort(compareIssues),
|
|
23
|
+
inputDigest,
|
|
24
|
+
outputDigest: digest(graph),
|
|
25
|
+
changes: { entitiesAdded: 0, edgesAdded: 0 }
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
validateDecisionEntityIds(input.graph, input.decisions ?? []);
|
|
29
|
+
reportUnsupportedLegacyEntities(input.graph, issues);
|
|
30
|
+
const invalidSupersessionSources = validateDecisionSupersession(input.decisions ?? [], issues);
|
|
31
|
+
const entities = migrateEntities(input.graph, input.decisions ?? []);
|
|
32
|
+
const entityById = new Map(entities.map((entity) => [entity.id, entity]));
|
|
33
|
+
const edges = new Map();
|
|
34
|
+
addExplicitProjectMembershipEdges(input.graph, entityById, edges, issues);
|
|
35
|
+
validateRelationshipEvidence(input.relationships?.relationships ?? [], entities, entityById, issues);
|
|
36
|
+
addDecisionEdges(input.graph, input.decisions ?? [], entityById, edges, issues, invalidSupersessionSources);
|
|
37
|
+
const owner = migrateOwner(input.graph, entities);
|
|
38
|
+
const graph = {
|
|
39
|
+
version: 2,
|
|
40
|
+
ontology: { ...canonicalGraphOntologyRelease.binding },
|
|
41
|
+
...(owner ? { owner } : {}),
|
|
42
|
+
entities: [...entities].sort((left, right) => left.id.localeCompare(right.id, 'en')),
|
|
43
|
+
edges: [...edges.values()].sort((left, right) => left.id.localeCompare(right.id, 'en'))
|
|
44
|
+
};
|
|
45
|
+
validateCanonicalGraph(graph);
|
|
46
|
+
return {
|
|
47
|
+
status: issues.length === 0 ? 'migration_required' : 'blocked',
|
|
48
|
+
graph,
|
|
49
|
+
issues: issues.sort(compareIssues),
|
|
50
|
+
inputDigest,
|
|
51
|
+
outputDigest: digest(graph),
|
|
52
|
+
changes: {
|
|
53
|
+
entitiesAdded: (input.decisions ?? []).filter((decision) => !input.graph.entities.some((entity) => entity.id === decision.id)).length,
|
|
54
|
+
edgesAdded: graph.edges.length
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function validateRelationshipIds(relationships) {
|
|
59
|
+
const ids = new Set();
|
|
60
|
+
for (const relationship of relationships) {
|
|
61
|
+
if (ids.has(relationship.id)) {
|
|
62
|
+
throw new Error(`ONT-RELATIONSHIP-ID-UNIQUE: duplicate relationship ID ${relationship.id}`);
|
|
63
|
+
}
|
|
64
|
+
ids.add(relationship.id);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function reportUnsupportedLegacyEntities(graph, issues) {
|
|
68
|
+
for (const entity of graph.entities) {
|
|
69
|
+
if (entity.type !== 'relationship')
|
|
70
|
+
continue;
|
|
71
|
+
issues.push({
|
|
72
|
+
code: 'unsupported_relationship_entity',
|
|
73
|
+
recordId: entity.id,
|
|
74
|
+
detail: 'Legacy relationship entities have no lossless canonical v2 representation; resolve them before writing migration output'
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function validateDecisionSupersession(decisions, issues) {
|
|
79
|
+
const decisionIds = new Set(decisions.map((decision) => decision.id));
|
|
80
|
+
const adjacency = new Map();
|
|
81
|
+
const invalidSources = new Set();
|
|
82
|
+
for (const decision of decisions) {
|
|
83
|
+
const targets = [...new Set(decision.supersedes ?? [])].sort((left, right) => left.localeCompare(right, 'en'));
|
|
84
|
+
if (targets.includes(decision.id)) {
|
|
85
|
+
invalidSources.add(decision.id);
|
|
86
|
+
issues.push({
|
|
87
|
+
code: 'decision_supersedes_self',
|
|
88
|
+
recordId: decision.id,
|
|
89
|
+
detail: 'ONT-DECISION-SUPERSEDES-SELF: a decision cannot supersede itself'
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
adjacency.set(decision.id, targets.filter((target) => target !== decision.id && decisionIds.has(target)));
|
|
93
|
+
}
|
|
94
|
+
for (const decisionId of [...decisionIds].sort((left, right) => left.localeCompare(right, 'en'))) {
|
|
95
|
+
if (!canReach(adjacency, decisionId, decisionId, new Set()))
|
|
96
|
+
continue;
|
|
97
|
+
invalidSources.add(decisionId);
|
|
98
|
+
issues.push({
|
|
99
|
+
code: 'decision_supersedes_cycle',
|
|
100
|
+
recordId: decisionId,
|
|
101
|
+
detail: 'ONT-DECISION-SUPERSEDES-CYCLE: decision supersession must be acyclic'
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return invalidSources;
|
|
105
|
+
}
|
|
106
|
+
function validateGraphSupersession(graph, issues) {
|
|
107
|
+
const decisionIds = new Set(graph.entities.filter((entity) => entity.type === 'decision').map((entity) => entity.id));
|
|
108
|
+
const adjacency = new Map([...decisionIds].map((id) => [id, []]));
|
|
109
|
+
for (const edge of graph.edges) {
|
|
110
|
+
if (edge.relation !== 'supersedes')
|
|
111
|
+
continue;
|
|
112
|
+
if (edge.fromId === edge.toId) {
|
|
113
|
+
issues.push({
|
|
114
|
+
code: 'decision_supersedes_self',
|
|
115
|
+
recordId: edge.fromId,
|
|
116
|
+
detail: 'ONT-DECISION-SUPERSEDES-SELF: a decision cannot supersede itself'
|
|
117
|
+
});
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
adjacency.get(edge.fromId)?.push(edge.toId);
|
|
121
|
+
}
|
|
122
|
+
for (const targets of adjacency.values())
|
|
123
|
+
targets.sort((left, right) => left.localeCompare(right, 'en'));
|
|
124
|
+
for (const decisionId of [...decisionIds].sort((left, right) => left.localeCompare(right, 'en'))) {
|
|
125
|
+
if (!canReach(adjacency, decisionId, decisionId, new Set()))
|
|
126
|
+
continue;
|
|
127
|
+
issues.push({
|
|
128
|
+
code: 'decision_supersedes_cycle',
|
|
129
|
+
recordId: decisionId,
|
|
130
|
+
detail: 'ONT-DECISION-SUPERSEDES-CYCLE: decision supersession must be acyclic'
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function canReach(adjacency, start, current, visited) {
|
|
135
|
+
if (visited.has(current))
|
|
136
|
+
return false;
|
|
137
|
+
visited.add(current);
|
|
138
|
+
for (const target of adjacency.get(current) ?? []) {
|
|
139
|
+
if (target === start)
|
|
140
|
+
return true;
|
|
141
|
+
if (canReach(adjacency, start, target, visited))
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
function migrateEntities(graph, decisions) {
|
|
147
|
+
const migrated = new Map();
|
|
148
|
+
for (const entity of graph.entities) {
|
|
149
|
+
if (entity.type === 'relationship')
|
|
150
|
+
continue;
|
|
151
|
+
migrated.set(entity.id, {
|
|
152
|
+
id: entity.id,
|
|
153
|
+
type: entity.type,
|
|
154
|
+
name: entity.name,
|
|
155
|
+
...(entity.summary === undefined ? {} : { summary: entity.summary }),
|
|
156
|
+
...(entity.tags === undefined ? {} : { tags: [...entity.tags] }),
|
|
157
|
+
...(entity.metadata === undefined ? {} : { metadata: structuredClone(entity.metadata) })
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
for (const decision of decisions) {
|
|
161
|
+
migrated.set(decision.id, {
|
|
162
|
+
id: decision.id,
|
|
163
|
+
type: 'decision',
|
|
164
|
+
name: decision.title,
|
|
165
|
+
...(decision.decision === decision.title ? {} : { aliases: [decision.decision] }),
|
|
166
|
+
summary: decision.decision,
|
|
167
|
+
...(decision.effectiveAt === undefined ? {} : { validFrom: decision.effectiveAt }),
|
|
168
|
+
...(decision.tags === undefined ? {} : { tags: [...decision.tags] }),
|
|
169
|
+
metadata: {
|
|
170
|
+
...(decision.topic === undefined ? {} : { topic: decision.topic }),
|
|
171
|
+
...(decision.rationale === undefined ? {} : { rationale: decision.rationale })
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return [...migrated.values()];
|
|
176
|
+
}
|
|
177
|
+
function addExplicitProjectMembershipEdges(graph, entityById, edges, issues) {
|
|
178
|
+
for (const entity of graph.entities) {
|
|
179
|
+
if (entity.type !== 'person')
|
|
180
|
+
continue;
|
|
181
|
+
const projectId = stringMetadata(entity.metadata, 'projectId');
|
|
182
|
+
if (!projectId)
|
|
183
|
+
continue;
|
|
184
|
+
if (entityById.get(projectId)?.type !== 'project') {
|
|
185
|
+
issues.push({
|
|
186
|
+
code: 'missing_project_endpoint',
|
|
187
|
+
recordId: entity.id,
|
|
188
|
+
detail: `Explicit project ID ${projectId} does not identify a canonical project`
|
|
189
|
+
});
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
addEdge(edges, {
|
|
193
|
+
fromId: entity.id,
|
|
194
|
+
relation: 'participates_in',
|
|
195
|
+
toId: projectId,
|
|
196
|
+
provenance: { sourceKind: 'migration', sourceId: entity.id }
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function validateRelationshipEvidence(relationships, entities, entityById, issues) {
|
|
201
|
+
for (const relationship of relationships) {
|
|
202
|
+
const people = entities.filter((entity) => entity.type === 'person' && entity.name === relationship.person);
|
|
203
|
+
if (people.length === 0) {
|
|
204
|
+
issues.push({ code: 'unresolved_person', recordId: relationship.id, detail: `No canonical person exactly matches ${relationship.person}` });
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (people.length > 1) {
|
|
208
|
+
issues.push({ code: 'ambiguous_person', recordId: relationship.id, detail: `${relationship.person} matches ${people.length} canonical people` });
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const person = people[0];
|
|
212
|
+
const projectId = stringMetadata(person.metadata, 'projectId');
|
|
213
|
+
if (!projectId || entityById.get(projectId)?.type !== 'project') {
|
|
214
|
+
issues.push({
|
|
215
|
+
code: 'missing_project_evidence',
|
|
216
|
+
recordId: relationship.id,
|
|
217
|
+
detail: 'Free-form relationship context cannot be promoted without an explicit canonical project ID'
|
|
218
|
+
});
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
// `role` and `context` are legacy display text, not typed relation evidence.
|
|
222
|
+
// The explicit person.metadata.projectId edge above is the only safe
|
|
223
|
+
// relationship promotion available in Graph v1.
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function validateDecisionEntityIds(graph, decisions) {
|
|
227
|
+
const occupied = new Set(graph.entities.map((entity) => entity.id));
|
|
228
|
+
for (const decision of decisions) {
|
|
229
|
+
if (occupied.has(decision.id)) {
|
|
230
|
+
throw new Error(`MIGRATION-ENTITY-ID-COLLISION: decision ${decision.id} conflicts with an existing canonical entity`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function validateDecisionRecordIds(decisions) {
|
|
235
|
+
const ids = new Set();
|
|
236
|
+
for (const decision of decisions) {
|
|
237
|
+
if (ids.has(decision.id)) {
|
|
238
|
+
throw new Error(`ONT-DECISION-ID-UNIQUE: duplicate decision ID ${decision.id}`);
|
|
239
|
+
}
|
|
240
|
+
ids.add(decision.id);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function addDecisionEdges(graph, decisions, entityById, edges, issues, invalidSupersessionSources) {
|
|
244
|
+
const projects = graph.entities.filter((entity) => entity.type === 'project');
|
|
245
|
+
for (const decision of decisions) {
|
|
246
|
+
const matchingProjects = projects.filter((project) => stringArrayMetadata(project.metadata, 'decisionPrinciples').includes(decision.decision));
|
|
247
|
+
if (matchingProjects.length === 1) {
|
|
248
|
+
addEdge(edges, {
|
|
249
|
+
fromId: decision.id,
|
|
250
|
+
relation: 'governs',
|
|
251
|
+
toId: matchingProjects[0].id,
|
|
252
|
+
...(decision.effectiveAt === undefined ? {} : { validFrom: decision.effectiveAt }),
|
|
253
|
+
provenance: { sourceKind: 'migration', sourceId: decision.id }
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
else if (matchingProjects.length > 1) {
|
|
257
|
+
issues.push({
|
|
258
|
+
code: 'ambiguous_decision_project',
|
|
259
|
+
recordId: decision.id,
|
|
260
|
+
detail: `Decision text exactly matches ${matchingProjects.length} project principles`
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
for (const supersededId of decision.supersedes ?? []) {
|
|
264
|
+
if (invalidSupersessionSources.has(decision.id))
|
|
265
|
+
continue;
|
|
266
|
+
if (entityById.get(supersededId)?.type !== 'decision') {
|
|
267
|
+
issues.push({ code: 'missing_decision_endpoint', recordId: decision.id, detail: `Missing superseded decision ${supersededId}` });
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
addEdge(edges, {
|
|
271
|
+
fromId: decision.id,
|
|
272
|
+
relation: 'supersedes',
|
|
273
|
+
toId: supersededId,
|
|
274
|
+
...(decision.effectiveAt === undefined ? {} : { validFrom: decision.effectiveAt }),
|
|
275
|
+
provenance: { sourceKind: 'migration', sourceId: decision.id }
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function addEdge(edges, edge) {
|
|
281
|
+
const complete = { ...edge, id: canonicalEdgeId(edge) };
|
|
282
|
+
edges.set(JSON.stringify([edge.fromId, edge.relation, edge.toId]), complete);
|
|
283
|
+
}
|
|
284
|
+
function migrateOwner(graph, entities) {
|
|
285
|
+
if (!graph.owner)
|
|
286
|
+
return undefined;
|
|
287
|
+
const matches = graph.owner.name
|
|
288
|
+
? entities.filter((entity) => entity.type === 'person' && entity.name === graph.owner.name)
|
|
289
|
+
: [];
|
|
290
|
+
return {
|
|
291
|
+
...(matches.length === 1 ? { id: matches[0].id } : {}),
|
|
292
|
+
...(graph.owner.name === undefined ? {} : { name: graph.owner.name }),
|
|
293
|
+
...(graph.owner.summary === undefined ? {} : { summary: graph.owner.summary })
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function stringMetadata(metadata, key) {
|
|
297
|
+
const value = metadata?.[key];
|
|
298
|
+
return typeof value === 'string' && value.trim() !== '' ? value : undefined;
|
|
299
|
+
}
|
|
300
|
+
function stringArrayMetadata(metadata, key) {
|
|
301
|
+
const value = metadata?.[key];
|
|
302
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : [];
|
|
303
|
+
}
|
|
304
|
+
function digest(value) {
|
|
305
|
+
return createHash('sha256').update(JSON.stringify(canonicalize(value))).digest('hex');
|
|
306
|
+
}
|
|
307
|
+
function normalizeMigrationInput(input) {
|
|
308
|
+
return {
|
|
309
|
+
...structuredClone(input),
|
|
310
|
+
graph: {
|
|
311
|
+
...structuredClone(input.graph),
|
|
312
|
+
entities: [...input.graph.entities].sort((left, right) => left.id.localeCompare(right.id, 'en')),
|
|
313
|
+
...(input.graph.version === 2
|
|
314
|
+
? { edges: [...input.graph.edges].sort((left, right) => left.id.localeCompare(right.id, 'en')) }
|
|
315
|
+
: {})
|
|
316
|
+
},
|
|
317
|
+
...(input.relationships === undefined ? {} : {
|
|
318
|
+
relationships: {
|
|
319
|
+
...structuredClone(input.relationships),
|
|
320
|
+
relationships: [...input.relationships.relationships].sort((left, right) => left.id.localeCompare(right.id, 'en'))
|
|
321
|
+
}
|
|
322
|
+
}),
|
|
323
|
+
...(input.decisions === undefined ? {} : {
|
|
324
|
+
decisions: [...input.decisions].sort((left, right) => left.id.localeCompare(right.id, 'en'))
|
|
325
|
+
})
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
function canonicalize(value) {
|
|
329
|
+
if (Array.isArray(value))
|
|
330
|
+
return value.map(canonicalize);
|
|
331
|
+
if (!value || typeof value !== 'object')
|
|
332
|
+
return value;
|
|
333
|
+
return Object.fromEntries(Object.entries(value)
|
|
334
|
+
.sort(([left], [right]) => left.localeCompare(right, 'en'))
|
|
335
|
+
.map(([key, nested]) => [key, canonicalize(nested)]));
|
|
336
|
+
}
|
|
337
|
+
function compareIssues(left, right) {
|
|
338
|
+
return `${left.recordId}\u0000${left.code}`.localeCompare(`${right.recordId}\u0000${right.code}`, 'en');
|
|
339
|
+
}
|
package/dist/ontology-ssot.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { auditOntology, resolveOntologyVersion } from './ontology.js';
|
|
2
2
|
import { loadPersonalOs } from './ssot.js';
|
|
3
3
|
export async function auditPersonalOsDirectory(dataDir, options = {}) {
|
|
4
|
-
const ontologyVersion = resolveOntologyVersion(options.ontologyVersion);
|
|
5
4
|
try {
|
|
6
|
-
return auditOntology(await loadPersonalOs(dataDir),
|
|
5
|
+
return auditOntology(await loadPersonalOs(dataDir), options);
|
|
7
6
|
}
|
|
8
7
|
catch (error) {
|
|
8
|
+
const ontologyVersion = resolveOntologyVersion(options.ontologyVersion);
|
|
9
9
|
const message = error instanceof Error ? error.message : String(error);
|
|
10
10
|
return {
|
|
11
11
|
status: 'unverified',
|
package/dist/ontology.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { DecisionRecord, PersonalOs } from './types.js';
|
|
2
|
-
export declare const
|
|
3
|
-
export declare const
|
|
2
|
+
export declare const ONTOLOGY_V1_VERSION: "1.0.0";
|
|
3
|
+
export declare const ONTOLOGY_VERSION: "2.0.0";
|
|
4
|
+
export declare const SUPPORTED_ONTOLOGY_VERSIONS: readonly ["0.0.0", "1.0.0", "2.0.0"];
|
|
4
5
|
export type OntologyVersion = typeof SUPPORTED_ONTOLOGY_VERSIONS[number];
|
|
5
6
|
export type OntologySeverity = 'error' | 'warning';
|
|
6
7
|
export interface OntologyViolation {
|
|
@@ -39,7 +40,8 @@ export interface UnverifiedOntologyAuditResult {
|
|
|
39
40
|
issues: OntologyViolation[];
|
|
40
41
|
}
|
|
41
42
|
export type PersonalOsOntologyAudit = OntologyAuditResult | UnverifiedOntologyAuditResult;
|
|
42
|
-
|
|
43
|
+
/** Historical public release. Never derive it from the active release. */
|
|
44
|
+
export declare const portableOntologyV1: {
|
|
43
45
|
readonly version: "1.0.0";
|
|
44
46
|
readonly effectiveAt: "2026-08-03T00:00:00.000Z";
|
|
45
47
|
readonly compatibility: "read-compatible-write-gated";
|
|
@@ -136,6 +138,102 @@ export declare const portableOntology: {
|
|
|
136
138
|
};
|
|
137
139
|
};
|
|
138
140
|
};
|
|
141
|
+
export declare const portableOntology: {
|
|
142
|
+
readonly version: "2.0.0";
|
|
143
|
+
readonly effectiveAt: "2026-08-17T00:00:00.000Z";
|
|
144
|
+
readonly compatibility: "read-compatible-write-gated";
|
|
145
|
+
readonly name: "Brainbase Portable Ontology Kernel";
|
|
146
|
+
readonly description: "A local-first semantic contract for canonical Graph entities and ID-based edges.";
|
|
147
|
+
readonly domains: {
|
|
148
|
+
readonly types: {
|
|
149
|
+
readonly concepts: readonly [{
|
|
150
|
+
readonly id: "person";
|
|
151
|
+
readonly meaning: "A human represented in the local Graph.";
|
|
152
|
+
readonly usageConditions: readonly ["Use only for a human identity approved for the canonical local SSOT."];
|
|
153
|
+
}, {
|
|
154
|
+
readonly id: "org";
|
|
155
|
+
readonly meaning: "An organization represented in the local Graph.";
|
|
156
|
+
readonly usageConditions: readonly ["Use for a named organizational actor, not for a project or product."];
|
|
157
|
+
}, {
|
|
158
|
+
readonly id: "project";
|
|
159
|
+
readonly meaning: "A bounded body of work represented in the local Graph.";
|
|
160
|
+
readonly usageConditions: readonly ["Use when the entity has a bounded work objective; do not use it as an organization alias."];
|
|
161
|
+
}, {
|
|
162
|
+
readonly id: "relationship";
|
|
163
|
+
readonly meaning: "A contextual connection to a person.";
|
|
164
|
+
readonly usageConditions: readonly ["The person field must resolve to a canonical person entity by name."];
|
|
165
|
+
}, {
|
|
166
|
+
readonly id: "decision";
|
|
167
|
+
readonly meaning: "A durable choice that may explicitly supersede another choice.";
|
|
168
|
+
readonly usageConditions: readonly ["Use for an explicit durable choice; replacement requires a supersedes Decision ID."];
|
|
169
|
+
}];
|
|
170
|
+
};
|
|
171
|
+
readonly relations: {
|
|
172
|
+
readonly vocabulary: {
|
|
173
|
+
id: import("./types.js").CoreRelation;
|
|
174
|
+
source: import("./types.js").CanonicalEntityKind;
|
|
175
|
+
target: import("./types.js").CanonicalEntityKind;
|
|
176
|
+
}[];
|
|
177
|
+
};
|
|
178
|
+
readonly constraints: {
|
|
179
|
+
readonly rules: readonly [{
|
|
180
|
+
readonly id: "ONT-ENTITY-ID-UNIQUE";
|
|
181
|
+
readonly severity: "error";
|
|
182
|
+
readonly meaning: "Graph entity IDs must be unique.";
|
|
183
|
+
}, {
|
|
184
|
+
readonly id: "ONT-RELATIONSHIP-ID-UNIQUE";
|
|
185
|
+
readonly severity: "error";
|
|
186
|
+
readonly meaning: "Relationship IDs must be unique.";
|
|
187
|
+
}, {
|
|
188
|
+
readonly id: "ONT-DECISION-ID-UNIQUE";
|
|
189
|
+
readonly severity: "error";
|
|
190
|
+
readonly meaning: "Decision IDs must be unique.";
|
|
191
|
+
}, {
|
|
192
|
+
readonly id: "ONT-RELATIONSHIP-PERSON-RESOLVES";
|
|
193
|
+
readonly severity: "warning";
|
|
194
|
+
readonly meaning: "A relationship person should resolve to a canonical person entity by name.";
|
|
195
|
+
}, {
|
|
196
|
+
readonly id: "ONT-DECISION-SUPERSEDES-EXISTS";
|
|
197
|
+
readonly severity: "error";
|
|
198
|
+
readonly meaning: "A supersedes reference must resolve to an existing decision.";
|
|
199
|
+
}, {
|
|
200
|
+
readonly id: "ONT-DECISION-SUPERSEDES-SELF";
|
|
201
|
+
readonly severity: "error";
|
|
202
|
+
readonly meaning: "A decision must not supersede itself.";
|
|
203
|
+
}, {
|
|
204
|
+
readonly id: "ONT-DECISION-SUPERSEDES-CYCLE";
|
|
205
|
+
readonly severity: "error";
|
|
206
|
+
readonly meaning: "Decision supersession edges must not form a cycle.";
|
|
207
|
+
}];
|
|
208
|
+
};
|
|
209
|
+
readonly inference: {
|
|
210
|
+
readonly rules: readonly [{
|
|
211
|
+
readonly id: "ONT-INFER-EXPLICIT-SUPERSESSION";
|
|
212
|
+
readonly meaning: "Only an explicit supersedes edge makes an older decision inactive.";
|
|
213
|
+
}, {
|
|
214
|
+
readonly id: "ONT-INFER-SAME-TOPIC-CONFLICT";
|
|
215
|
+
readonly meaning: "Multiple active decisions on the same explicit topic are reported as a conflict.";
|
|
216
|
+
}];
|
|
217
|
+
};
|
|
218
|
+
readonly evolution: {
|
|
219
|
+
readonly compatibility: readonly [{
|
|
220
|
+
readonly fromVersion: "0.0.0";
|
|
221
|
+
readonly toVersion: "2.0.0";
|
|
222
|
+
readonly level: "read-compatible-write-gated";
|
|
223
|
+
readonly changes: readonly ["Adds a versioned public semantic contract.", "Adds canonical Graph v2 entities and ID-based edges governed by the Relation Registry."];
|
|
224
|
+
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.";
|
|
225
|
+
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.";
|
|
226
|
+
}, {
|
|
227
|
+
readonly fromVersion: "1.0.0";
|
|
228
|
+
readonly toVersion: "2.0.0";
|
|
229
|
+
readonly level: "read-compatible-write-gated";
|
|
230
|
+
readonly changes: readonly ["Adds canonical Graph v2 entities and ID-based edges.", "Binds the portable ontology release to the canonical Relation Registry."];
|
|
231
|
+
readonly migration: "Run ontology:audit --ontology-version 1.0.0, preview ontology:migrate, then write using the preview expectedInputDigest.";
|
|
232
|
+
readonly rollback: "Restore the pre-migration Personal OS backup; the immutable 1.0.0 interpretation remains available for historical reads.";
|
|
233
|
+
}];
|
|
234
|
+
};
|
|
235
|
+
};
|
|
236
|
+
};
|
|
139
237
|
export declare function auditOntology(os: PersonalOs, options?: {
|
|
140
238
|
ontologyVersion?: OntologyVersion;
|
|
141
239
|
}): OntologyAuditResult;
|
package/dist/ontology.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
export const
|
|
3
|
-
const
|
|
4
|
-
|
|
1
|
+
import { canonicalRelationRegistry } from './relation-registry.js';
|
|
2
|
+
export const ONTOLOGY_V1_VERSION = '1.0.0';
|
|
3
|
+
export const ONTOLOGY_VERSION = '2.0.0';
|
|
4
|
+
export const SUPPORTED_ONTOLOGY_VERSIONS = ['0.0.0', ONTOLOGY_V1_VERSION, ONTOLOGY_VERSION];
|
|
5
|
+
const releaseV1 = {
|
|
6
|
+
version: ONTOLOGY_V1_VERSION,
|
|
5
7
|
effectiveAt: '2026-08-03T00:00:00.000Z',
|
|
6
8
|
compatibility: 'read-compatible-write-gated',
|
|
7
9
|
name: 'Brainbase Portable Ontology Kernel',
|
|
@@ -70,7 +72,7 @@ const release = {
|
|
|
70
72
|
compatibility: [
|
|
71
73
|
{
|
|
72
74
|
fromVersion: '0.0.0',
|
|
73
|
-
toVersion:
|
|
75
|
+
toVersion: ONTOLOGY_V1_VERSION,
|
|
74
76
|
level: 'read-compatible-write-gated',
|
|
75
77
|
changes: [
|
|
76
78
|
'Adds a versioned public semantic contract.',
|
|
@@ -83,9 +85,56 @@ const release = {
|
|
|
83
85
|
}
|
|
84
86
|
}
|
|
85
87
|
};
|
|
86
|
-
|
|
88
|
+
/** Historical public release. Never derive it from the active release. */
|
|
89
|
+
export const portableOntologyV1 = deepFreeze(releaseV1);
|
|
90
|
+
const releaseV2 = {
|
|
91
|
+
version: ONTOLOGY_VERSION,
|
|
92
|
+
effectiveAt: '2026-08-17T00:00:00.000Z',
|
|
93
|
+
compatibility: 'read-compatible-write-gated',
|
|
94
|
+
name: 'Brainbase Portable Ontology Kernel',
|
|
95
|
+
description: 'A local-first semantic contract for canonical Graph entities and ID-based edges.',
|
|
96
|
+
domains: {
|
|
97
|
+
types: portableOntologyV1.domains.types,
|
|
98
|
+
relations: {
|
|
99
|
+
vocabulary: Object.values(canonicalRelationRegistry).map((definition) => ({
|
|
100
|
+
id: definition.id,
|
|
101
|
+
source: definition.from,
|
|
102
|
+
target: definition.to
|
|
103
|
+
}))
|
|
104
|
+
},
|
|
105
|
+
constraints: portableOntologyV1.domains.constraints,
|
|
106
|
+
inference: portableOntologyV1.domains.inference,
|
|
107
|
+
evolution: {
|
|
108
|
+
compatibility: [
|
|
109
|
+
{
|
|
110
|
+
fromVersion: '0.0.0',
|
|
111
|
+
toVersion: ONTOLOGY_VERSION,
|
|
112
|
+
level: 'read-compatible-write-gated',
|
|
113
|
+
changes: [
|
|
114
|
+
'Adds a versioned public semantic contract.',
|
|
115
|
+
'Adds canonical Graph v2 entities and ID-based edges governed by the Relation Registry.'
|
|
116
|
+
],
|
|
117
|
+
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.',
|
|
118
|
+
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.'
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
fromVersion: ONTOLOGY_V1_VERSION,
|
|
122
|
+
toVersion: ONTOLOGY_VERSION,
|
|
123
|
+
level: 'read-compatible-write-gated',
|
|
124
|
+
changes: [
|
|
125
|
+
'Adds canonical Graph v2 entities and ID-based edges.',
|
|
126
|
+
'Binds the portable ontology release to the canonical Relation Registry.'
|
|
127
|
+
],
|
|
128
|
+
migration: 'Run ontology:audit --ontology-version 1.0.0, preview ontology:migrate, then write using the preview expectedInputDigest.',
|
|
129
|
+
rollback: 'Restore the pre-migration Personal OS backup; the immutable 1.0.0 interpretation remains available for historical reads.'
|
|
130
|
+
}
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
export const portableOntology = deepFreeze(releaseV2);
|
|
87
136
|
export function auditOntology(os, options = {}) {
|
|
88
|
-
const ontologyVersion =
|
|
137
|
+
const ontologyVersion = resolvePersonalOsOntologyVersion(os, options.ontologyVersion);
|
|
89
138
|
const violations = [];
|
|
90
139
|
// 0.0.0 names the pre-kernel legacy semantics. Canonical shape validation is
|
|
91
140
|
// still performed by the SSOT reader, but no 1.0.0 semantic rules are
|
|
@@ -118,7 +167,7 @@ export function assertOntologyValid(os) {
|
|
|
118
167
|
}
|
|
119
168
|
}
|
|
120
169
|
export function inferPersonalOs(os, options = {}) {
|
|
121
|
-
const ontologyVersion =
|
|
170
|
+
const ontologyVersion = resolvePersonalOsOntologyVersion(os, options.ontologyVersion);
|
|
122
171
|
const audit = auditOntology(os, { ontologyVersion });
|
|
123
172
|
const errors = audit.violations.filter((violation) => violation.severity === 'error');
|
|
124
173
|
if (errors.length > 0) {
|
|
@@ -311,6 +360,13 @@ export function resolveOntologyVersion(version) {
|
|
|
311
360
|
}
|
|
312
361
|
return requested;
|
|
313
362
|
}
|
|
363
|
+
function resolvePersonalOsOntologyVersion(os, requestedVersion) {
|
|
364
|
+
if (requestedVersion !== undefined)
|
|
365
|
+
return resolveOntologyVersion(requestedVersion);
|
|
366
|
+
if (os.graph.version === 2)
|
|
367
|
+
return resolveOntologyVersion(os.graph.ontology.version);
|
|
368
|
+
return resolveOntologyVersion(undefined);
|
|
369
|
+
}
|
|
314
370
|
function auditDecisionSupersession(decisions, violations) {
|
|
315
371
|
const ids = new Set(decisions.map((decision) => decision.id));
|
|
316
372
|
const edges = new Map(decisions.map((decision) => [decision.id, decision.supersedes ?? []]));
|
|
@@ -51,21 +51,21 @@ export function buildOperationalizationPlan(input = {}) {
|
|
|
51
51
|
},
|
|
52
52
|
{
|
|
53
53
|
id: 'verification',
|
|
54
|
-
title: 'doctorとMCP get_context / searchで確認する',
|
|
54
|
+
title: 'doctorとMCP resolve_entity / get_context / searchで確認する',
|
|
55
55
|
status: 'pending',
|
|
56
56
|
command: `brainbase doctor --dir ${commandArg(dataDir)}`,
|
|
57
57
|
why: '新しいエージェントセッションでdoctorとMCPツールが承認済み文脈を示して初めて運用可能です。',
|
|
58
|
-
safety: 'コマンド生成だけで完了扱いせず、設定反映後にget_context/searchを確認します。'
|
|
58
|
+
safety: 'コマンド生成だけで完了扱いせず、設定反映後にresolve_entity/get_context/searchを確認します。'
|
|
59
59
|
}
|
|
60
60
|
],
|
|
61
61
|
recommendedOrder: [
|
|
62
62
|
'Brainbase MCP設定を実際のCodex / Claude / CodeCode設定へ反映し、エージェントを再起動する。',
|
|
63
|
-
'新しいエージェントで現実の依頼を送り、MCP get_context
|
|
63
|
+
'新しいエージェントで現実の依頼を送り、MCP resolve_entity、get_context、searchを使った実回答を確認する。',
|
|
64
64
|
'利用者本人に役立ったかを確認する。役立たなければ不足文脈だけを追加して再試行する。',
|
|
65
65
|
'対象エージェントへ公開スキルを配置する。',
|
|
66
66
|
'必要になった時だけohayo / oyasumi / retroを確認付きで登録する。',
|
|
67
67
|
'追加文脈が必要な場合だけ参照許可範囲を決め、取込と候補レビューを行う。',
|
|
68
|
-
'doctorを実行し、新しいエージェントセッションでMCP get_context
|
|
68
|
+
'doctorを実行し、新しいエージェントセッションでMCP resolve_entity、get_context、searchを確認する。'
|
|
69
69
|
],
|
|
70
70
|
safetyRules: [
|
|
71
71
|
'利用者の承認なしに実設定、スケジュール、正本ファクトを書き込みません。',
|
|
@@ -74,10 +74,10 @@ export function buildOperationalizationPlan(input = {}) {
|
|
|
74
74
|
'参照元の取込はローカル優先とし、正本化前にレビューします。'
|
|
75
75
|
],
|
|
76
76
|
completionCheck: [
|
|
77
|
-
'実際のエージェントがBrainbase MCP get_context/searchを使って現実の依頼へ回答済み。',
|
|
77
|
+
'実際のエージェントがBrainbase MCP resolve_entity/get_context/searchを使って現実の依頼へ回答済み。',
|
|
78
78
|
'利用者本人がその回答を役立つと確認済み。',
|
|
79
79
|
'完了報告に残っている運用設定をすべて記載済み。',
|
|
80
|
-
'実設定反映後、対象エージェントからBrainbase MCP get_context/searchを呼び出せる。',
|
|
80
|
+
'実設定反映後、対象エージェントからBrainbase MCP resolve_entity/get_context/searchを呼び出せる。',
|
|
81
81
|
'参照許可範囲と候補レビューを完了、または明示的に延期済み。'
|
|
82
82
|
]
|
|
83
83
|
};
|
package/dist/projects.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DecisionRecord, GraphEntity, PersonalKgEntry, RelationshipRecord } from './types.js';
|
|
1
|
+
import type { CanonicalEdge, CanonicalEntity, DecisionRecord, GraphEntity, PersonalKgEntry, RelationshipRecord } from './types.js';
|
|
2
2
|
export type ProjectSourceArea = 'mail' | 'calendar' | 'drive' | 'tasks' | 'local' | 'other';
|
|
3
3
|
export interface ProjectSourceReference {
|
|
4
4
|
area: ProjectSourceArea;
|
|
@@ -38,6 +38,8 @@ export interface ProjectRegistrationPlan {
|
|
|
38
38
|
stakeholders: ProjectStakeholder[];
|
|
39
39
|
writes: {
|
|
40
40
|
graphEntities: GraphEntity[];
|
|
41
|
+
canonicalEntities: CanonicalEntity[];
|
|
42
|
+
canonicalEdges: CanonicalEdge[];
|
|
41
43
|
relationships: RelationshipRecord[];
|
|
42
44
|
personalKg: PersonalKgEntry[];
|
|
43
45
|
decisions: DecisionRecord[];
|