@unson/brainbase-mcp 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +146 -471
- package/contracts/judgment-dag/digest.json +4 -4
- package/contracts/judgment-dag/source-lock.json +65 -1
- package/contracts/judgment-value-proof/fixture.json +54 -0
- package/contracts/judgment-value-proof/schema.json +163 -0
- package/contracts/public-message-candidate.schema.json +143 -0
- package/dist/cli.js +46 -6
- package/dist/embedding-provider.d.ts +72 -0
- package/dist/embedding-provider.js +391 -0
- package/dist/graph-retrieval.d.ts +94 -0
- package/dist/graph-retrieval.js +443 -0
- package/dist/judgment-autonomy.d.ts +62 -0
- package/dist/judgment-autonomy.js +301 -0
- package/dist/judgment-dag-artifact-store.d.ts +29 -0
- package/dist/judgment-dag-artifact-store.js +416 -0
- package/dist/judgment-dag-replay-evaluation.d.ts +108 -0
- package/dist/judgment-dag-replay-evaluation.js +424 -0
- package/dist/judgment-dag-runner.d.ts +70 -0
- package/dist/judgment-dag-runner.js +325 -0
- package/dist/judgment-dag.d.ts +6 -0
- package/dist/judgment-dag.js +3 -0
- package/dist/judgment-host.d.ts +21 -0
- package/dist/judgment-host.js +180 -0
- package/dist/judgment-value-proof.d.ts +92 -0
- package/dist/judgment-value-proof.js +229 -0
- package/dist/organization-graph.d.ts +76 -0
- package/dist/organization-graph.js +331 -0
- package/dist/portable-graph.d.ts +39 -0
- package/dist/portable-graph.js +240 -0
- package/dist/server.d.ts +42 -3
- package/dist/server.js +52 -7
- package/docs/management/judgment-dag-milestones.md +31 -1
- package/package.json +20 -4
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import { isActiveAt, validateCanonicalGraph } from './canonical-graph.js';
|
|
2
|
+
import { getCanonicalRelation } from './relation-registry.js';
|
|
3
|
+
/** The deliberately small bounds keep a model-supplied relation plan finite. */
|
|
4
|
+
export const GRAPH_RETRIEVAL_MAX_SEEDS = 10;
|
|
5
|
+
export const GRAPH_RETRIEVAL_MAX_STEPS = 3;
|
|
6
|
+
export const GRAPH_RETRIEVAL_MAX_LIMIT = 50;
|
|
7
|
+
const canonicalEntityKinds = new Set(['person', 'org', 'project', 'decision']);
|
|
8
|
+
/**
|
|
9
|
+
* Retrieve canonical Graph v2 records in three explicit phases:
|
|
10
|
+
* candidate discovery, bounded typed edge traversal, and evidence reporting.
|
|
11
|
+
*
|
|
12
|
+
* A provider is optional by design. Without one this function performs only
|
|
13
|
+
* deterministic lexical discovery and marks that fact in the receipt; it does
|
|
14
|
+
* not present lexical matches as semantic understanding.
|
|
15
|
+
*/
|
|
16
|
+
export async function retrieveGraph(os, input, provider) {
|
|
17
|
+
const normalized = normalizeInput(input);
|
|
18
|
+
validateCanonicalGraph(os.graph);
|
|
19
|
+
if (os.graph.version === 1)
|
|
20
|
+
return migrationResponse(normalized);
|
|
21
|
+
const graph = os.graph;
|
|
22
|
+
const activeEntities = graph.entities.filter((entity) => isActiveAt(entity, normalized.asOf));
|
|
23
|
+
const activeIds = new Set(activeEntities.map((entity) => entity.id));
|
|
24
|
+
const project = resolveProject(activeEntities, normalized.project);
|
|
25
|
+
const partialReasons = [...normalized.partialReasons];
|
|
26
|
+
if (normalized.project && !project) {
|
|
27
|
+
partialReasons.push('project_scope_not_found_or_inactive');
|
|
28
|
+
}
|
|
29
|
+
const scoped = project ? scopeToProject(graph, activeEntities, activeIds, project, normalized.asOf) : normalized.project ? [] : activeEntities.map((entity) => ({ entity, scopePath: [] }));
|
|
30
|
+
const byId = new Map(scoped.map((item) => [item.entity.id, item]));
|
|
31
|
+
const requestedSeeds = normalized.seedIds;
|
|
32
|
+
const validRequestedSeeds = requestedSeeds.filter((id) => byId.has(id));
|
|
33
|
+
if (requestedSeeds.length > validRequestedSeeds.length) {
|
|
34
|
+
partialReasons.push('seed_ids_out_of_scope_or_inactive');
|
|
35
|
+
}
|
|
36
|
+
let method;
|
|
37
|
+
let initial;
|
|
38
|
+
let providerId;
|
|
39
|
+
if (requestedSeeds.length > 0) {
|
|
40
|
+
method = 'seed';
|
|
41
|
+
initial = validRequestedSeeds.map((id) => {
|
|
42
|
+
const scopedEntity = byId.get(id);
|
|
43
|
+
return {
|
|
44
|
+
entity: scopedEntity.entity,
|
|
45
|
+
score: 1,
|
|
46
|
+
discovery: 'seed',
|
|
47
|
+
relationPath: [...scopedEntity.scopePath]
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
else if (provider) {
|
|
52
|
+
method = 'semantic';
|
|
53
|
+
providerId = assertProviderId(provider);
|
|
54
|
+
initial = await discoverSemantic(scoped, normalized.query, normalized.limit, provider);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
method = 'lexical';
|
|
58
|
+
initial = discoverLexical(scoped, normalized.query, normalized.limit);
|
|
59
|
+
partialReasons.push('semantic_provider_unavailable');
|
|
60
|
+
}
|
|
61
|
+
const traversalSeedLimit = normalized.steps.length > 0
|
|
62
|
+
? Math.min(GRAPH_RETRIEVAL_MAX_SEEDS, normalized.limit)
|
|
63
|
+
: normalized.limit;
|
|
64
|
+
const traversalSeeds = initial.slice(0, traversalSeedLimit).map((seed) => normalized.steps.length > 0
|
|
65
|
+
? { ...seed, relationPath: [] }
|
|
66
|
+
: seed);
|
|
67
|
+
if (initial.length > traversalSeeds.length) {
|
|
68
|
+
partialReasons.push(normalized.steps.length > 0 ? 'seed_candidates_truncated_max_10' : 'result_limit_truncated');
|
|
69
|
+
}
|
|
70
|
+
const observedRelations = observeRelations(graph, traversalSeeds.map((candidate) => candidate.entity.id), new Set(byId.keys()), normalized.asOf);
|
|
71
|
+
const traversed = normalized.steps.length > 0
|
|
72
|
+
? traverse(graph, byId, traversalSeeds, normalized.steps, activeIds, normalized.asOf, partialReasons)
|
|
73
|
+
: traversalSeeds;
|
|
74
|
+
const ranked = rankStates(traversed).slice(0, normalized.limit);
|
|
75
|
+
if (traversed.length > ranked.length)
|
|
76
|
+
partialReasons.push('result_limit_truncated');
|
|
77
|
+
const candidates = ranked.map((state) => toCandidate(state, evidenceFor(os, state.entity)));
|
|
78
|
+
const evidence = candidates.flatMap((candidate) => candidate.evidence);
|
|
79
|
+
const missingEvidence = missingEvidenceFor(candidates);
|
|
80
|
+
if (missingEvidence.length > 0)
|
|
81
|
+
partialReasons.push('missing_evidence');
|
|
82
|
+
const sufficiency = evidence.length > 0 ? 'needs_model_verification' : 'insufficient';
|
|
83
|
+
const coverage = partialReasons.length > 0 ? 'partial' : 'complete';
|
|
84
|
+
const observedRelationCatalog = observedRelations;
|
|
85
|
+
return {
|
|
86
|
+
graphVersion: 2,
|
|
87
|
+
schemaVersion: 2,
|
|
88
|
+
status: 'ok',
|
|
89
|
+
migrationRequired: false,
|
|
90
|
+
authority: 'local_graph',
|
|
91
|
+
query: normalized.query,
|
|
92
|
+
asOf: normalized.asOf,
|
|
93
|
+
...(project ? { project: { id: project.id, name: project.name } } : {}),
|
|
94
|
+
method,
|
|
95
|
+
semantic: {
|
|
96
|
+
available: method === 'semantic',
|
|
97
|
+
method,
|
|
98
|
+
...(providerId ? { providerId } : {})
|
|
99
|
+
},
|
|
100
|
+
candidates,
|
|
101
|
+
results: candidates,
|
|
102
|
+
observedRelations,
|
|
103
|
+
observedRelationCatalog,
|
|
104
|
+
traversal: {
|
|
105
|
+
seedIds: traversalSeeds.map((candidate) => candidate.entity.id),
|
|
106
|
+
steps: normalized.steps,
|
|
107
|
+
maxSeeds: GRAPH_RETRIEVAL_MAX_SEEDS,
|
|
108
|
+
maxSteps: GRAPH_RETRIEVAL_MAX_STEPS,
|
|
109
|
+
maxLimit: GRAPH_RETRIEVAL_MAX_LIMIT
|
|
110
|
+
},
|
|
111
|
+
coverage,
|
|
112
|
+
partialReasons: unique(partialReasons),
|
|
113
|
+
missingEvidence,
|
|
114
|
+
evidence,
|
|
115
|
+
sufficiency,
|
|
116
|
+
absenceConfirmed: false
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function normalizeInput(input) {
|
|
120
|
+
if (!input || typeof input !== 'object')
|
|
121
|
+
throw new Error('GRAPH-RETRIEVAL-INPUT: input must be an object');
|
|
122
|
+
if (typeof input.query !== 'string' || input.query.trim() === '')
|
|
123
|
+
throw new Error('GRAPH-RETRIEVAL-QUERY: query must be a non-empty string');
|
|
124
|
+
const asOf = input.asOf ?? new Date().toISOString();
|
|
125
|
+
isActiveAt({}, asOf);
|
|
126
|
+
const partialReasons = [];
|
|
127
|
+
const requestedLimit = input.limit ?? 10;
|
|
128
|
+
if (!Number.isInteger(requestedLimit) || requestedLimit < 1)
|
|
129
|
+
throw new Error('GRAPH-RETRIEVAL-LIMIT: limit must be a positive integer');
|
|
130
|
+
const limit = Math.min(requestedLimit, GRAPH_RETRIEVAL_MAX_LIMIT);
|
|
131
|
+
if (requestedLimit > GRAPH_RETRIEVAL_MAX_LIMIT)
|
|
132
|
+
partialReasons.push('limit_capped_max_50');
|
|
133
|
+
if (input.seedIds !== undefined && (!Array.isArray(input.seedIds) || input.seedIds.some((id) => typeof id !== 'string' || id.trim() === ''))) {
|
|
134
|
+
throw new Error('GRAPH-RETRIEVAL-SEEDS: seedIds must be an array of non-empty strings');
|
|
135
|
+
}
|
|
136
|
+
const allSeeds = unique((input.seedIds ?? []).map((id) => id.trim()));
|
|
137
|
+
const seedIds = allSeeds.slice(0, GRAPH_RETRIEVAL_MAX_SEEDS);
|
|
138
|
+
if (allSeeds.length > seedIds.length)
|
|
139
|
+
partialReasons.push('seed_ids_capped_max_10');
|
|
140
|
+
if (input.steps !== undefined && !Array.isArray(input.steps))
|
|
141
|
+
throw new Error('GRAPH-RETRIEVAL-STEPS: steps must be an array');
|
|
142
|
+
const allSteps = (input.steps ?? []).map((step, index) => normalizeStep(step, index));
|
|
143
|
+
const steps = allSteps.slice(0, GRAPH_RETRIEVAL_MAX_STEPS);
|
|
144
|
+
if (allSteps.length > steps.length)
|
|
145
|
+
partialReasons.push('steps_capped_max_3');
|
|
146
|
+
return { ...input, query: input.query.trim(), asOf, limit, seedIds, steps, partialReasons };
|
|
147
|
+
}
|
|
148
|
+
function normalizeStep(step, index) {
|
|
149
|
+
if (!step || typeof step !== 'object')
|
|
150
|
+
throw new Error(`GRAPH-RETRIEVAL-STEP-${index}: step must be an object`);
|
|
151
|
+
try {
|
|
152
|
+
getCanonicalRelation(step.relation);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
throw new Error(`GRAPH-RETRIEVAL-STEP-${index}: ${error instanceof Error ? error.message : String(error)}`);
|
|
156
|
+
}
|
|
157
|
+
if (step.direction !== 'incoming' && step.direction !== 'outgoing') {
|
|
158
|
+
throw new Error(`GRAPH-RETRIEVAL-STEP-${index}: direction must be incoming or outgoing`);
|
|
159
|
+
}
|
|
160
|
+
if (step.targetType !== undefined && !canonicalEntityKinds.has(step.targetType)) {
|
|
161
|
+
throw new Error(`GRAPH-RETRIEVAL-STEP-${index}: targetType is not a canonical entity kind`);
|
|
162
|
+
}
|
|
163
|
+
return { relation: step.relation, direction: step.direction, ...(step.targetType ? { targetType: step.targetType } : {}) };
|
|
164
|
+
}
|
|
165
|
+
function assertProviderId(provider) {
|
|
166
|
+
if (typeof provider.id !== 'string' || provider.id.trim() === '')
|
|
167
|
+
throw new Error('GRAPH-RETRIEVAL-EMBEDDING-PROVIDER: provider.id must be a non-empty string');
|
|
168
|
+
return provider.id;
|
|
169
|
+
}
|
|
170
|
+
async function discoverSemantic(scoped, query, _limit, provider) {
|
|
171
|
+
const texts = scoped.map(({ entity }) => entityEmbeddingText(entity));
|
|
172
|
+
const vectors = await provider.embed([...texts, query]);
|
|
173
|
+
if (!Array.isArray(vectors) || vectors.length !== texts.length + 1) {
|
|
174
|
+
throw new Error(`GRAPH-RETRIEVAL-EMBEDDING-COUNT: provider ${provider.id} returned ${Array.isArray(vectors) ? vectors.length : 'non-array'} vectors for ${texts.length + 1} texts`);
|
|
175
|
+
}
|
|
176
|
+
const queryVector = validateVector(vectors[vectors.length - 1], -1, provider.id);
|
|
177
|
+
const scored = scoped.map((item, index) => ({
|
|
178
|
+
entity: item.entity,
|
|
179
|
+
score: cosine(queryVector, validateVector(vectors[index], index, provider.id)),
|
|
180
|
+
discovery: 'semantic',
|
|
181
|
+
relationPath: [...item.scopePath]
|
|
182
|
+
}));
|
|
183
|
+
return scored.sort(compareStates);
|
|
184
|
+
}
|
|
185
|
+
function validateVector(value, index, providerId) {
|
|
186
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
187
|
+
throw new Error(`GRAPH-RETRIEVAL-EMBEDDING-VECTOR: provider ${providerId} returned an empty vector at index ${index}`);
|
|
188
|
+
if (value.some((component) => typeof component !== 'number' || !Number.isFinite(component))) {
|
|
189
|
+
throw new Error(`GRAPH-RETRIEVAL-EMBEDDING-FINITE: provider ${providerId} returned a non-finite vector at index ${index}`);
|
|
190
|
+
}
|
|
191
|
+
if (Math.hypot(...value) === 0)
|
|
192
|
+
throw new Error(`GRAPH-RETRIEVAL-EMBEDDING-ZERO: provider ${providerId} returned a zero vector at index ${index}`);
|
|
193
|
+
return value;
|
|
194
|
+
}
|
|
195
|
+
function cosine(left, right) {
|
|
196
|
+
if (left.length !== right.length)
|
|
197
|
+
throw new Error(`GRAPH-RETRIEVAL-EMBEDDING-DIMENSION: vectors have dimensions ${left.length} and ${right.length}`);
|
|
198
|
+
let dot = 0;
|
|
199
|
+
let leftNorm = 0;
|
|
200
|
+
let rightNorm = 0;
|
|
201
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
202
|
+
dot += left[index] * right[index];
|
|
203
|
+
leftNorm += left[index] ** 2;
|
|
204
|
+
rightNorm += right[index] ** 2;
|
|
205
|
+
}
|
|
206
|
+
const denominator = Math.sqrt(leftNorm) * Math.sqrt(rightNorm);
|
|
207
|
+
if (!Number.isFinite(denominator) || denominator === 0)
|
|
208
|
+
throw new Error('GRAPH-RETRIEVAL-EMBEDDING-COSINE: cannot calculate cosine similarity');
|
|
209
|
+
return dot / denominator;
|
|
210
|
+
}
|
|
211
|
+
function discoverLexical(scoped, query, _limit) {
|
|
212
|
+
return scoped
|
|
213
|
+
.map((item) => ({
|
|
214
|
+
entity: item.entity,
|
|
215
|
+
score: lexicalScore(query, entitySearchFields(item.entity)),
|
|
216
|
+
discovery: 'lexical',
|
|
217
|
+
relationPath: [...item.scopePath]
|
|
218
|
+
}))
|
|
219
|
+
.filter((candidate) => candidate.score > 0)
|
|
220
|
+
.sort(compareStates);
|
|
221
|
+
}
|
|
222
|
+
function traverse(graph, scoped, seeds, steps, activeIds, asOf, partialReasons) {
|
|
223
|
+
let frontier = seeds;
|
|
224
|
+
for (const [stepIndex, step] of steps.entries()) {
|
|
225
|
+
const next = new Map();
|
|
226
|
+
for (const current of frontier) {
|
|
227
|
+
for (const edge of graph.edges) {
|
|
228
|
+
const targetId = edgeTarget(edge, step, current.entity.id);
|
|
229
|
+
if (!targetId || !activeIds.has(edge.fromId) || !activeIds.has(edge.toId) || !isActiveAt(edge, asOf))
|
|
230
|
+
continue;
|
|
231
|
+
const target = scoped.get(targetId);
|
|
232
|
+
if (!target)
|
|
233
|
+
continue;
|
|
234
|
+
const state = {
|
|
235
|
+
entity: target.entity,
|
|
236
|
+
score: current.score * 0.9,
|
|
237
|
+
discovery: 'traversal',
|
|
238
|
+
relationPath: [...current.relationPath, edge.id]
|
|
239
|
+
};
|
|
240
|
+
const prior = next.get(targetId);
|
|
241
|
+
if (!prior || compareStates(state, prior) < 0)
|
|
242
|
+
next.set(targetId, state);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (next.size === 0)
|
|
246
|
+
partialReasons.push(`no_matching_edge_for_step_${stepIndex + 1}`);
|
|
247
|
+
frontier = [...next.values()];
|
|
248
|
+
if (frontier.length === 0)
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
return frontier;
|
|
252
|
+
}
|
|
253
|
+
function edgeTarget(edge, step, currentId) {
|
|
254
|
+
if (edge.relation !== step.relation)
|
|
255
|
+
return undefined;
|
|
256
|
+
const relation = getCanonicalRelation(edge.relation);
|
|
257
|
+
const targetId = step.direction === 'outgoing'
|
|
258
|
+
? edge.fromId === currentId ? edge.toId : undefined
|
|
259
|
+
: edge.toId === currentId ? edge.fromId : undefined;
|
|
260
|
+
if (!targetId)
|
|
261
|
+
return undefined;
|
|
262
|
+
const currentType = step.direction === 'outgoing' ? relation.from : relation.to;
|
|
263
|
+
const targetType = step.direction === 'outgoing' ? relation.to : relation.from;
|
|
264
|
+
// The graph validator already checks this, but keep the traversal rail local
|
|
265
|
+
// so a future graph source cannot turn an untyped edge into a result.
|
|
266
|
+
if (step.targetType && step.targetType !== targetType)
|
|
267
|
+
return undefined;
|
|
268
|
+
return currentType && targetType ? targetId : undefined;
|
|
269
|
+
}
|
|
270
|
+
function observeRelations(graph, seedIds, visibleIds, asOf) {
|
|
271
|
+
const observed = new Map();
|
|
272
|
+
const seedSet = new Set(seedIds);
|
|
273
|
+
for (const edge of graph.edges) {
|
|
274
|
+
if (!visibleIds.has(edge.fromId) || !visibleIds.has(edge.toId) || !isActiveAt(edge, asOf))
|
|
275
|
+
continue;
|
|
276
|
+
const relation = getCanonicalRelation(edge.relation);
|
|
277
|
+
const observations = [];
|
|
278
|
+
if (seedSet.has(edge.fromId))
|
|
279
|
+
observations.push({ direction: 'outgoing', targetId: edge.toId, targetType: relation.to });
|
|
280
|
+
if (seedSet.has(edge.toId))
|
|
281
|
+
observations.push({ direction: 'incoming', targetId: edge.fromId, targetType: relation.from });
|
|
282
|
+
for (const observation of observations) {
|
|
283
|
+
const key = `${edge.relation}:${observation.direction}:${observation.targetType}`;
|
|
284
|
+
const existing = observed.get(key) ?? {
|
|
285
|
+
relation: edge.relation,
|
|
286
|
+
meaning: relation.meaning,
|
|
287
|
+
direction: observation.direction,
|
|
288
|
+
targetType: observation.targetType,
|
|
289
|
+
edgeIds: [],
|
|
290
|
+
targetIds: []
|
|
291
|
+
};
|
|
292
|
+
if (!existing.edgeIds.includes(edge.id))
|
|
293
|
+
existing.edgeIds.push(edge.id);
|
|
294
|
+
if (!existing.targetIds.includes(observation.targetId))
|
|
295
|
+
existing.targetIds.push(observation.targetId);
|
|
296
|
+
observed.set(key, existing);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return [...observed.values()].sort((left, right) => `${left.relation}:${left.direction}:${left.targetType}`.localeCompare(`${right.relation}:${right.direction}:${right.targetType}`));
|
|
300
|
+
}
|
|
301
|
+
function toCandidate(state, bundle) {
|
|
302
|
+
const entity = state.entity;
|
|
303
|
+
return {
|
|
304
|
+
source: 'graph',
|
|
305
|
+
id: entity.id,
|
|
306
|
+
title: entity.name,
|
|
307
|
+
text: entitySearchText(entity),
|
|
308
|
+
score: state.score,
|
|
309
|
+
canonicalEntityId: entity.id,
|
|
310
|
+
recordClass: 'canonical',
|
|
311
|
+
authority: 'local_graph',
|
|
312
|
+
relationPath: [...state.relationPath],
|
|
313
|
+
discovery: state.discovery,
|
|
314
|
+
evidence: bundle.evidence,
|
|
315
|
+
...(bundle.rationale ? { decisionRationale: bundle.rationale } : {})
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function evidenceFor(os, entity) {
|
|
319
|
+
if (entity.type !== 'decision')
|
|
320
|
+
return { evidence: [] };
|
|
321
|
+
// Deliberately compare canonical IDs only. A matching title/name is not proof
|
|
322
|
+
// that a legacy decision belongs to this Graph node.
|
|
323
|
+
const decision = os.decisions.find((candidate) => candidate.id === entity.id);
|
|
324
|
+
if (!decision)
|
|
325
|
+
return { evidence: [] };
|
|
326
|
+
const statement = [decision.decision, decision.rationale].filter((value) => Boolean(value?.trim())).join('\n');
|
|
327
|
+
if (!statement)
|
|
328
|
+
return { evidence: [] };
|
|
329
|
+
const rationale = {
|
|
330
|
+
decisionId: decision.id,
|
|
331
|
+
title: decision.title,
|
|
332
|
+
decision: decision.decision,
|
|
333
|
+
...(decision.rationale ? { rationale: decision.rationale } : {})
|
|
334
|
+
};
|
|
335
|
+
return {
|
|
336
|
+
evidence: [{ source: 'legacy_decisions', sourceId: decision.id, canonicalEntityId: entity.id, statement }],
|
|
337
|
+
rationale
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function missingEvidenceFor(candidates) {
|
|
341
|
+
if (candidates.length === 0)
|
|
342
|
+
return ['no_candidate_evidence'];
|
|
343
|
+
return candidates.filter((candidate) => candidate.evidence.length === 0).map((candidate) => `missing_evidence:${candidate.id}`);
|
|
344
|
+
}
|
|
345
|
+
function scopeToProject(graph, activeEntities, activeIds, project, asOf) {
|
|
346
|
+
const activeEdges = graph.edges.filter((edge) => activeIds.has(edge.fromId) && activeIds.has(edge.toId) && isActiveAt(edge, asOf));
|
|
347
|
+
return activeEntities.flatMap((entity) => {
|
|
348
|
+
const scopePath = pathToProject(entity.id, project.id, activeEdges);
|
|
349
|
+
return scopePath === undefined ? [] : [{ entity, scopePath }];
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
function pathToProject(entityId, projectId, edges) {
|
|
353
|
+
if (entityId === projectId)
|
|
354
|
+
return [];
|
|
355
|
+
const queue = [{ id: entityId, path: [] }];
|
|
356
|
+
const visited = new Set([entityId]);
|
|
357
|
+
while (queue.length > 0) {
|
|
358
|
+
const current = queue.shift();
|
|
359
|
+
for (const edge of edges) {
|
|
360
|
+
const relation = getCanonicalRelation(edge.relation);
|
|
361
|
+
if (relation.scopeTraversal === 'none' || relation.traversalDirection === 'none')
|
|
362
|
+
continue;
|
|
363
|
+
const next = relation.traversalDirection === 'forward'
|
|
364
|
+
? edge.fromId === current.id ? edge.toId : undefined
|
|
365
|
+
: edge.toId === current.id ? edge.fromId : undefined;
|
|
366
|
+
if (!next)
|
|
367
|
+
continue;
|
|
368
|
+
const path = [...current.path, edge.id];
|
|
369
|
+
if (next === projectId)
|
|
370
|
+
return path;
|
|
371
|
+
if (relation.scopeTraversal === 'project_transitive' && !visited.has(next)) {
|
|
372
|
+
visited.add(next);
|
|
373
|
+
queue.push({ id: next, path });
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return undefined;
|
|
378
|
+
}
|
|
379
|
+
function resolveProject(entities, requested) {
|
|
380
|
+
if (!requested)
|
|
381
|
+
return undefined;
|
|
382
|
+
const needle = normalize(requested);
|
|
383
|
+
const matches = entities.filter((entity) => entity.type === 'project' && [entity.id, entity.name, ...(entity.aliases ?? [])].some((value) => normalize(value) === needle));
|
|
384
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
385
|
+
}
|
|
386
|
+
function entitySearchFields(entity) {
|
|
387
|
+
return [entity.id, entity.type, entity.name, ...(entity.aliases ?? []), entity.summary ?? '', ...(entity.tags ?? []), metadataText(entity.metadata)];
|
|
388
|
+
}
|
|
389
|
+
function entityEmbeddingText(entity) { return entitySearchFields(entity).filter(Boolean).join('\n'); }
|
|
390
|
+
function entitySearchText(entity) { return [entity.summary ?? entity.name, metadataText(entity.metadata)].filter(Boolean).join('\n'); }
|
|
391
|
+
function metadataText(metadata) { return metadata ? JSON.stringify(metadata) : ''; }
|
|
392
|
+
function lexicalScore(query, fields) {
|
|
393
|
+
let best = 0;
|
|
394
|
+
for (const variant of queryAliases(query)) {
|
|
395
|
+
const terms = searchTokens(variant);
|
|
396
|
+
if (terms.length === 0)
|
|
397
|
+
continue;
|
|
398
|
+
const haystack = fields.join('\n').normalize('NFKC').toLowerCase();
|
|
399
|
+
const phrase = variant.trim().normalize('NFKC').toLowerCase();
|
|
400
|
+
const matched = terms.filter((term) => haystack.includes(term));
|
|
401
|
+
best = Math.max(best, phrase && haystack.includes(phrase) ? terms.length + 20 : matched.length === terms.length ? terms.length + 10 : matched.length);
|
|
402
|
+
}
|
|
403
|
+
return best;
|
|
404
|
+
}
|
|
405
|
+
function queryAliases(query) {
|
|
406
|
+
const canonical = query.normalize('NFKC').trim();
|
|
407
|
+
const alias = canonical.replace(/(?:さん|様|氏|くん|君|ちゃん)$/u, '').trim();
|
|
408
|
+
return alias && alias !== canonical ? [canonical, alias] : [canonical];
|
|
409
|
+
}
|
|
410
|
+
function searchTokens(query) {
|
|
411
|
+
const seen = new Set();
|
|
412
|
+
return query.normalize('NFKC').replace(/[|/\\,;:()[\]{}"'`“”‘’、。・・,.:;()[]{}「」『』【】]/g, ' ').replace(/[-_]+/g, ' ').toLowerCase().split(/\s+/).map((term) => term.trim()).filter(Boolean).filter((term) => seen.has(term) ? false : (seen.add(term), true));
|
|
413
|
+
}
|
|
414
|
+
function normalize(value) { return value.normalize('NFKC').replace(/\s+/g, '').toLowerCase(); }
|
|
415
|
+
function rankStates(states) { return [...states].sort(compareStates); }
|
|
416
|
+
function compareStates(left, right) { return right.score - left.score || left.entity.id.localeCompare(right.entity.id); }
|
|
417
|
+
function unique(values) { return [...new Set(values)]; }
|
|
418
|
+
function migrationResponse(input) {
|
|
419
|
+
const partialReasons = unique([...input.partialReasons, 'graph_v1_migration_required']);
|
|
420
|
+
const empty = [];
|
|
421
|
+
return {
|
|
422
|
+
graphVersion: 1,
|
|
423
|
+
schemaVersion: 1,
|
|
424
|
+
status: 'migration_required',
|
|
425
|
+
migrationRequired: true,
|
|
426
|
+
authority: 'local_graph',
|
|
427
|
+
query: input.query,
|
|
428
|
+
asOf: input.asOf,
|
|
429
|
+
method: 'seed',
|
|
430
|
+
semantic: { available: false, method: 'seed' },
|
|
431
|
+
candidates: empty,
|
|
432
|
+
results: empty,
|
|
433
|
+
observedRelations: [],
|
|
434
|
+
observedRelationCatalog: [],
|
|
435
|
+
traversal: { seedIds: [], steps: input.steps, maxSeeds: GRAPH_RETRIEVAL_MAX_SEEDS, maxSteps: GRAPH_RETRIEVAL_MAX_STEPS, maxLimit: GRAPH_RETRIEVAL_MAX_LIMIT },
|
|
436
|
+
coverage: 'unknown',
|
|
437
|
+
partialReasons,
|
|
438
|
+
missingEvidence: ['graph_v1_migration_required'],
|
|
439
|
+
evidence: [],
|
|
440
|
+
sufficiency: 'insufficient',
|
|
441
|
+
absenceConfirmed: false
|
|
442
|
+
};
|
|
443
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export type JudgmentAutonomyVerdict = 'continue' | 'human_required' | 'not_applicable';
|
|
2
|
+
export type JudgmentAutonomyReasonCode = 'not_a_human_escalation' | 'routine_reversible_work' | 'semantic_judgment_required' | 'irreversible_external_action' | 'missing_authority_or_secret' | 'material_commitment' | 'owner_value_choice' | 'resolver_selected_clarification' | 'resolver_continue' | 'resolver_human_required';
|
|
3
|
+
export interface JudgmentAutonomyInstructionPatch {
|
|
4
|
+
cancel: readonly string[];
|
|
5
|
+
do_next: readonly string[];
|
|
6
|
+
acceptance_criteria: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
export interface JudgmentAutonomyContext {
|
|
9
|
+
turn_id: string;
|
|
10
|
+
request: string;
|
|
11
|
+
final_answer: string;
|
|
12
|
+
project_code?: string;
|
|
13
|
+
selected_dag_ids: readonly string[];
|
|
14
|
+
}
|
|
15
|
+
export interface JudgmentAutonomyResolverRequest {
|
|
16
|
+
schema_version: 'brainbase-autonomy-resolver-request-v1';
|
|
17
|
+
case_id: string;
|
|
18
|
+
turn_id: string;
|
|
19
|
+
task_request: string;
|
|
20
|
+
proposed_human_question: string;
|
|
21
|
+
project_code?: string;
|
|
22
|
+
selected_dag_ids: readonly string[];
|
|
23
|
+
policy: {
|
|
24
|
+
default_behavior: 'continue';
|
|
25
|
+
complexity_behavior: 'decompose_and_continue';
|
|
26
|
+
human_only: readonly [
|
|
27
|
+
'irreversible_external_action',
|
|
28
|
+
'missing_authority_or_secret',
|
|
29
|
+
'material_commitment',
|
|
30
|
+
'new_owner_value_choice'
|
|
31
|
+
];
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export interface JudgmentAutonomyResolverDecision {
|
|
35
|
+
schema_version: 'brainbase-autonomy-resolver-decision-v1';
|
|
36
|
+
case_id: string;
|
|
37
|
+
verdict: 'continue' | 'human_required';
|
|
38
|
+
reason_code: string;
|
|
39
|
+
reason: string;
|
|
40
|
+
basis: ReadonlyArray<{
|
|
41
|
+
entity_id: string;
|
|
42
|
+
application: string;
|
|
43
|
+
}>;
|
|
44
|
+
instruction_patch?: JudgmentAutonomyInstructionPatch;
|
|
45
|
+
human_question?: string;
|
|
46
|
+
}
|
|
47
|
+
export type JudgmentAutonomyResolver = (request: JudgmentAutonomyResolverRequest) => Promise<JudgmentAutonomyResolverDecision>;
|
|
48
|
+
export interface JudgmentAutonomyEvaluation {
|
|
49
|
+
schema_version: 'brainbase-autonomy-evaluation-v1';
|
|
50
|
+
case_id: string;
|
|
51
|
+
verdict: JudgmentAutonomyVerdict;
|
|
52
|
+
reason_code: JudgmentAutonomyReasonCode;
|
|
53
|
+
reason: string;
|
|
54
|
+
question: string | null;
|
|
55
|
+
source: 'deterministic' | 'same_codex' | 'independent_resolver';
|
|
56
|
+
instruction_patch: JudgmentAutonomyInstructionPatch | null;
|
|
57
|
+
resolver_decision: JudgmentAutonomyResolverDecision | null;
|
|
58
|
+
}
|
|
59
|
+
export declare function visibleAnswerBody(answer: string): string;
|
|
60
|
+
export declare function extractHumanDecisionQuestion(answer: string): string | null;
|
|
61
|
+
export declare function evaluateJudgmentAutonomy(context: JudgmentAutonomyContext, resolver?: JudgmentAutonomyResolver): Promise<JudgmentAutonomyEvaluation>;
|
|
62
|
+
export declare function renderJudgmentAutonomyContinuation(evaluation: JudgmentAutonomyEvaluation): string;
|