@unson/brainbase-mcp 0.2.3 → 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.
@@ -3,6 +3,7 @@ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
3
3
  import { hostname } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import { planApply } from './import-extract.js';
6
+ import { applyCanonicalWrites } from './canonical-edge-builder.js';
6
7
  import { initializePersonalOs, loadPersonalOs, mutatePersonalOsWithSidecar } from './ssot.js';
7
8
  const LEDGER_SCHEMA_VERSION = 'connected_onboarding.v1';
8
9
  const SHA256 = /^sha256:[a-f0-9]{64}$/;
@@ -136,6 +137,9 @@ export class ConnectedOnboardingRuntime {
136
137
  return withLedgerLock(this.dataDir, async () => {
137
138
  const ledger = await loadLedger(this.dataDir);
138
139
  return mutatePersonalOsWithSidecar(this.dataDir, 'runs/connected-onboarding.json', (os) => {
140
+ if (os.graph.version !== 2) {
141
+ throw new ConnectedOnboardingError('migration_required', 'Graph v1 cannot store canonical ID edges; migrate graph.json to Graph v2 before writing');
142
+ }
139
143
  assertLedgerCanonicalReferences(ledger, os);
140
144
  const run = requireRun(ledger, runId);
141
145
  const prepared = prepareReview(run, actions);
@@ -144,10 +148,12 @@ export class ConnectedOnboardingRuntime {
144
148
  }
145
149
  const result = planApply(prepared.promotions, { ids: new Set(), all: true }, {
146
150
  graphEntities: [...os.graph.entities],
151
+ graphEdges: [...os.graph.edges],
147
152
  relationships: [...os.relationships.relationships],
148
153
  personalKg: os.personalKg,
149
154
  decisions: os.decisions,
150
- ownerName: os.graph.owner?.name
155
+ ownerName: os.graph.owner?.name,
156
+ provenanceSourceKind: 'onboarding'
151
157
  }, this.timestamp());
152
158
  if (result.skipped.length > 0 || result.applied.length !== prepared.promotions.length) {
153
159
  throw new ConnectedOnboardingError('candidate_not_promotable', result.skipped[0]?.reason ?? 'candidate was not promoted');
@@ -165,13 +171,13 @@ export class ConnectedOnboardingRuntime {
165
171
  run.state = advanceState(run.state, 'promotion_reviewed');
166
172
  run.updatedAt = this.timestamp();
167
173
  const sidecarContent = serializeLedger(ledger);
174
+ const graph = applyCanonicalWrites(os.graph, result.canonicalWrites);
168
175
  return {
169
176
  next: {
170
177
  ...os,
171
178
  graph: {
172
- ...os.graph,
173
- owner: result.ownerName ? { ...os.graph.owner, name: result.ownerName } : os.graph.owner,
174
- entities: result.graphEntities
179
+ ...graph,
180
+ owner: result.ownerName ? { ...graph.owner, name: result.ownerName } : graph.owner
175
181
  },
176
182
  relationships: { version: 1, relationships: result.relationships },
177
183
  personalKg: mergeById(os.personalKg, result.personalKgAdditions),
@@ -518,9 +524,8 @@ function isSameTerminalReview(candidate, action, reason) {
518
524
  return action.payload === undefined && action.mergeIntoCandidateId === undefined;
519
525
  }
520
526
  function assertPromotable(candidate) {
521
- const result = planApply([toApplyCandidate(candidate)], { ids: new Set(), all: true }, {
522
- graphEntities: [], relationships: [], personalKg: [], decisions: []
523
- }, '2000-01-01T00:00:00.000Z');
527
+ const validation = candidateValidationContext(candidate, true);
528
+ const result = planApply([validation.candidate], { ids: new Set(), all: true }, validation.base, '2000-01-01T00:00:00.000Z');
524
529
  if (result.skipped.length > 0 || result.applied.length !== 1) {
525
530
  throw new ConnectedOnboardingError('candidate_not_promotable', result.skipped[0]?.reason ?? 'candidate is not promotable');
526
531
  }
@@ -787,14 +792,42 @@ function isValidLedgerCandidate(value, runId, sourceIds) {
787
792
  || value.mergedIntoCandidateId !== undefined
788
793
  || value.promotedCanonicalIds.length === 0)
789
794
  return false;
790
- return stableJson(value.promotedCanonicalIds) === stableJson(expectedCanonicalIds(value));
795
+ return stableJson(value.promotedCanonicalIds) === stableJson(expectedCanonicalIds({
796
+ id: String(value.id),
797
+ kind: String(value.kind),
798
+ payload: value.payload
799
+ }));
791
800
  }
792
801
  function expectedCanonicalIds(candidate) {
793
- const result = planApply([toApplyCandidate(candidate)], { ids: new Set(), all: true }, {
794
- graphEntities: [], relationships: [], personalKg: [], decisions: []
795
- }, '2000-01-01T00:00:00.000Z');
802
+ const validation = candidateValidationContext(candidate, false);
803
+ const result = planApply([validation.candidate], { ids: new Set(), all: true }, validation.base, '2000-01-01T00:00:00.000Z');
796
804
  return result.skipped.length === 0 && result.applied.length === 1 ? result.applied[0].canonicalIds : [];
797
805
  }
806
+ function candidateValidationContext(candidate, fillMissingProject) {
807
+ const payload = { ...candidate.payload };
808
+ const requiresProject = ['person', 'relationship', 'decision'].includes(candidate.kind);
809
+ const explicitProjectId = typeof payload.projectId === 'string' ? payload.projectId.trim() : '';
810
+ const projectId = explicitProjectId || (requiresProject && fillMissingProject ? 'project-validation-only' : '');
811
+ if (projectId && !explicitProjectId)
812
+ payload.projectId = projectId;
813
+ const supersedes = Array.isArray(payload.supersedes)
814
+ ? payload.supersedes.filter((id) => typeof id === 'string' && id.trim().length > 0)
815
+ : [];
816
+ return {
817
+ candidate: { id: candidate.id, kind: candidate.kind, payload },
818
+ base: {
819
+ graphEntities: [
820
+ ...(projectId ? [{ id: projectId, type: 'project', name: projectId }] : []),
821
+ ...supersedes.map((id) => ({ id, type: 'decision', name: id }))
822
+ ],
823
+ graphEdges: [],
824
+ relationships: [],
825
+ personalKg: [],
826
+ decisions: [],
827
+ provenanceSourceKind: 'onboarding'
828
+ }
829
+ };
830
+ }
798
831
  function isValidFirstValueReceipt(value, promoted) {
799
832
  if (value === undefined)
800
833
  return true;
@@ -820,6 +853,7 @@ function isValidFirstValueReview(value, hasReceipt) {
820
853
  function assertCanonicalReferences(run, os) {
821
854
  const canonicalIds = new Set([
822
855
  ...os.graph.entities.map((item) => item.id),
856
+ ...(os.graph.version === 2 ? os.graph.edges.map((item) => item.id) : []),
823
857
  ...os.relationships.relationships.map((item) => item.id),
824
858
  ...os.personalKg.map((item) => item.id),
825
859
  ...os.decisions.map((item) => item.id)
@@ -0,0 +1,111 @@
1
+ import type { CanonicalEntityKind, GraphFileV2 } from './types.js';
2
+ export type SourceStatus = 'complete' | 'partial' | 'unavailable' | 'invalid';
3
+ export type MentionStatus = 'resolved' | 'ambiguous' | 'unresolved';
4
+ export type ResolutionStatus = 'complete' | 'partial' | 'none' | 'blocked';
5
+ export type ScopePolicy = 'strict' | 'prefer_project' | 'allow_global_fallback';
6
+ export interface ResolutionSource {
7
+ authority: 'local_graph';
8
+ status: SourceStatus;
9
+ revision?: string;
10
+ issues?: Array<{
11
+ code: string;
12
+ message: string;
13
+ }>;
14
+ graph?: GraphFileV2;
15
+ }
16
+ export interface ResolveTextInput {
17
+ text: string;
18
+ mentionSpans?: Array<{
19
+ start: number;
20
+ end: number;
21
+ }>;
22
+ projectScope?: {
23
+ projectIds: string[];
24
+ policy?: ScopePolicy;
25
+ };
26
+ asOf: string;
27
+ entityTypes?: CanonicalEntityKind[];
28
+ source: ResolutionSource;
29
+ }
30
+ export interface CandidateEvidence {
31
+ kind: 'name_exact' | 'alias_exact' | 'honorific_variant' | 'project_scope' | 'relation_path' | 'valid_at';
32
+ projectId?: string;
33
+ edgeId?: string;
34
+ edgeIds?: string[];
35
+ asOf?: string;
36
+ }
37
+ export interface ResolutionCandidate {
38
+ entityId: string;
39
+ score: number;
40
+ evidence: CandidateEvidence[];
41
+ }
42
+ export interface MentionResolution {
43
+ span: {
44
+ start: number;
45
+ end: number;
46
+ };
47
+ surface?: string;
48
+ surfaceHash: string;
49
+ normalized: string;
50
+ status: MentionStatus;
51
+ selectedEntityId?: string;
52
+ candidates: ResolutionCandidate[];
53
+ }
54
+ export type PortableMentionResolution = Omit<MentionResolution, 'surface' | 'normalized'>;
55
+ export interface ResolutionReceiptV1 {
56
+ schemaVersion: 1;
57
+ resolverVersion: string;
58
+ graphSchemaVersion: 2 | null;
59
+ ontology: {
60
+ id: string;
61
+ version: string;
62
+ releaseDigest: string;
63
+ } | null;
64
+ request: {
65
+ textSha256: string;
66
+ textLength: number;
67
+ projectScope?: {
68
+ projectIds: string[];
69
+ policy: ScopePolicy;
70
+ };
71
+ asOf: string;
72
+ entityTypes?: CanonicalEntityKind[];
73
+ };
74
+ source: {
75
+ authority: 'local_graph';
76
+ status: SourceStatus;
77
+ revisionSha256?: string;
78
+ issueCodes: string[];
79
+ };
80
+ resolutionStatus: ResolutionStatus;
81
+ mentions: PortableMentionResolution[];
82
+ summary: {
83
+ resolved: number;
84
+ ambiguous: number;
85
+ unresolved: number;
86
+ } | null;
87
+ digest: string;
88
+ }
89
+ export declare const resolverVersion = "1.0.0";
90
+ export declare function resolveText(input: ResolveTextInput): {
91
+ mentions: MentionResolution[];
92
+ receipt: ResolutionReceiptV1;
93
+ };
94
+ export declare function verifyResolutionReceipt(receipt: ResolutionReceiptV1, expected: {
95
+ text: string;
96
+ asOf: string;
97
+ sourceStatus: SourceStatus;
98
+ sourceRevision?: string;
99
+ sourceIssueCodes?: string[];
100
+ graph?: GraphFileV2;
101
+ mentionSpans?: Array<{
102
+ start: number;
103
+ end: number;
104
+ }>;
105
+ projectScope?: {
106
+ projectIds: string[];
107
+ policy?: ScopePolicy;
108
+ };
109
+ entityTypes?: CanonicalEntityKind[];
110
+ }): boolean;
111
+ export declare function normalizeResolverText(value: string): string;
@@ -0,0 +1,381 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { isActiveAt, validateCanonicalGraph } from './canonical-graph.js';
3
+ import { getCanonicalRelation } from './relation-registry.js';
4
+ export const resolverVersion = '1.0.0';
5
+ export function resolveText(input) {
6
+ assertResolveTextInput(input);
7
+ const asOf = input.asOf;
8
+ isActiveAt({}, asOf);
9
+ const projectScope = input.projectScope
10
+ ? { projectIds: [...new Set(input.projectScope.projectIds)].sort(), policy: input.projectScope.policy ?? 'strict' }
11
+ : undefined;
12
+ if (input.source.status === 'unavailable' || input.source.status === 'invalid') {
13
+ return finalize(input, asOf, projectScope, [], 'blocked', null);
14
+ }
15
+ if (!input.source.graph) {
16
+ throw new Error(`RESOLUTION-SOURCE-GRAPH-REQUIRED: source status ${input.source.status} requires a Graph snapshot`);
17
+ }
18
+ validateCanonicalGraph(input.source.graph);
19
+ const graph = input.source.graph;
20
+ const allowedTypes = input.entityTypes ? new Set(input.entityTypes) : undefined;
21
+ const entities = graph.entities
22
+ .filter((entity) => !allowedTypes || allowedTypes.has(entity.type))
23
+ .filter((entity) => isActiveAt(entity, asOf));
24
+ const mentions = collectMentions(input.text, input.mentionSpans ?? [], entities, graph, asOf, projectScope);
25
+ const summary = {
26
+ resolved: mentions.filter((mention) => mention.status === 'resolved').length,
27
+ ambiguous: mentions.filter((mention) => mention.status === 'ambiguous').length,
28
+ unresolved: mentions.filter((mention) => mention.status === 'unresolved').length
29
+ };
30
+ const status = mentions.length === 0 || summary.resolved === 0
31
+ ? 'none'
32
+ : summary.ambiguous + summary.unresolved === 0 && input.source.status === 'complete'
33
+ ? 'complete'
34
+ : 'partial';
35
+ return finalize(input, asOf, projectScope, mentions, status, summary);
36
+ }
37
+ function collectMentions(text, requestedSpans, entities, graph, asOf, scope) {
38
+ const bySpan = new Map();
39
+ for (const span of requestedSpans) {
40
+ if (!Number.isInteger(span.start) || !Number.isInteger(span.end) || span.start < 0 || span.end <= span.start || span.end > text.length) {
41
+ throw new Error(`Invalid mention span: ${span.start}:${span.end}`);
42
+ }
43
+ const surface = text.slice(span.start, span.end);
44
+ bySpan.set(`${span.start}:${span.end}`, {
45
+ ...span,
46
+ surface,
47
+ normalized: normalizeResolverText(surface),
48
+ requested: true,
49
+ matches: []
50
+ });
51
+ }
52
+ for (const entity of entities) {
53
+ for (const alias of [entity.name, ...(entity.aliases ?? [])]) {
54
+ for (const variant of resolverVariants(alias)) {
55
+ for (const span of findAll(text, variant.value)) {
56
+ const key = `${span.start}:${span.end}`;
57
+ const bucket = bySpan.get(key) ?? { ...span, normalized: normalizeResolverText(span.surface), requested: false, matches: [] };
58
+ const evidence = [{ kind: alias === entity.name ? 'name_exact' : 'alias_exact' }];
59
+ if (variant.honorific)
60
+ evidence.push({ kind: 'honorific_variant' });
61
+ bucket.matches.push({ entity, evidence });
62
+ bySpan.set(key, bucket);
63
+ }
64
+ }
65
+ }
66
+ }
67
+ for (const bucket of bySpan.values()) {
68
+ for (const entity of entities) {
69
+ for (const alias of [entity.name, ...(entity.aliases ?? [])]) {
70
+ const variant = resolverVariants(alias).find((item) => item.value === bucket.normalized);
71
+ if (!variant)
72
+ continue;
73
+ const evidence = [{ kind: alias === entity.name ? 'name_exact' : 'alias_exact' }];
74
+ if (variant.honorific)
75
+ evidence.push({ kind: 'honorific_variant' });
76
+ bucket.matches.push({ entity, evidence });
77
+ }
78
+ }
79
+ }
80
+ return [...bySpan.values()]
81
+ .sort((left, right) => left.start - right.start || right.end - left.end)
82
+ .filter((mention, index, all) => mention.requested || !all.some((other, otherIndex) => otherIndex !== index && other.start <= mention.start && other.end >= mention.end && (other.end - other.start) > (mention.end - mention.start)))
83
+ .map((mention) => resolveMention(mention, graph, asOf, scope));
84
+ }
85
+ function resolveMention(mention, graph, asOf, scope) {
86
+ const deduplicated = new Map();
87
+ for (const match of mention.matches) {
88
+ const scopedPaths = scope ? projectPaths(match.entity, graph, asOf, scope.projectIds) : [];
89
+ if (scope?.policy === 'strict' && scopedPaths.length === 0)
90
+ continue;
91
+ const evidence = [...match.evidence];
92
+ for (const path of scopedPaths.sort((left, right) => stableJson(left).localeCompare(stableJson(right)))) {
93
+ evidence.push(path.edgeIds.length === 0
94
+ ? { kind: 'project_scope', projectId: path.projectId }
95
+ : path.edgeIds.length === 1
96
+ ? { kind: 'project_scope', projectId: path.projectId, edgeId: path.edgeIds[0] }
97
+ : { kind: 'relation_path', edgeIds: path.edgeIds });
98
+ }
99
+ evidence.push({ kind: 'valid_at', asOf });
100
+ const stableEvidence = deduplicateAndSortEvidence(evidence);
101
+ const score = match.evidence[0]?.kind === 'name_exact' ? 100 : 95;
102
+ const scopedScore = scopedPaths.length > 0 ? score + 20 : score;
103
+ const existing = deduplicated.get(match.entity.id);
104
+ if (!existing || existing.score < scopedScore)
105
+ deduplicated.set(match.entity.id, { entityId: match.entity.id, score: scopedScore, evidence: stableEvidence });
106
+ }
107
+ let candidates = [...deduplicated.values()].sort((left, right) => right.score - left.score || left.entityId.localeCompare(right.entityId));
108
+ if (scope?.policy === 'allow_global_fallback' && candidates.some((candidate) => candidate.evidence.some(isScopeEvidence))) {
109
+ candidates = candidates.filter((candidate) => candidate.evidence.some(isScopeEvidence));
110
+ }
111
+ const topScore = candidates[0]?.score;
112
+ const top = candidates.filter((candidate) => candidate.score === topScore);
113
+ const status = candidates.length === 0 ? 'unresolved' : top.length === 1 ? 'resolved' : 'ambiguous';
114
+ return {
115
+ span: { start: mention.start, end: mention.end },
116
+ surface: mention.surface,
117
+ surfaceHash: sha256(mention.surface),
118
+ normalized: mention.normalized,
119
+ status,
120
+ ...(status === 'resolved' ? { selectedEntityId: top[0].entityId } : {}),
121
+ candidates
122
+ };
123
+ }
124
+ function projectPaths(entity, graph, asOf, projectIds) {
125
+ const activeEntities = new Set(graph.entities
126
+ .filter((candidate) => isActiveAt(candidate, asOf))
127
+ .map((candidate) => candidate.id));
128
+ const activeProjectIds = new Set(graph.entities
129
+ .filter((candidate) => candidate.type === 'project' && activeEntities.has(candidate.id))
130
+ .map((candidate) => candidate.id));
131
+ const scopedProjectIds = projectIds.filter((projectId) => activeProjectIds.has(projectId));
132
+ if (entity.type === 'project') {
133
+ return scopedProjectIds.includes(entity.id) ? [{ projectId: entity.id, edgeIds: [] }] : [];
134
+ }
135
+ const activeEdges = graph.edges
136
+ .filter((edge) => isActiveAt(edge, asOf) && activeEntities.has(edge.fromId) && activeEntities.has(edge.toId))
137
+ .sort((left, right) => left.id < right.id ? -1 : left.id > right.id ? 1 : 0);
138
+ const queue = [{ entityId: entity.id, edgeIds: [] }];
139
+ const visited = new Set([entity.id]);
140
+ const paths = [];
141
+ while (queue.length > 0) {
142
+ const current = queue.shift();
143
+ for (const edge of activeEdges) {
144
+ const relation = getCanonicalRelation(edge.relation);
145
+ if (relation.scopeTraversal === 'none' || relation.traversalDirection === 'none')
146
+ continue;
147
+ const nextEntityId = relation.traversalDirection === 'forward'
148
+ ? edge.fromId === current.entityId ? edge.toId : undefined
149
+ : edge.toId === current.entityId ? edge.fromId : undefined;
150
+ if (!nextEntityId)
151
+ continue;
152
+ const edgeIds = [...current.edgeIds, edge.id];
153
+ if (scopedProjectIds.includes(nextEntityId)) {
154
+ paths.push({ projectId: nextEntityId, edgeIds });
155
+ }
156
+ if (relation.scopeTraversal === 'project_transitive' && !visited.has(nextEntityId)) {
157
+ visited.add(nextEntityId);
158
+ queue.push({ entityId: nextEntityId, edgeIds });
159
+ }
160
+ }
161
+ }
162
+ return [...new Map(paths.map((path) => [stableJson(path), path])).entries()]
163
+ .sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
164
+ .map(([, path]) => path);
165
+ }
166
+ function isScopeEvidence(evidence) {
167
+ return evidence.kind === 'project_scope' || evidence.kind === 'relation_path';
168
+ }
169
+ function deduplicateAndSortEvidence(evidence) {
170
+ return [...new Map(evidence.map((item) => [stableJson(item), item])).entries()]
171
+ .sort(([left], [right]) => left.localeCompare(right))
172
+ .map(([, item]) => item);
173
+ }
174
+ function finalize(input, asOf, projectScope, mentions, resolutionStatus, summary) {
175
+ const graph = input.source.graph;
176
+ const payload = {
177
+ schemaVersion: 1,
178
+ resolverVersion,
179
+ graphSchemaVersion: graph?.version ?? null,
180
+ ontology: graph?.ontology ?? null,
181
+ request: {
182
+ textSha256: sha256(input.text),
183
+ textLength: input.text.length,
184
+ ...(projectScope ? { projectScope } : {}),
185
+ asOf,
186
+ ...(input.entityTypes ? { entityTypes: [...new Set(input.entityTypes)].sort() } : {})
187
+ },
188
+ source: {
189
+ authority: input.source.authority,
190
+ status: input.source.status,
191
+ ...(input.source.revision ? { revisionSha256: sha256(input.source.revision) } : {}),
192
+ issueCodes: [...new Set((input.source.issues ?? []).map((issue) => issue.code))].sort()
193
+ },
194
+ resolutionStatus,
195
+ mentions: mentions.map(({ surface: _surface, normalized: _normalized, ...mention }) => mention),
196
+ summary
197
+ };
198
+ return { mentions, receipt: { ...payload, digest: sha256(stableJson(payload)) } };
199
+ }
200
+ export function verifyResolutionReceipt(receipt, expected) {
201
+ try {
202
+ const { digest, ...payload } = receipt;
203
+ if (receipt.schemaVersion !== 1 || receipt.resolverVersion !== resolverVersion)
204
+ return false;
205
+ if (sha256(stableJson(payload)) !== digest)
206
+ return false;
207
+ if (receipt.request.textSha256 !== sha256(expected.text) || receipt.request.textLength !== expected.text.length)
208
+ return false;
209
+ if (receipt.request.asOf !== expected.asOf)
210
+ return false;
211
+ if (receipt.source.status !== expected.sourceStatus)
212
+ return false;
213
+ if (receipt.source.revisionSha256 !== (expected.sourceRevision ? sha256(expected.sourceRevision) : undefined))
214
+ return false;
215
+ const sourceBlocked = receipt.source.status === 'unavailable' || receipt.source.status === 'invalid';
216
+ if (sourceBlocked !== (receipt.resolutionStatus === 'blocked'))
217
+ return false;
218
+ if (sourceBlocked) {
219
+ if (receipt.summary !== null || receipt.graphSchemaVersion !== null || receipt.ontology !== null || receipt.mentions.length !== 0)
220
+ return false;
221
+ }
222
+ else if (receipt.graphSchemaVersion !== 2 || !receipt.ontology || receipt.summary === null) {
223
+ return false;
224
+ }
225
+ for (const mention of receipt.mentions) {
226
+ const { start, end } = mention.span;
227
+ if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end <= start || end > expected.text.length)
228
+ return false;
229
+ if (mention.surfaceHash !== sha256(expected.text.slice(start, end)))
230
+ return false;
231
+ const selected = mention.selectedEntityId;
232
+ const candidateIds = new Set(mention.candidates.map((candidate) => candidate.entityId));
233
+ if ((mention.status === 'resolved') !== Boolean(selected))
234
+ return false;
235
+ if (selected && !candidateIds.has(selected))
236
+ return false;
237
+ }
238
+ if (receipt.summary) {
239
+ const computed = {
240
+ resolved: receipt.mentions.filter((mention) => mention.status === 'resolved').length,
241
+ ambiguous: receipt.mentions.filter((mention) => mention.status === 'ambiguous').length,
242
+ unresolved: receipt.mentions.filter((mention) => mention.status === 'unresolved').length
243
+ };
244
+ if (stableJson(computed) !== stableJson(receipt.summary))
245
+ return false;
246
+ const expectedStatus = receipt.mentions.length === 0 || computed.resolved === 0
247
+ ? 'none'
248
+ : computed.ambiguous + computed.unresolved === 0 && receipt.source.status === 'complete'
249
+ ? 'complete'
250
+ : 'partial';
251
+ if (receipt.resolutionStatus !== expectedStatus)
252
+ return false;
253
+ }
254
+ const recomputed = resolveText({
255
+ text: expected.text,
256
+ ...(expected.mentionSpans ? { mentionSpans: expected.mentionSpans } : {}),
257
+ ...(expected.projectScope ? { projectScope: expected.projectScope } : {}),
258
+ asOf: expected.asOf,
259
+ ...(expected.entityTypes ? { entityTypes: expected.entityTypes } : {}),
260
+ source: {
261
+ authority: 'local_graph',
262
+ status: expected.sourceStatus,
263
+ ...(expected.sourceRevision ? { revision: expected.sourceRevision } : {}),
264
+ issues: (expected.sourceIssueCodes ?? []).map((code) => ({ code, message: '' })),
265
+ ...(expected.graph ? { graph: expected.graph } : {})
266
+ }
267
+ }).receipt;
268
+ return recomputed.digest === receipt.digest;
269
+ }
270
+ catch {
271
+ return false;
272
+ }
273
+ }
274
+ function assertResolveTextInput(input) {
275
+ if (!input || typeof input !== 'object' || typeof input.text !== 'string') {
276
+ throw new Error('RESOLUTION-INPUT-TEXT: text must be a string');
277
+ }
278
+ if (!input.source || input.source.authority !== 'local_graph' || !['complete', 'partial', 'unavailable', 'invalid'].includes(input.source.status)) {
279
+ throw new Error('RESOLUTION-INPUT-SOURCE: source authority and status are invalid');
280
+ }
281
+ if ((input.source.status === 'complete' || input.source.status === 'partial') && !input.source.graph) {
282
+ throw new Error(`RESOLUTION-SOURCE-GRAPH-REQUIRED: source status ${input.source.status} requires a Graph snapshot`);
283
+ }
284
+ if ((input.source.status === 'unavailable' || input.source.status === 'invalid') && input.source.graph) {
285
+ throw new Error(`RESOLUTION-SOURCE-GRAPH-FORBIDDEN: source status ${input.source.status} must not carry a trusted Graph snapshot`);
286
+ }
287
+ if (input.source.revision !== undefined && typeof input.source.revision !== 'string') {
288
+ throw new Error('RESOLUTION-INPUT-REVISION: source revision must be a string');
289
+ }
290
+ if (input.source.issues !== undefined && (!Array.isArray(input.source.issues) || input.source.issues.some((issue) => !issue || typeof issue.code !== 'string' || typeof issue.message !== 'string'))) {
291
+ throw new Error('RESOLUTION-INPUT-ISSUES: source issues must contain string code and message');
292
+ }
293
+ if (input.projectScope) {
294
+ if (!Array.isArray(input.projectScope.projectIds) || input.projectScope.projectIds.some((id) => typeof id !== 'string' || id.trim() === '')) {
295
+ throw new Error('RESOLUTION-INPUT-PROJECT-SCOPE: project IDs must be non-empty strings');
296
+ }
297
+ if (input.projectScope.policy !== undefined && !['strict', 'prefer_project', 'allow_global_fallback'].includes(input.projectScope.policy)) {
298
+ throw new Error('RESOLUTION-INPUT-SCOPE-POLICY: unsupported project scope policy');
299
+ }
300
+ }
301
+ if (input.entityTypes !== undefined && (!Array.isArray(input.entityTypes) || input.entityTypes.some((type) => !['person', 'org', 'project', 'decision'].includes(type)))) {
302
+ throw new Error('RESOLUTION-INPUT-ENTITY-TYPES: unsupported canonical entity type');
303
+ }
304
+ }
305
+ export function normalizeResolverText(value) {
306
+ return value.normalize('NFKC').toLocaleLowerCase('ja-JP').replace(/\s+/gu, ' ').trim();
307
+ }
308
+ function resolverVariants(value) {
309
+ const normalized = normalizeResolverText(value);
310
+ const values = new Map([[normalized, false]]);
311
+ for (const suffix of ['さん', '様', '氏'])
312
+ values.set(`${normalized}${suffix}`, true);
313
+ return [...values].map(([variant, honorific]) => ({ value: variant, honorific }));
314
+ }
315
+ function findAll(text, search) {
316
+ const { value: normalizedText, ranges } = normalizeWithRanges(text);
317
+ const result = [];
318
+ let from = 0;
319
+ while (from <= normalizedText.length) {
320
+ const start = normalizedText.indexOf(search, from);
321
+ if (start < 0)
322
+ break;
323
+ const end = start + search.length;
324
+ const sourceStart = ranges[start]?.start;
325
+ const sourceEnd = ranges[end - 1]?.end;
326
+ if (sourceStart !== undefined && sourceEnd !== undefined) {
327
+ result.push({ start: sourceStart, end: sourceEnd, surface: text.slice(sourceStart, sourceEnd) });
328
+ }
329
+ from = start + Math.max(search.length, 1);
330
+ }
331
+ return result;
332
+ }
333
+ function normalizeWithRanges(text) {
334
+ const segments = [...new Intl.Segmenter('ja', { granularity: 'grapheme' }).segment(text)];
335
+ const characters = [];
336
+ const ranges = [];
337
+ let pendingWhitespaceStart;
338
+ let pendingWhitespaceEnd;
339
+ for (const segment of segments) {
340
+ const normalized = segment.segment.normalize('NFKC').toLocaleLowerCase('ja-JP');
341
+ const end = segment.index + segment.segment.length;
342
+ if (/^\s+$/u.test(normalized)) {
343
+ if (characters.length > 0) {
344
+ pendingWhitespaceStart ??= segment.index;
345
+ pendingWhitespaceEnd = end;
346
+ }
347
+ continue;
348
+ }
349
+ if (pendingWhitespaceStart !== undefined) {
350
+ characters.push(' ');
351
+ ranges.push({ start: pendingWhitespaceStart, end: pendingWhitespaceEnd });
352
+ pendingWhitespaceStart = undefined;
353
+ pendingWhitespaceEnd = undefined;
354
+ }
355
+ for (const character of normalized) {
356
+ characters.push(character);
357
+ for (let index = 0; index < character.length; index += 1) {
358
+ if (index > 0)
359
+ characters.push('');
360
+ ranges.push({ start: segment.index, end });
361
+ }
362
+ }
363
+ }
364
+ return { value: characters.join(''), ranges };
365
+ }
366
+ function stableJson(value) {
367
+ return JSON.stringify(canonicalize(value));
368
+ }
369
+ function canonicalize(value) {
370
+ if (Array.isArray(value))
371
+ return value.map(canonicalize);
372
+ if (value && typeof value === 'object') {
373
+ return Object.fromEntries(Object.entries(value)
374
+ .sort(([left], [right]) => left.localeCompare(right))
375
+ .map(([key, item]) => [key, canonicalize(item)]));
376
+ }
377
+ return value;
378
+ }
379
+ function sha256(value) {
380
+ return createHash('sha256').update(value).digest('hex');
381
+ }
@@ -0,0 +1,24 @@
1
+ export interface GraphDiagnosis {
2
+ status: 'healthy' | 'issues' | 'migration_required' | 'invalid' | 'unavailable';
3
+ schemaVersion: number | null;
4
+ ontology: unknown;
5
+ asOf: string;
6
+ migrationRequired: boolean;
7
+ counts: {
8
+ entities: number;
9
+ edges: number;
10
+ activeEdges: number;
11
+ danglingEdges: number;
12
+ invalidEdges: number;
13
+ duplicateEdges: number;
14
+ duplicateEntities: number;
15
+ unresolvedRecords: number;
16
+ projections: number;
17
+ };
18
+ issues: Array<{
19
+ class: 'dangling' | 'invalid' | 'duplicate' | 'unresolved' | 'projection' | 'migration' | 'unavailable';
20
+ recordId?: string;
21
+ detail: string;
22
+ }>;
23
+ }
24
+ export declare function diagnoseGraph(dataDir: string, asOf?: string): Promise<GraphDiagnosis>;