@unson/brainbase-mcp 0.37.0 → 0.39.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/dist/graph-corrections.d.ts +16 -1
- package/dist/graph-corrections.js +81 -2
- package/dist/graph-maintenance-engine.js +20 -3
- package/dist/graph-revision-reader.js +1 -0
- package/dist/graph-web-http.d.ts +1 -1
- package/dist/graph-web-http.js +7 -2
- package/dist/graph-web.d.ts +73 -0
- package/dist/graph-web.js +112 -2
- package/dist/local-web-host.js +3 -1
- package/dist/relation-registry.js +8 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.js +2 -2
- package/dist/templates.js +1 -1
- package/dist/types.d.ts +1 -1
- package/package.json +4 -2
- package/ui/graph-view-shared.js +87 -7
- package/ui/local-web-shell.css +13 -0
- package/ui/local-web-shell.js +61 -10
- package/ui/people-and-companies-view.css +121 -0
- package/ui/people-and-companies-view.js +596 -0
|
@@ -14,12 +14,16 @@ export declare const GRAPH_CORRECTION_ENTITY_FIELDS: readonly ["name", "aliases"
|
|
|
14
14
|
export declare const GRAPH_CORRECTION_PROJECT_FIELDS: readonly ["goal", "status", "icon"];
|
|
15
15
|
export declare const GRAPH_CORRECTION_EDGE_FIELDS: readonly ["role", "context", "validTo"];
|
|
16
16
|
export declare const GRAPH_CORRECTION_NEW_EDGE_RELATIONS: readonly ["participates_in", "accountable_for", "member_of"];
|
|
17
|
+
/** Kinds a correction may create (people and companies only). */
|
|
18
|
+
export declare const GRAPH_CORRECTION_NEW_ENTITY_TYPES: readonly ["person", "org"];
|
|
19
|
+
export declare const GRAPH_CORRECTION_NEW_ENTITY_FIELDS: readonly ["type", "name", "aliases", "summary", "validFrom"];
|
|
17
20
|
/** MCP tools that read the same graph.json on every call and therefore see a saved correction next time. */
|
|
18
21
|
export declare const GRAPH_CORRECTION_APPLIES_TO: readonly ["search", "get_context", "resolve_entity"];
|
|
19
22
|
/** Decoded bytes, shared with the browser-side project-icon helper. */
|
|
20
23
|
export declare const PROJECT_ICON_MAX_BYTES: number;
|
|
21
|
-
export type GraphCorrectionKind = 'update_entity' | 'update_edge' | 'create_edge';
|
|
24
|
+
export type GraphCorrectionKind = 'update_entity' | 'update_edge' | 'create_edge' | 'create_entity';
|
|
22
25
|
export type GraphNewEdgeRelation = typeof GRAPH_CORRECTION_NEW_EDGE_RELATIONS[number];
|
|
26
|
+
export type GraphNewEntityType = typeof GRAPH_CORRECTION_NEW_ENTITY_TYPES[number];
|
|
23
27
|
export interface GraphEntityCorrectionChanges {
|
|
24
28
|
name?: string;
|
|
25
29
|
aliases?: string[];
|
|
@@ -47,6 +51,13 @@ export interface GraphEdgeCreation {
|
|
|
47
51
|
context?: string;
|
|
48
52
|
validFrom?: string;
|
|
49
53
|
}
|
|
54
|
+
export interface GraphEntityCreation {
|
|
55
|
+
type: GraphNewEntityType;
|
|
56
|
+
name: string;
|
|
57
|
+
aliases?: string[];
|
|
58
|
+
summary?: string;
|
|
59
|
+
validFrom?: string;
|
|
60
|
+
}
|
|
50
61
|
export type GraphCorrectionInput = {
|
|
51
62
|
kind: 'update_entity';
|
|
52
63
|
entityId: string;
|
|
@@ -63,6 +74,10 @@ export type GraphCorrectionInput = {
|
|
|
63
74
|
kind: 'create_edge';
|
|
64
75
|
reason: string;
|
|
65
76
|
edge: GraphEdgeCreation;
|
|
77
|
+
} | {
|
|
78
|
+
kind: 'create_entity';
|
|
79
|
+
reason: string;
|
|
80
|
+
entity: GraphEntityCreation;
|
|
66
81
|
};
|
|
67
82
|
export type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
68
83
|
[key: string]: JsonValue;
|
|
@@ -21,6 +21,9 @@ export const GRAPH_CORRECTION_ENTITY_FIELDS = ['name', 'aliases', 'summary', 'va
|
|
|
21
21
|
export const GRAPH_CORRECTION_PROJECT_FIELDS = ['goal', 'status', 'icon'];
|
|
22
22
|
export const GRAPH_CORRECTION_EDGE_FIELDS = ['role', 'context', 'validTo'];
|
|
23
23
|
export const GRAPH_CORRECTION_NEW_EDGE_RELATIONS = ['participates_in', 'accountable_for', 'member_of'];
|
|
24
|
+
/** Kinds a correction may create (people and companies only). */
|
|
25
|
+
export const GRAPH_CORRECTION_NEW_ENTITY_TYPES = ['person', 'org'];
|
|
26
|
+
export const GRAPH_CORRECTION_NEW_ENTITY_FIELDS = ['type', 'name', 'aliases', 'summary', 'validFrom'];
|
|
24
27
|
/** MCP tools that read the same graph.json on every call and therefore see a saved correction next time. */
|
|
25
28
|
export const GRAPH_CORRECTION_APPLIES_TO = ['search', 'get_context', 'resolve_entity'];
|
|
26
29
|
const MAX_REASON_LENGTH = 500;
|
|
@@ -83,7 +86,11 @@ export function parseGraphCorrectionInput(value) {
|
|
|
83
86
|
assertOnlyKeys(value, ['kind', 'reason', 'edge'], '');
|
|
84
87
|
return { kind, reason: requireReason(value.reason), edge: parseEdgeCreation(value.edge) };
|
|
85
88
|
}
|
|
86
|
-
|
|
89
|
+
if (kind === 'create_entity') {
|
|
90
|
+
assertOnlyKeys(value, ['kind', 'reason', 'entity'], '');
|
|
91
|
+
return { kind, reason: requireReason(value.reason), entity: parseEntityCreation(value.entity) };
|
|
92
|
+
}
|
|
93
|
+
invalid('correction_kind_unsupported', 'kind must be update_entity, update_edge, create_edge or create_entity');
|
|
87
94
|
}
|
|
88
95
|
/**
|
|
89
96
|
* Applies one owner correction to the canonical Graph v2 in `dataDir`.
|
|
@@ -190,7 +197,7 @@ function isGraphCorrectionRecord(value) {
|
|
|
190
197
|
const target = value.target;
|
|
191
198
|
return typeof value.id === 'string'
|
|
192
199
|
&& typeof value.at === 'string'
|
|
193
|
-
&& (value.kind === 'update_entity' || value.kind === 'update_edge' || value.kind === 'create_edge')
|
|
200
|
+
&& (value.kind === 'update_entity' || value.kind === 'update_edge' || value.kind === 'create_edge' || value.kind === 'create_entity')
|
|
194
201
|
&& isRecord(target) && (target.recordType === 'entity' || target.recordType === 'edge') && typeof target.id === 'string'
|
|
195
202
|
&& typeof value.reason === 'string'
|
|
196
203
|
&& Array.isArray(value.changedFields) && value.changedFields.every((field) => typeof field === 'string')
|
|
@@ -204,8 +211,51 @@ function planCorrection(graph, relationships, correction, at) {
|
|
|
204
211
|
return planEntityUpdate(graph, relationships, correction, at);
|
|
205
212
|
if (correction.kind === 'update_edge')
|
|
206
213
|
return planEdgeUpdate(graph, relationships, correction, at);
|
|
214
|
+
if (correction.kind === 'create_entity')
|
|
215
|
+
return planEntityCreation(graph, relationships, correction, at);
|
|
207
216
|
return planEdgeCreation(graph, relationships, correction, at);
|
|
208
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* Creates a person or a company. A record of the same kind whose name or
|
|
220
|
+
* alias already reads the same (ignoring spacing, case and full/half width) is
|
|
221
|
+
* returned as a conflict instead of creating a duplicate. The new record
|
|
222
|
+
* keeps where it came from (`metadata.source`, `metadata.sourceId`).
|
|
223
|
+
*/
|
|
224
|
+
function planEntityCreation(graph, relationships, correction, at) {
|
|
225
|
+
const input = correction.entity;
|
|
226
|
+
const names = new Set([input.name, ...(input.aliases ?? [])].map(comparableName));
|
|
227
|
+
const existing = graph.entities.find((entity) => entity.type === input.type
|
|
228
|
+
&& [entity.name, ...(entity.aliases ?? [])].some((label) => names.has(comparableName(label))));
|
|
229
|
+
if (existing) {
|
|
230
|
+
throw new GraphCorrectionError('conflict', 'entity_already_exists', 'A record with the same name already exists; open it (and clear validTo if it has ended) instead of creating another', { recordType: 'entity', record: existing, digest: graphRecordDigest(existing) });
|
|
231
|
+
}
|
|
232
|
+
const base = `${input.type}-${stableHash(input.name)}`;
|
|
233
|
+
const correctionId = graphCorrectionId(at, 'create_entity', base, null, correction.reason);
|
|
234
|
+
const id = graph.entities.some((entity) => entity.id === base) ? `${base}-${correctionId.slice(-8)}` : base;
|
|
235
|
+
if (graph.entities.some((entity) => entity.id === id)) {
|
|
236
|
+
invalid('entity_id_ambiguous', `Entity id ${id} is already used; repair graph.json with the CLI first`);
|
|
237
|
+
}
|
|
238
|
+
const entity = compact({
|
|
239
|
+
id,
|
|
240
|
+
type: input.type,
|
|
241
|
+
name: input.name,
|
|
242
|
+
aliases: input.aliases && input.aliases.length > 0 ? input.aliases : undefined,
|
|
243
|
+
summary: input.summary,
|
|
244
|
+
validFrom: input.validFrom,
|
|
245
|
+
metadata: { source: 'user_approved', sourceId: correctionId }
|
|
246
|
+
});
|
|
247
|
+
const nextGraph = validatedGraph({ ...graph, entities: [...graph.entities, entity] });
|
|
248
|
+
const diff = {};
|
|
249
|
+
for (const [key, value] of Object.entries(entity)) {
|
|
250
|
+
if (key === 'id')
|
|
251
|
+
continue;
|
|
252
|
+
diff[key] = { before: null, after: value };
|
|
253
|
+
}
|
|
254
|
+
return finalize('create_entity', 'entity', null, entity, diff, correction.reason, at, nextGraph, { relationships, changes: [] }, correctionId);
|
|
255
|
+
}
|
|
256
|
+
function comparableName(value) {
|
|
257
|
+
return value.normalize('NFKC').replace(/\s+/gu, '').toLowerCase();
|
|
258
|
+
}
|
|
209
259
|
function planEntityUpdate(graph, relationships, correction, at) {
|
|
210
260
|
const matches = graph.entities.filter((entity) => entity.id === correction.entityId);
|
|
211
261
|
if (matches.length === 0)
|
|
@@ -539,6 +589,35 @@ function parseEdgeCreation(value) {
|
|
|
539
589
|
validFrom: validFrom ?? undefined
|
|
540
590
|
});
|
|
541
591
|
}
|
|
592
|
+
function parseEntityCreation(value) {
|
|
593
|
+
if (!isRecord(value))
|
|
594
|
+
invalid('correction_invalid', 'entity must be an object');
|
|
595
|
+
const type = value.type;
|
|
596
|
+
if (typeof type !== 'string' || !GRAPH_CORRECTION_NEW_ENTITY_TYPES.includes(type)) {
|
|
597
|
+
invalid('correction_entity_type_not_allowed', `entity.type must be one of ${GRAPH_CORRECTION_NEW_ENTITY_TYPES.join(', ')}`);
|
|
598
|
+
}
|
|
599
|
+
const allowed = GRAPH_CORRECTION_NEW_ENTITY_FIELDS.filter((key) => key !== 'type');
|
|
600
|
+
for (const key of Object.keys(value)) {
|
|
601
|
+
if (key === 'type' || allowed.includes(key))
|
|
602
|
+
continue;
|
|
603
|
+
if (key === 'id')
|
|
604
|
+
invalid('correction_id_change_not_allowed', 'entity.id is assigned by Brainbase');
|
|
605
|
+
invalid('correction_field_not_allowed', `entity.${key} cannot be set when creating a record`);
|
|
606
|
+
}
|
|
607
|
+
const name = value.name === undefined ? null : optionalText(value.name, 'entity.name', MAX_NAME_LENGTH);
|
|
608
|
+
if (name === null)
|
|
609
|
+
invalid('correction_name_required', 'entity.name must be a non-empty string');
|
|
610
|
+
const aliases = value.aliases === undefined ? [] : parseAliases(value.aliases).filter((alias) => alias !== name);
|
|
611
|
+
const summary = value.summary === undefined ? null : optionalText(value.summary, 'entity.summary', MAX_TEXT_LENGTH);
|
|
612
|
+
const validFrom = value.validFrom === undefined ? null : optionalDateTime(value.validFrom, 'entity.validFrom');
|
|
613
|
+
return compact({
|
|
614
|
+
type: type,
|
|
615
|
+
name,
|
|
616
|
+
aliases: aliases.length > 0 ? aliases : undefined,
|
|
617
|
+
summary: summary ?? undefined,
|
|
618
|
+
validFrom: validFrom ?? undefined
|
|
619
|
+
});
|
|
620
|
+
}
|
|
542
621
|
function assertOnlyKeys(value, allowed, prefix) {
|
|
543
622
|
for (const key of Object.keys(value)) {
|
|
544
623
|
if (allowed.includes(key))
|
|
@@ -425,8 +425,20 @@ export function applyGraphOperations(snapshot, operations, { projectCode, humanG
|
|
|
425
425
|
&& !externalIds.has(toEntity.id)
|
|
426
426
|
&& toEntity.entity_type === 'app'
|
|
427
427
|
&& toEntity.lifecycle_status === 'active';
|
|
428
|
-
|
|
428
|
+
// A person's membership of a company: both active, each either in this
|
|
429
|
+
// snapshot or a same-organization reference. Cross-organization
|
|
430
|
+
// references are never a membership endpoint.
|
|
431
|
+
const membershipFrom = localFrom || externalFrom;
|
|
432
|
+
const sameOrganizationPersonMembership = operation.rel_type === 'member_of'
|
|
433
|
+
&& membershipFrom?.entity_type === 'person'
|
|
434
|
+
&& membershipFrom.lifecycle_status === 'active'
|
|
435
|
+
&& (Boolean(localFrom) || (externalIds.has(membershipFrom.id) && membershipFrom.reference_scope === 'same_organization'))
|
|
436
|
+
&& toEntity.entity_type === 'org'
|
|
437
|
+
&& toEntity.lifecycle_status === 'active'
|
|
438
|
+
&& (!externalIds.has(toEntity.id) || toEntity.reference_scope === 'same_organization');
|
|
439
|
+
if (!localFrom && !sameOrganizationToolAccountable && !sameOrganizationPersonMembership) {
|
|
429
440
|
throw new Error(`Unknown entity: ${operation.from_id}`);
|
|
441
|
+
}
|
|
430
442
|
const fromEntity = (localFrom || externalFrom);
|
|
431
443
|
// A project of this scope using a same-organization tool of another project.
|
|
432
444
|
const sameOrganizationProjectUsesApp = operation.rel_type === 'uses_app'
|
|
@@ -455,7 +467,7 @@ export function applyGraphOperations(snapshot, operations, { projectCode, humanG
|
|
|
455
467
|
throw new Error('decision_id must match the Decision owner source');
|
|
456
468
|
}
|
|
457
469
|
if (fromEntity.project_code !== toEntity.project_code && !sameOrganizationAppOwner && !sameOrganizationDecisionOwner
|
|
458
|
-
&& !sameOrganizationToolAccountable && !sameOrganizationProjectUsesApp) {
|
|
470
|
+
&& !sameOrganizationToolAccountable && !sameOrganizationProjectUsesApp && !sameOrganizationPersonMembership) {
|
|
459
471
|
throw new Error('edge endpoint project scope mismatch');
|
|
460
472
|
}
|
|
461
473
|
if (!String(operation.rel_type || '').trim())
|
|
@@ -491,7 +503,12 @@ export function applyGraphOperations(snapshot, operations, { projectCode, humanG
|
|
|
491
503
|
id: operation.edge_id,
|
|
492
504
|
from_id: operation.from_id, to_id: operation.to_id, rel_type: operation.rel_type,
|
|
493
505
|
// The accountable person lives in another project; the edge belongs to the tool's project.
|
|
494
|
-
|
|
506
|
+
// A membership belongs to the company's project when the company is
|
|
507
|
+
// in this snapshot, else to the person's, else to the plan's.
|
|
508
|
+
project_code: sameOrganizationToolAccountable ? toEntity.project_code
|
|
509
|
+
: sameOrganizationPersonMembership
|
|
510
|
+
? (!externalIds.has(toEntity.id) ? toEntity.project_code : localFrom ? fromEntity.project_code : state.project_code)
|
|
511
|
+
: fromEntity.project_code,
|
|
495
512
|
payload: operation.payload || {}, role_min: roleMin,
|
|
496
513
|
sensitivity, lifecycle_status: 'active', version: 1
|
|
497
514
|
});
|
package/dist/graph-web-http.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export type GraphWebHttpHandler = (request: IncomingMessage, response: ServerRes
|
|
|
16
16
|
/**
|
|
17
17
|
* Node-style handler for the local Graph read and correction API:
|
|
18
18
|
*
|
|
19
|
-
* - `GET {base}/status`, `GET {base}/projects`, `GET {base}/projects/:id`
|
|
19
|
+
* - `GET {base}/status`, `GET {base}/projects`, `GET {base}/projects/:id`, `GET {base}/people-and-companies`
|
|
20
20
|
* - `GET {base}/search?q=&type=&as_of=&limit=`, `GET {base}/entities/:id`, `GET {base}/ontology`
|
|
21
21
|
* - `POST {base}/corrections`
|
|
22
22
|
*
|
package/dist/graph-web-http.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { applyGraphCorrection, GraphCorrectionError } from './graph-corrections.js';
|
|
2
|
-
import { GraphWebError, listGraphProjects, readGraphEntity, readGraphOntology, readGraphProject, readGraphWebStatus, searchGraphEntities } from './graph-web.js';
|
|
2
|
+
import { GraphWebError, listGraphProjects, readGraphEntity, readGraphOntology, readGraphPeopleAndCompanies, readGraphProject, readGraphWebStatus, searchGraphEntities } from './graph-web.js';
|
|
3
3
|
export const GRAPH_WEB_HTTP_VERSION = 'graph-web-http.v1';
|
|
4
4
|
const DEFAULT_BASE_PATH = '/api/graph';
|
|
5
5
|
// A 256KiB decoded image becomes roughly 350KiB as base64 in JSON. Keep a
|
|
@@ -21,7 +21,7 @@ class HttpError extends Error {
|
|
|
21
21
|
/**
|
|
22
22
|
* Node-style handler for the local Graph read and correction API:
|
|
23
23
|
*
|
|
24
|
-
* - `GET {base}/status`, `GET {base}/projects`, `GET {base}/projects/:id`
|
|
24
|
+
* - `GET {base}/status`, `GET {base}/projects`, `GET {base}/projects/:id`, `GET {base}/people-and-companies`
|
|
25
25
|
* - `GET {base}/search?q=&type=&as_of=&limit=`, `GET {base}/entities/:id`, `GET {base}/ontology`
|
|
26
26
|
* - `POST {base}/corrections`
|
|
27
27
|
*
|
|
@@ -74,6 +74,9 @@ export function createGraphWebHttpHandler(options) {
|
|
|
74
74
|
case 'project':
|
|
75
75
|
writeJson(response, 200, await readGraphProject(dataDir, route.id, readOptions));
|
|
76
76
|
break;
|
|
77
|
+
case 'people':
|
|
78
|
+
writeJson(response, 200, await readGraphPeopleAndCompanies(dataDir, readOptions));
|
|
79
|
+
break;
|
|
77
80
|
case 'search':
|
|
78
81
|
writeJson(response, 200, await searchGraphEntities(dataDir, {
|
|
79
82
|
q: queryValue(url, 'q'),
|
|
@@ -105,6 +108,8 @@ function matchRoute(pathname, basePath) {
|
|
|
105
108
|
return { name: 'status' };
|
|
106
109
|
if (rest === '/projects')
|
|
107
110
|
return { name: 'projects' };
|
|
111
|
+
if (rest === '/people-and-companies')
|
|
112
|
+
return { name: 'people' };
|
|
108
113
|
if (rest === '/search')
|
|
109
114
|
return { name: 'search' };
|
|
110
115
|
if (rest === '/ontology')
|
package/dist/graph-web.d.ts
CHANGED
|
@@ -320,6 +320,8 @@ export interface GraphOntologySummary<S extends GraphWebAnySource = GraphWebSour
|
|
|
320
320
|
projectFields: readonly string[];
|
|
321
321
|
edgeFields: readonly string[];
|
|
322
322
|
newEdgeRelations: readonly string[];
|
|
323
|
+
/** Kinds a correction may create (`create_entity`). */
|
|
324
|
+
newEntityTypes: readonly string[];
|
|
323
325
|
};
|
|
324
326
|
}
|
|
325
327
|
export type GraphWebErrorKind = 'invalid' | 'not_found' | 'unavailable';
|
|
@@ -335,6 +337,77 @@ export interface GraphWebReadOptions {
|
|
|
335
337
|
}
|
|
336
338
|
export declare function readGraphWebStatus(dataDir: string, options?: GraphWebReadOptions): Promise<GraphWebStatus | GraphWebUnavailable>;
|
|
337
339
|
export declare function listGraphProjects(dataDir: string, options?: GraphWebReadOptions): Promise<GraphProjectList | GraphWebUnavailable>;
|
|
340
|
+
export type GraphOwnCompanyReason = 'resolved' | 'owner_not_found' | 'owner_has_no_company' | 'owner_has_several_companies';
|
|
341
|
+
export interface GraphPersonSummaryView extends GraphEntityView {
|
|
342
|
+
type: 'person';
|
|
343
|
+
/** The owner of this Graph (the person whose companies decide the own company). */
|
|
344
|
+
isOwner: boolean;
|
|
345
|
+
/** Active companies of the person (active member_of to an active org), by company name. */
|
|
346
|
+
companyIds: string[];
|
|
347
|
+
/** Distinct active projects with an active participates_in or accountable_for relation. */
|
|
348
|
+
projectCount: number;
|
|
349
|
+
accountableProjectCount: number;
|
|
350
|
+
/**
|
|
351
|
+
* The company text recorded on the person (`org`, or `organization` when
|
|
352
|
+
* `org` is absent), as written; null when none. A note only: it never
|
|
353
|
+
* decides the companies, the own company or internal / external.
|
|
354
|
+
*/
|
|
355
|
+
orgNote: string | null;
|
|
356
|
+
}
|
|
357
|
+
export type GraphCompanyKind = 'customer' | 'partner' | 'prospect';
|
|
358
|
+
export type GraphCompanyKindSource = 'orgType' | 'lifecycle_stage' | 'related_to' | 'deals';
|
|
359
|
+
export interface GraphCompanyDealView {
|
|
360
|
+
/** The deal name as recorded (`name` or `title`), or null when the record has none. */
|
|
361
|
+
name: string | null;
|
|
362
|
+
/** The deal stage as recorded (`stage` or `status`), or null. */
|
|
363
|
+
stage: string | null;
|
|
364
|
+
}
|
|
365
|
+
export interface GraphCompanySummaryView extends GraphEntityView {
|
|
366
|
+
type: 'org';
|
|
367
|
+
/** Active people with an active member_of to the company, by name. */
|
|
368
|
+
memberIds: string[];
|
|
369
|
+
/** Active projects with an active owned_by to the company, by name. */
|
|
370
|
+
projectIds: string[];
|
|
371
|
+
/** Active projects the company is involved in through an active related_to, by name, with the edge role. */
|
|
372
|
+
relatedProjects: Array<{
|
|
373
|
+
projectId: string;
|
|
374
|
+
role: string | null;
|
|
375
|
+
}>;
|
|
376
|
+
/** The recorded company attributes, as written; null when not recorded. */
|
|
377
|
+
orgType: string | null;
|
|
378
|
+
lifecycleStage: string | null;
|
|
379
|
+
/** Proposals in progress recorded on the company (`deals`). */
|
|
380
|
+
deals: GraphCompanyDealView[];
|
|
381
|
+
/**
|
|
382
|
+
* What kind of company this is for the owner, only from the records above
|
|
383
|
+
* (attributes, related_to roles and deals), with where each kind came from.
|
|
384
|
+
* Empty when nothing is recorded: the kind is never guessed.
|
|
385
|
+
*/
|
|
386
|
+
kinds: Array<{
|
|
387
|
+
kind: GraphCompanyKind;
|
|
388
|
+
sources: GraphCompanyKindSource[];
|
|
389
|
+
}>;
|
|
390
|
+
}
|
|
391
|
+
export interface GraphPeopleAndCompanies<S extends GraphWebAnySource = GraphWebSource> {
|
|
392
|
+
status: 'ok';
|
|
393
|
+
source: S;
|
|
394
|
+
asOf: string;
|
|
395
|
+
/**
|
|
396
|
+
* The company the owner belongs to. Null with the reason when the owner has
|
|
397
|
+
* no active company, several, or no record: the screen then does not tell
|
|
398
|
+
* internal from external people. No internal / external mark is stored.
|
|
399
|
+
*/
|
|
400
|
+
ownCompany: {
|
|
401
|
+
id: string | null;
|
|
402
|
+
reason: GraphOwnCompanyReason;
|
|
403
|
+
candidateIds: string[];
|
|
404
|
+
};
|
|
405
|
+
people: GraphPersonSummaryView[];
|
|
406
|
+
companies: GraphCompanySummaryView[];
|
|
407
|
+
/** True only when the readable Graph v2 holds no person and no company. */
|
|
408
|
+
absenceConfirmed: boolean;
|
|
409
|
+
}
|
|
410
|
+
export declare function readGraphPeopleAndCompanies(dataDir: string, options?: GraphWebReadOptions): Promise<GraphPeopleAndCompanies | GraphWebUnavailable>;
|
|
338
411
|
export declare function readGraphProject(dataDir: string, projectId: string, options?: GraphWebReadOptions): Promise<GraphProjectDetail | GraphWebUnavailable>;
|
|
339
412
|
export declare function searchGraphEntities(dataDir: string, input?: GraphEntitySearchInput): Promise<GraphEntitySearchResult | GraphWebUnavailable>;
|
|
340
413
|
export declare function readGraphEntity(dataDir: string, entityId: string, options?: GraphWebReadOptions): Promise<GraphEntityDetail | GraphWebUnavailable>;
|
package/dist/graph-web.js
CHANGED
|
@@ -2,7 +2,7 @@ import { access, readdir, readFile } from 'node:fs/promises';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { isActiveAt } from './canonical-graph.js';
|
|
4
4
|
import { assertFoundationCatalog, findFoundationRecord } from './foundation-catalog.js';
|
|
5
|
-
import { GRAPH_CORRECTION_EDGE_FIELDS, GRAPH_CORRECTION_ENTITY_FIELDS, GRAPH_CORRECTION_NEW_EDGE_RELATIONS, GRAPH_CORRECTION_PROJECT_FIELDS, GRAPH_CORRECTIONS_FILE, graphRecordDigest, isProjectIconDataUrl, readGraphCorrectionHistory } from './graph-corrections.js';
|
|
5
|
+
import { GRAPH_CORRECTION_EDGE_FIELDS, GRAPH_CORRECTION_ENTITY_FIELDS, GRAPH_CORRECTION_NEW_EDGE_RELATIONS, GRAPH_CORRECTION_NEW_ENTITY_TYPES, GRAPH_CORRECTION_PROJECT_FIELDS, GRAPH_CORRECTIONS_FILE, graphRecordDigest, isProjectIconDataUrl, readGraphCorrectionHistory } from './graph-corrections.js';
|
|
6
6
|
import { ONTOLOGY_VERSION, portableOntology } from './ontology.js';
|
|
7
7
|
import { canonicalRelationRegistry, getCanonicalRelation } from './relation-registry.js';
|
|
8
8
|
import { loadPersonalOs } from './ssot.js';
|
|
@@ -93,6 +93,115 @@ function projectListOf(graph, source, asOf) {
|
|
|
93
93
|
}).sort(byActiveThenName);
|
|
94
94
|
return { status: 'ok', source, asOf, projects, absenceConfirmed: projects.length === 0 };
|
|
95
95
|
}
|
|
96
|
+
export async function readGraphPeopleAndCompanies(dataDir, options = {}) {
|
|
97
|
+
const loaded = await loadForRead(dataDir);
|
|
98
|
+
if ('status' in loaded)
|
|
99
|
+
return loaded;
|
|
100
|
+
return peopleAndCompaniesOf(loaded.graph, loaded.source, resolveAsOf(options));
|
|
101
|
+
}
|
|
102
|
+
function peopleAndCompaniesOf(graph, source, asOf) {
|
|
103
|
+
const entities = entityIndex(graph);
|
|
104
|
+
const name = (id) => entities.get(id)?.name ?? id;
|
|
105
|
+
const byName = (left, right) => name(left).localeCompare(name(right), 'ja') || left.localeCompare(right, 'en');
|
|
106
|
+
const live = graph.edges.filter((edge) => isActiveAt(edge, asOf)
|
|
107
|
+
&& isEntityActive(entities.get(edge.fromId), asOf) && isEntityActive(entities.get(edge.toId), asOf));
|
|
108
|
+
const membership = live.filter((edge) => edge.relation === 'member_of' && entities.get(edge.toId)?.type === 'org');
|
|
109
|
+
const companiesOf = (personId) => [...new Set(membership.filter((edge) => edge.fromId === personId).map((edge) => edge.toId))].sort(byName);
|
|
110
|
+
const ownerId = graph.owner?.id ?? 'self';
|
|
111
|
+
const owner = entities.get(ownerId);
|
|
112
|
+
const ownCandidates = owner?.type === 'person' && isActiveAt(owner, asOf) ? companiesOf(ownerId) : [];
|
|
113
|
+
const ownCompany = owner?.type !== 'person' || !isActiveAt(owner, asOf)
|
|
114
|
+
? { id: null, reason: 'owner_not_found', candidateIds: [] }
|
|
115
|
+
: ownCandidates.length === 1
|
|
116
|
+
? { id: ownCandidates[0], reason: 'resolved', candidateIds: ownCandidates }
|
|
117
|
+
: { id: null, reason: ownCandidates.length === 0 ? 'owner_has_no_company' : 'owner_has_several_companies', candidateIds: ownCandidates };
|
|
118
|
+
const people = graph.entities.filter((entity) => entity.type === 'person').map((person) => {
|
|
119
|
+
const work = live.filter((edge) => edge.fromId === person.id && PARTICIPATION.has(edge.relation) && entities.get(edge.toId)?.type === 'project');
|
|
120
|
+
return {
|
|
121
|
+
...entityView(person, asOf),
|
|
122
|
+
type: 'person',
|
|
123
|
+
isOwner: person.id === ownerId,
|
|
124
|
+
companyIds: companiesOf(person.id),
|
|
125
|
+
projectCount: new Set(work.map((edge) => edge.toId)).size,
|
|
126
|
+
accountableProjectCount: new Set(work.filter((edge) => edge.relation === 'accountable_for').map((edge) => edge.toId)).size,
|
|
127
|
+
orgNote: metadataString(person, 'org')?.trim() || metadataString(person, 'organization')?.trim() || null
|
|
128
|
+
};
|
|
129
|
+
}).sort((left, right) => Number(right.active) - Number(left.active) || Number(right.isOwner) - Number(left.isOwner) || byActiveThenName(left, right));
|
|
130
|
+
const companies = graph.entities.filter((entity) => entity.type === 'org').map((company) => {
|
|
131
|
+
const relatedProjects = live.filter((edge) => edge.relation === 'related_to' && edge.fromId === company.id
|
|
132
|
+
&& entities.get(edge.toId)?.type === 'project')
|
|
133
|
+
.map((edge) => ({ projectId: edge.toId, role: edge.role?.trim() || null }))
|
|
134
|
+
.sort((left, right) => byName(left.projectId, right.projectId));
|
|
135
|
+
const orgType = metadataString(company, 'orgType')?.trim() || null;
|
|
136
|
+
const lifecycleStage = metadataString(company, 'lifecycle_stage')?.trim() || null;
|
|
137
|
+
const deals = companyDeals(company.metadata?.deals);
|
|
138
|
+
return {
|
|
139
|
+
...entityView(company, asOf),
|
|
140
|
+
type: 'org',
|
|
141
|
+
memberIds: [...new Set(membership.filter((edge) => edge.toId === company.id).map((edge) => edge.fromId))]
|
|
142
|
+
.sort((left, right) => Number(left !== ownerId) - Number(right !== ownerId) || byName(left, right)),
|
|
143
|
+
projectIds: [...new Set(live.filter((edge) => edge.relation === 'owned_by' && edge.toId === company.id
|
|
144
|
+
&& entities.get(edge.fromId)?.type === 'project').map((edge) => edge.fromId))].sort(byName),
|
|
145
|
+
relatedProjects,
|
|
146
|
+
orgType,
|
|
147
|
+
lifecycleStage,
|
|
148
|
+
deals,
|
|
149
|
+
kinds: companyKinds({ orgType, lifecycleStage, roles: relatedProjects.map((item) => item.role), dealCount: deals.length })
|
|
150
|
+
};
|
|
151
|
+
}).sort((left, right) => Number(right.active) - Number(left.active)
|
|
152
|
+
|| Number(right.id === ownCompany.id) - Number(left.id === ownCompany.id)
|
|
153
|
+
|| Number(right.kinds.length > 0) - Number(left.kinds.length > 0)
|
|
154
|
+
|| byActiveThenName(left, right));
|
|
155
|
+
return { status: 'ok', source, asOf, ownCompany, people, companies, absenceConfirmed: people.length === 0 && companies.length === 0 };
|
|
156
|
+
}
|
|
157
|
+
const COMPANY_KINDS = ['customer', 'partner', 'prospect'];
|
|
158
|
+
const RELATION_KIND = Object.freeze({ customer: 'customer', partner: 'partner' });
|
|
159
|
+
const STAGE_KIND = Object.freeze({ customer: 'customer', partner: 'partner', opportunity: 'prospect' });
|
|
160
|
+
/**
|
|
161
|
+
* The kinds of a company, only from what is recorded: `orgType` and
|
|
162
|
+
* related_to roles of `customer` / `partner`, `lifecycle_stage` of
|
|
163
|
+
* `customer` / `partner` / `opportunity`, and any recorded deal (prospect).
|
|
164
|
+
* Other values give no kind; nothing is guessed from names.
|
|
165
|
+
*/
|
|
166
|
+
function companyKinds(input) {
|
|
167
|
+
const sources = new Map();
|
|
168
|
+
const add = (kind, source) => {
|
|
169
|
+
if (!kind)
|
|
170
|
+
return;
|
|
171
|
+
sources.set(kind, (sources.get(kind) ?? new Set()).add(source));
|
|
172
|
+
};
|
|
173
|
+
const lookup = (table, value) => {
|
|
174
|
+
const key = value?.toLowerCase();
|
|
175
|
+
return key && Object.hasOwn(table, key) ? table[key] : undefined;
|
|
176
|
+
};
|
|
177
|
+
add(lookup(RELATION_KIND, input.orgType), 'orgType');
|
|
178
|
+
add(lookup(STAGE_KIND, input.lifecycleStage), 'lifecycle_stage');
|
|
179
|
+
for (const role of input.roles)
|
|
180
|
+
add(lookup(RELATION_KIND, role), 'related_to');
|
|
181
|
+
if (input.dealCount > 0)
|
|
182
|
+
add('prospect', 'deals');
|
|
183
|
+
return COMPANY_KINDS.filter((kind) => sources.has(kind)).map((kind) => ({ kind, sources: [...sources.get(kind)] }));
|
|
184
|
+
}
|
|
185
|
+
function companyDeals(value) {
|
|
186
|
+
if (!Array.isArray(value))
|
|
187
|
+
return [];
|
|
188
|
+
return value.flatMap((deal) => {
|
|
189
|
+
if (typeof deal === 'string')
|
|
190
|
+
return deal.trim() ? [{ name: deal.trim(), stage: null }] : [];
|
|
191
|
+
if (!deal || typeof deal !== 'object' || Array.isArray(deal))
|
|
192
|
+
return [];
|
|
193
|
+
const record = deal;
|
|
194
|
+
const text = (...keys) => {
|
|
195
|
+
for (const key of keys) {
|
|
196
|
+
const item = record[key];
|
|
197
|
+
if (typeof item === 'string' && item.trim())
|
|
198
|
+
return item.trim();
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
};
|
|
202
|
+
return [{ name: text('name', 'title'), stage: text('stage', 'status') }];
|
|
203
|
+
});
|
|
204
|
+
}
|
|
96
205
|
export async function readGraphProject(dataDir, projectId, options = {}) {
|
|
97
206
|
const loaded = await loadForRead(dataDir);
|
|
98
207
|
if ('status' in loaded)
|
|
@@ -236,7 +345,8 @@ function ontologyOf(graph, source, asOf) {
|
|
|
236
345
|
entityFields: GRAPH_CORRECTION_ENTITY_FIELDS,
|
|
237
346
|
projectFields: GRAPH_CORRECTION_PROJECT_FIELDS,
|
|
238
347
|
edgeFields: GRAPH_CORRECTION_EDGE_FIELDS,
|
|
239
|
-
newEdgeRelations: GRAPH_CORRECTION_NEW_EDGE_RELATIONS
|
|
348
|
+
newEdgeRelations: GRAPH_CORRECTION_NEW_EDGE_RELATIONS,
|
|
349
|
+
newEntityTypes: GRAPH_CORRECTION_NEW_ENTITY_TYPES
|
|
240
350
|
}
|
|
241
351
|
};
|
|
242
352
|
}
|
package/dist/local-web-host.js
CHANGED
|
@@ -882,7 +882,9 @@ export function createGraphWebModule(context) {
|
|
|
882
882
|
'project-graph-entry.js',
|
|
883
883
|
'project-graph-vendor.js',
|
|
884
884
|
'graph-registry-view.js',
|
|
885
|
-
'graph-registry-view.css'
|
|
885
|
+
'graph-registry-view.css',
|
|
886
|
+
'people-and-companies-view.js',
|
|
887
|
+
'people-and-companies-view.css'
|
|
886
888
|
],
|
|
887
889
|
handle: handler
|
|
888
890
|
};
|
|
@@ -31,6 +31,14 @@ const definitions = {
|
|
|
31
31
|
scopeTraversal: 'project_direct',
|
|
32
32
|
traversalDirection: 'reverse'
|
|
33
33
|
},
|
|
34
|
+
related_to: {
|
|
35
|
+
id: 'related_to',
|
|
36
|
+
from: 'org',
|
|
37
|
+
to: 'project',
|
|
38
|
+
meaning: 'An organization is involved in a project it does not own, such as a customer or a partner; the edge role names how.',
|
|
39
|
+
scopeTraversal: 'project_direct',
|
|
40
|
+
traversalDirection: 'forward'
|
|
41
|
+
},
|
|
34
42
|
governs: {
|
|
35
43
|
id: 'governs',
|
|
36
44
|
from: 'decision',
|
package/dist/server.d.ts
CHANGED
|
@@ -73,7 +73,7 @@ export declare const toolDefinitions: readonly [{
|
|
|
73
73
|
readonly required: readonly ["relation", "direction"];
|
|
74
74
|
readonly properties: {
|
|
75
75
|
readonly relation: {
|
|
76
|
-
readonly enum: readonly ["member_of", "participates_in", "accountable_for", "owned_by", "governs", "supersedes"];
|
|
76
|
+
readonly enum: readonly ["member_of", "participates_in", "accountable_for", "owned_by", "related_to", "governs", "supersedes"];
|
|
77
77
|
};
|
|
78
78
|
readonly direction: {
|
|
79
79
|
readonly enum: readonly ["incoming", "outgoing"];
|
package/dist/server.js
CHANGED
|
@@ -30,7 +30,7 @@ const argsSchema = z.object({
|
|
|
30
30
|
as_of: z.string().datetime({ offset: true }).optional(),
|
|
31
31
|
seedIds: z.array(z.string().min(1)).min(1).max(10).optional(),
|
|
32
32
|
steps: z.array(z.object({
|
|
33
|
-
relation: z.enum(['member_of', 'participates_in', 'accountable_for', 'owned_by', 'governs', 'supersedes']),
|
|
33
|
+
relation: z.enum(['member_of', 'participates_in', 'accountable_for', 'owned_by', 'related_to', 'governs', 'supersedes']),
|
|
34
34
|
direction: z.enum(['incoming', 'outgoing']),
|
|
35
35
|
targetType: z.enum(['person', 'org', 'project', 'decision']).optional()
|
|
36
36
|
}).strict()).min(1).max(3).optional(),
|
|
@@ -188,7 +188,7 @@ export const toolDefinitions = [
|
|
|
188
188
|
seedIds: { type: 'array', minItems: 1, maxItems: 10, items: { type: 'string' }, description: 'Canonical IDs chosen by the calling model; no embeddings needed.' },
|
|
189
189
|
steps: { type: 'array', minItems: 1, maxItems: 3, items: {
|
|
190
190
|
type: 'object', additionalProperties: false, required: ['relation', 'direction'], properties: {
|
|
191
|
-
relation: { enum: ['member_of', 'participates_in', 'accountable_for', 'owned_by', 'governs', 'supersedes'] },
|
|
191
|
+
relation: { enum: ['member_of', 'participates_in', 'accountable_for', 'owned_by', 'related_to', 'governs', 'supersedes'] },
|
|
192
192
|
direction: { enum: ['incoming', 'outgoing'] },
|
|
193
193
|
targetType: { enum: ['person', 'org', 'project', 'decision'] }
|
|
194
194
|
}
|
package/dist/templates.js
CHANGED
|
@@ -85,7 +85,7 @@ export const schemaTemplates = {
|
|
|
85
85
|
id: { type: 'string', minLength: 1 },
|
|
86
86
|
fromId: { type: 'string', minLength: 1 },
|
|
87
87
|
relation: {
|
|
88
|
-
enum: ['member_of', 'participates_in', 'accountable_for', 'owned_by', 'governs', 'supersedes']
|
|
88
|
+
enum: ['member_of', 'participates_in', 'accountable_for', 'owned_by', 'related_to', 'governs', 'supersedes']
|
|
89
89
|
},
|
|
90
90
|
toId: { type: 'string', minLength: 1 },
|
|
91
91
|
role: { type: 'string' },
|
package/dist/types.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { FoundationDefinition, FoundationRelationReference, FoundationRevision } from './ontology-foundation.js';
|
|
2
2
|
export type EntityKind = 'person' | 'org' | 'project' | 'relationship' | 'decision';
|
|
3
3
|
export type CanonicalEntityKind = 'person' | 'org' | 'project' | 'decision';
|
|
4
|
-
export type CoreRelation = 'member_of' | 'participates_in' | 'accountable_for' | 'owned_by' | 'governs' | 'supersedes';
|
|
4
|
+
export type CoreRelation = 'member_of' | 'participates_in' | 'accountable_for' | 'owned_by' | 'related_to' | 'governs' | 'supersedes';
|
|
5
5
|
export interface GraphEntity {
|
|
6
6
|
id: string;
|
|
7
7
|
type: Exclude<EntityKind, 'decision'>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unson/brainbase-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.0",
|
|
4
4
|
"description": "Local-first Brainbase judgment OS that lets supported AI agents reuse a user's approved goals, priorities, decisions, and judgment criteria.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -326,6 +326,8 @@
|
|
|
326
326
|
"./ui/objective-editor-http-port": "./ui/objective-editor-http-port.js",
|
|
327
327
|
"./ui/world-model-view": "./ui/world-model-view.js",
|
|
328
328
|
"./ui/world-model-view.css": "./ui/world-model-view.css",
|
|
329
|
+
"./ui/people-and-companies-view": "./ui/people-and-companies-view.js",
|
|
330
|
+
"./ui/people-and-companies-view.css": "./ui/people-and-companies-view.css",
|
|
329
331
|
"./ui/local-web-shell": "./ui/local-web-shell.js",
|
|
330
332
|
"./ui/local-web-shell.css": "./ui/local-web-shell.css",
|
|
331
333
|
"./ui/workspace-kit": "./ui/workspace-kit.js",
|
|
@@ -479,5 +481,5 @@
|
|
|
479
481
|
"engines": {
|
|
480
482
|
"node": ">=20"
|
|
481
483
|
},
|
|
482
|
-
"gitHead": "
|
|
484
|
+
"gitHead": "f6631b4f6a088162dbd4a3d1fb974de3e599dc85"
|
|
483
485
|
}
|