@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.
@@ -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>;
package/dist/tools.js CHANGED
@@ -1,14 +1,16 @@
1
+ import { isActiveAt } from './canonical-graph.js';
1
2
  import { buildOperationalizationPlan } from './operationalization.js';
2
- export function getContext(os) {
3
+ import { getCanonicalRelation } from './relation-registry.js';
4
+ export function getContext(os, options = {}) {
3
5
  const selfEntries = os.personalKg.filter((entry) => entry.type === 'self' || entry.type === 'value' || entry.type === 'judgment');
4
6
  const workEntries = os.personalKg.filter((entry) => entry.type === 'work' || entry.type === 'experience');
5
- const projects = os.graph.entities.filter((entity) => entity.type === 'project').map((entity) => ({
6
- id: entity.id,
7
- name: entity.name,
8
- summary: entity.summary,
9
- goal: readMetadataString(entity.metadata, 'goal'),
10
- status: readMetadataString(entity.metadata, 'status'),
11
- role: readMetadataString(entity.metadata, 'role'),
7
+ const scoped = scopedRecords(os, options);
8
+ const visibleIds = os.graph.version === 2 ? new Set(scoped.records.map((record) => record.entity.id)) : undefined;
9
+ const projects = os.graph.entities
10
+ .filter((entity) => entity.type === 'project' && (!visibleIds || visibleIds.has(entity.id)))
11
+ .map((entity) => ({
12
+ id: entity.id, name: entity.name, summary: entity.summary,
13
+ goal: readMetadataString(entity.metadata, 'goal'), status: readMetadataString(entity.metadata, 'status'), role: readMetadataString(entity.metadata, 'role'),
12
14
  sources: Array.isArray(entity.metadata?.sources) ? entity.metadata.sources : [],
13
15
  taskSources: Array.isArray(entity.metadata?.taskSources) ? entity.metadata.taskSources : [],
14
16
  decisionPrinciples: Array.isArray(entity.metadata?.decisionPrinciples) ? entity.metadata.decisionPrinciples : []
@@ -17,175 +19,210 @@ export function getContext(os) {
17
19
  owner: os.graph.owner ?? {},
18
20
  self: selfEntries.map((entry) => entry.text),
19
21
  projects,
20
- work: [
21
- ...projects.map((project) => project.summary ? `${project.name}: ${project.summary}` : project.name),
22
- ...workEntries.map((entry) => entry.text)
23
- ],
24
- relationships: os.relationships.relationships.map((relationship) => ({
25
- person: relationship.person,
26
- role: relationship.role,
27
- context: relationship.context
28
- })),
29
- decisions: os.decisions.map((decision) => ({
30
- title: decision.title,
31
- decision: decision.decision,
32
- rationale: decision.rationale
33
- })),
22
+ work: [...projects.map((project) => project.summary ? `${project.name}: ${project.summary}` : project.name), ...workEntries.map((entry) => entry.text)],
23
+ relationships: os.relationships.relationships.map((relationship) => ({ person: relationship.person, role: relationship.role, context: relationship.context })),
24
+ decisions: os.decisions.map((decision) => ({ title: decision.title, decision: decision.decision, rationale: decision.rationale })),
34
25
  canonicalFiles: ['graph.json', 'personal-kg.jsonl', 'relationships.json', 'decisions.jsonl'],
35
- note: 'Canonical local SSOT data is preferred over raw sources.'
26
+ note: 'Canonical local SSOT data is preferred over raw sources.',
27
+ ...(os.graph.version === 2 ? { canonicalGraph: {
28
+ schemaVersion: 2,
29
+ ontology: os.graph.ontology,
30
+ authority: 'local_graph',
31
+ asOf: scoped.asOf,
32
+ project: scoped.project ? { id: scoped.project.id, name: scoped.project.name } : null,
33
+ records: scoped.records.map(({ entity, relationPath }) => ({
34
+ ...entity, canonicalEntityId: entity.id, recordClass: 'canonical', relationPath, authority: 'local_graph'
35
+ }))
36
+ } } : {})
36
37
  };
37
38
  }
38
39
  export function listEntities(os, type) {
39
40
  const graphEntities = os.graph.entities.filter((entity) => !type || entity.type === type);
40
- const relationshipEntities = type && type !== 'relationship'
41
- ? []
42
- : os.relationships.relationships.map((relationship) => ({
43
- id: relationship.id,
44
- type: 'relationship',
45
- name: relationship.person,
46
- summary: relationship.context,
47
- tags: relationship.tags
48
- }));
49
- const decisionEntities = type && type !== 'decision'
50
- ? []
51
- : os.decisions.map((decision) => ({
52
- id: decision.id,
53
- type: 'decision',
54
- name: decision.title,
55
- summary: decision.decision,
56
- tags: decision.tags
57
- }));
58
- return {
59
- entities: [...graphEntities, ...relationshipEntities, ...decisionEntities]
60
- };
41
+ const relationshipEntities = type && type !== 'relationship' ? [] : os.relationships.relationships.map((relationship) => ({
42
+ id: relationship.id, type: 'relationship', name: relationship.person, summary: relationship.context, tags: relationship.tags
43
+ }));
44
+ const decisionEntities = type && type !== 'decision' ? [] : os.decisions.map((decision) => ({
45
+ id: decision.id, type: 'decision', name: decision.title, summary: decision.decision, tags: decision.tags
46
+ }));
47
+ return { entities: [...graphEntities, ...relationshipEntities, ...decisionEntities] };
61
48
  }
62
49
  export function searchPersonalKg(os, query, limit = 10) {
63
50
  return rank(os.personalKg.map((entry) => ({
64
- source: 'personal-kg',
65
- id: entry.id,
66
- title: entry.type,
67
- text: entry.text,
68
- score: scoreText(query, [entry.id, entry.type, entry.text, ...(entry.tags ?? [])])
51
+ source: 'personal-kg', id: entry.id, title: entry.type, text: entry.text,
52
+ score: scoreText(query, [entry.id, entry.type, entry.text, ...(entry.tags ?? [])]),
53
+ recordClass: 'unresolved', authority: 'personal_kg'
69
54
  })), limit);
70
55
  }
71
- export function searchAll(os, query, limit = 10) {
72
- const graphResults = os.graph.entities.map((entity) => ({
73
- source: 'graph',
74
- id: entity.id,
75
- title: entity.name,
56
+ export function searchAll(os, query, limit = 10, options = {}) {
57
+ const scoped = scopedRecords(os, options);
58
+ const graphRecords = os.graph.version === 2
59
+ ? scoped.records
60
+ : os.graph.entities.map((entity) => ({ entity: entity, relationPath: [] }));
61
+ const graphResults = graphRecords.map(({ entity, relationPath }) => ({
62
+ source: 'graph', id: entity.id, title: entity.name,
76
63
  text: [entity.summary ?? entity.name, metadataText(entity.metadata)].filter(Boolean).join('\n'),
77
- score: scoreText(query, [entity.id, entity.type, entity.name, entity.summary ?? '', metadataText(entity.metadata), ...(entity.tags ?? [])])
64
+ score: scoreText(query, [entity.id, entity.type, entity.name, ...(entity.aliases ?? []), entity.summary ?? '', metadataText(entity.metadata), ...(entity.tags ?? [])]),
65
+ canonicalEntityId: entity.id, recordClass: 'canonical', relationPath, authority: 'local_graph'
78
66
  }));
79
- const relationshipResults = os.relationships.relationships.map((relationship) => ({
80
- source: 'relationships',
81
- id: relationship.id,
82
- title: relationship.person,
83
- text: relationship.context,
84
- score: scoreText(query, [relationship.id, relationship.person, relationship.role ?? '', relationship.context, ...(relationship.tags ?? [])])
85
- }));
86
- const decisionResults = os.decisions.map((decision) => ({
87
- source: 'decisions',
88
- id: decision.id,
89
- title: decision.title,
90
- text: [decision.decision, decision.rationale ?? ''].filter(Boolean).join('\n'),
91
- score: scoreText(query, [decision.id, decision.title, decision.decision, decision.rationale ?? '', ...(decision.tags ?? [])])
92
- }));
93
- return rank([...graphResults, ...searchPersonalKg(os, query, limit), ...relationshipResults, ...decisionResults], limit);
67
+ const visibleIds = new Set(scoped.records.map((record) => record.entity.id));
68
+ const targets = os.graph.version === 2 ? projectionTargets(os.graph, scoped.asOf, visibleIds) : undefined;
69
+ const relationshipResults = os.relationships.relationships.map((relationship) => {
70
+ const projectionOf = targets?.person.get(normalize(relationship.person));
71
+ return {
72
+ source: 'relationships', id: relationship.id, title: relationship.person, text: relationship.context,
73
+ score: scoreText(query, [relationship.id, relationship.person, relationship.role ?? '', relationship.context, ...(relationship.tags ?? [])]),
74
+ ...(projectionOf ? { canonicalEntityId: projectionOf, projectionOf } : {}),
75
+ recordClass: projectionOf ? 'projection' : 'unresolved', authority: 'legacy_relationships'
76
+ };
77
+ });
78
+ const decisionResults = os.decisions.map((decision) => {
79
+ const projectionOf = targets?.decision.get(decision.id) ?? targets?.decision.get(normalize(decision.title));
80
+ return {
81
+ source: 'decisions', id: decision.id, title: decision.title,
82
+ text: [decision.decision, decision.rationale ?? ''].filter(Boolean).join('\n'),
83
+ score: scoreText(query, [decision.id, decision.title, decision.decision, decision.rationale ?? '', ...(decision.tags ?? [])]),
84
+ ...(projectionOf ? { canonicalEntityId: projectionOf, projectionOf } : {}),
85
+ recordClass: projectionOf ? 'projection' : 'unresolved', authority: 'legacy_decisions'
86
+ };
87
+ });
88
+ const additional = [...relationshipResults, ...decisionResults, ...(options.project ? [] : searchPersonalKg(os, query, limit))]
89
+ .filter((result) => !options.project || Boolean(result.projectionOf));
90
+ const canonicalById = new Map(graphResults.map((result) => [result.id, result]));
91
+ const retained = [];
92
+ for (const result of additional) {
93
+ const canonical = result.projectionOf ? canonicalById.get(result.projectionOf) : undefined;
94
+ if (canonical) {
95
+ canonical.score = Math.max(canonical.score, result.score);
96
+ if (result.source === 'relationships' || result.source === 'decisions') {
97
+ canonical.projectionSources = [...new Set([...(canonical.projectionSources ?? []), result.source])];
98
+ }
99
+ }
100
+ else
101
+ retained.push(result);
102
+ }
103
+ return rank([...graphResults, ...retained], limit);
94
104
  }
95
105
  export function onboardingStatus(os) {
96
106
  const seeded = {
97
107
  self: Boolean(os.graph.owner?.name) || os.personalKg.some((entry) => entry.type === 'self'),
98
108
  work: os.graph.entities.some((entity) => entity.type === 'project') || os.personalKg.some((entry) => entry.type === 'work'),
99
- relationships: os.relationships.relationships.length > 0
109
+ relationships: os.relationships.relationships.length > 0 || (os.graph.version === 2 && os.graph.edges.length > 0)
100
110
  };
101
- const missing = Object.entries(seeded)
102
- .filter(([, value]) => !value)
103
- .map(([key]) => key);
111
+ const missing = Object.entries(seeded).filter(([, value]) => !value).map(([key]) => key);
104
112
  const valueDemoReady = missing.length === 0;
105
- const operationalization = buildOperationalizationPlan({
106
- dataDir: os.dataDir,
107
- firstValueReady: valueDemoReady
108
- });
113
+ const operationalization = buildOperationalizationPlan({ dataDir: os.dataDir, firstValueReady: valueDemoReady });
109
114
  return {
110
- dataDir: os.dataDir,
111
- localBackend: {
112
- connected: true,
113
- backend: 'local'
114
- },
115
- agentMcp: {
116
- status: 'not_verified',
117
- verification: 'MCP設定を反映した新しいエージェントセッションで get_context / search を確認してください。'
118
- },
119
- operationallyReady: false,
120
- seeded,
121
- missing,
122
- valueDemo: {
123
- scope: 'local_cli_sample',
124
- ready: valueDemoReady,
125
- missing,
126
- command: `brainbase onboard:demo --dir ${shellArg(os.dataDir)} --scenario "<real request>"`,
127
- completionSignal: valueDemoReady ? 'cli_sample_ready' : 'needs_seed'
128
- },
115
+ dataDir: os.dataDir, localBackend: { connected: true, backend: 'local' },
116
+ agentMcp: { status: 'not_verified', verification: 'MCP設定を反映した新しいエージェントセッションで resolve_entity / get_context / search を確認してください。' },
117
+ operationallyReady: false, seeded, missing,
118
+ valueDemo: { scope: 'local_cli_sample', ready: valueDemoReady, missing, command: `brainbase onboard:demo --dir ${shellArg(os.dataDir)} --scenario "<real request>"`, completionSignal: valueDemoReady ? 'cli_sample_ready' : 'needs_seed' },
129
119
  operationalization,
130
- counts: {
131
- graphEntities: os.graph.entities.length,
132
- personalKgEntries: os.personalKg.length,
133
- relationships: os.relationships.relationships.length,
134
- decisions: os.decisions.length,
135
- rawSources: os.sourceCount
136
- }
120
+ counts: { graphEntities: os.graph.entities.length, graphEdges: os.graph.version === 2 ? os.graph.edges.length : 0, personalKgEntries: os.personalKg.length, relationships: os.relationships.relationships.length, decisions: os.decisions.length, rawSources: os.sourceCount }
121
+ };
122
+ }
123
+ function scopedRecords(os, options) {
124
+ const asOf = options.asOf ?? new Date().toISOString();
125
+ if (os.graph.version !== 2)
126
+ return { records: [], asOf };
127
+ const graph = os.graph;
128
+ const active = graph.entities.filter((entity) => isActiveAt(entity, asOf));
129
+ const activeIds = new Set(active.map((entity) => entity.id));
130
+ const project = options.project ? resolveProject(active, options.project) : undefined;
131
+ if (options.project && !project)
132
+ return { records: [], asOf };
133
+ if (!project)
134
+ return { records: active.map((entity) => ({ entity, relationPath: [] })), asOf };
135
+ const edges = graph.edges.filter((edge) => isActiveAt(edge, asOf) && activeIds.has(edge.fromId) && activeIds.has(edge.toId));
136
+ return {
137
+ records: active.flatMap((entity) => {
138
+ const relationPath = pathToProject(entity.id, project.id, edges);
139
+ return relationPath === undefined ? [] : [{ entity, relationPath }];
140
+ }), project, asOf
137
141
  };
138
142
  }
139
- function shellArg(value) {
140
- return /^[A-Za-z0-9_./:@%+=,-]+$/.test(value) ? value : JSON.stringify(value);
143
+ function pathToProject(entityId, projectId, edges) {
144
+ if (entityId === projectId)
145
+ return [];
146
+ const queue = [{ id: entityId, path: [] }];
147
+ const visited = new Set([entityId]);
148
+ while (queue.length > 0) {
149
+ const current = queue.shift();
150
+ for (const edge of edges) {
151
+ const relation = getCanonicalRelation(edge.relation);
152
+ if (relation.scopeTraversal === 'none' || relation.traversalDirection === 'none')
153
+ continue;
154
+ const next = relation.traversalDirection === 'forward' ? (edge.fromId === current.id ? edge.toId : undefined) : (edge.toId === current.id ? edge.fromId : undefined);
155
+ if (!next)
156
+ continue;
157
+ const path = [...current.path, edge.id];
158
+ if (next === projectId)
159
+ return path;
160
+ if (relation.scopeTraversal === 'project_transitive' && !visited.has(next)) {
161
+ visited.add(next);
162
+ queue.push({ id: next, path });
163
+ }
164
+ }
165
+ }
166
+ return undefined;
167
+ }
168
+ function resolveProject(entities, value) {
169
+ const needle = normalize(value);
170
+ const matches = entities.filter((entity) => entity.type === 'project' && [entity.id, entity.name, ...(entity.aliases ?? [])].some((candidate) => normalize(candidate) === needle));
171
+ return matches.length === 1 ? matches[0] : undefined;
172
+ }
173
+ function projectionTargets(graph, asOf, visibleIds) {
174
+ const person = new Map();
175
+ const decision = new Map();
176
+ const ambiguous = new Set();
177
+ for (const entity of graph.entities.filter((candidate) => visibleIds.has(candidate.id) && isActiveAt(candidate, asOf))) {
178
+ const target = entity.type === 'person' ? person : entity.type === 'decision' ? decision : undefined;
179
+ if (!target)
180
+ continue;
181
+ for (const value of [entity.id, entity.name, ...(entity.aliases ?? [])]) {
182
+ const key = value === entity.id ? value : normalize(value);
183
+ if (target.has(key) && target.get(key) !== entity.id)
184
+ ambiguous.add(`${entity.type}:${key}`);
185
+ else
186
+ target.set(key, entity.id);
187
+ }
188
+ }
189
+ for (const marker of ambiguous) {
190
+ const [kind, ...parts] = marker.split(':');
191
+ (kind === 'person' ? person : decision).delete(parts.join(':'));
192
+ }
193
+ return { person, decision };
141
194
  }
142
195
  function rank(results, limit) {
143
- return results
144
- .filter((result) => result.score > 0)
145
- .sort((a, b) => b.score - a.score || a.title.localeCompare(b.title))
196
+ const priority = { canonical: 0, projection: 1, unresolved: 2 };
197
+ return results.filter((result) => result.score > 0)
198
+ .sort((a, b) => b.score - a.score || priority[a.recordClass] - priority[b.recordClass] || a.title.localeCompare(b.title))
146
199
  .slice(0, Math.max(1, limit));
147
200
  }
148
201
  function scoreText(query, fields) {
149
- const terms = searchTokens(query);
150
- if (terms.length === 0) {
151
- return 0;
152
- }
153
- const haystack = fields.join('\n').toLowerCase();
154
- const phrase = query.trim().toLowerCase();
155
- const matchedTerms = terms.filter((term) => haystack.includes(term));
156
- if (phrase && haystack.includes(phrase)) {
157
- return terms.length + 20;
202
+ let best = 0;
203
+ for (const variant of queryAliases(query)) {
204
+ const terms = searchTokens(variant);
205
+ if (terms.length === 0)
206
+ continue;
207
+ const haystack = fields.join('\n').normalize('NFKC').toLowerCase();
208
+ const phrase = variant.trim().normalize('NFKC').toLowerCase();
209
+ const matched = terms.filter((term) => haystack.includes(term));
210
+ best = Math.max(best, phrase && haystack.includes(phrase) ? terms.length + 20 : matched.length === terms.length ? terms.length + 10 : matched.length);
158
211
  }
159
- if (matchedTerms.length === terms.length) {
160
- return terms.length + 10;
161
- }
162
- return matchedTerms.length;
212
+ return best;
213
+ }
214
+ function queryAliases(query) {
215
+ const canonical = query.normalize('NFKC').trim();
216
+ const alias = canonical.replace(/(?:さん|様|氏|くん|君|ちゃん)$/u, '').trim();
217
+ return alias && alias !== canonical ? [canonical, alias] : [canonical];
163
218
  }
164
219
  function searchTokens(query) {
165
220
  const seen = new Set();
166
- return query
167
- .normalize('NFKC')
168
- .replace(/[|/\\,;:()[\]{}"'`“”‘’、。・・,.:;()[]{}「」『』【】]/g, ' ')
169
- .replace(/[-_]+/g, ' ')
170
- .toLowerCase()
171
- .split(/\s+/)
172
- .map((term) => term.trim())
173
- .filter(Boolean)
174
- .filter((term) => {
175
- if (seen.has(term)) {
176
- return false;
177
- }
178
- seen.add(term);
179
- return true;
180
- });
181
- }
182
- function metadataText(metadata) {
183
- if (!metadata) {
184
- return '';
185
- }
186
- return JSON.stringify(metadata);
187
- }
188
- function readMetadataString(metadata, key) {
189
- const value = metadata?.[key];
190
- return typeof value === 'string' ? value : undefined;
221
+ return query.normalize('NFKC').replace(/[|/\\,;:()[\]{}"'`“”‘’、。・・,.:;()[]{}「」『』【】]/g, ' ')
222
+ .replace(/[-_]+/g, ' ').toLowerCase().split(/\s+/).map((term) => term.trim()).filter(Boolean)
223
+ .filter((term) => seen.has(term) ? false : (seen.add(term), true));
191
224
  }
225
+ function normalize(value) { return value.normalize('NFKC').replace(/\s+/g, '').toLowerCase(); }
226
+ function metadataText(metadata) { return metadata ? JSON.stringify(metadata) : ''; }
227
+ function readMetadataString(metadata, key) { const value = metadata?.[key]; return typeof value === 'string' ? value : undefined; }
228
+ function shellArg(value) { return /^[A-Za-z0-9_./:@%+=,-]+$/.test(value) ? value : JSON.stringify(value); }