@unson/brainbase-mcp 0.4.1 → 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 +5 -5
- 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/dist/cli.js +17 -0
- 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-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.d.ts +4 -0
- package/dist/judgment-dag.js +2 -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 +16 -6
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
function requiredText(value, field) {
|
|
2
|
+
const normalized = value.trim();
|
|
3
|
+
if (!normalized)
|
|
4
|
+
throw new TypeError(`${field} must be a non-empty string`);
|
|
5
|
+
return normalized;
|
|
6
|
+
}
|
|
7
|
+
function optionalText(value) {
|
|
8
|
+
if (value === null)
|
|
9
|
+
return null;
|
|
10
|
+
const normalized = value.trim();
|
|
11
|
+
return normalized || null;
|
|
12
|
+
}
|
|
13
|
+
function outcomeStatusLabel(status) {
|
|
14
|
+
if (status === 'outcome_verified')
|
|
15
|
+
return '成果確認済み';
|
|
16
|
+
if (status === 'unconfirmed')
|
|
17
|
+
return '結果未確認';
|
|
18
|
+
return '成果確認の対象外';
|
|
19
|
+
}
|
|
20
|
+
function evidenceLabel(evidence) {
|
|
21
|
+
const label = evidence.label?.trim() || evidence.kind;
|
|
22
|
+
return `${label} (${evidence.status === 'verified' ? '確認済み' : '未確認'})`;
|
|
23
|
+
}
|
|
24
|
+
export function validateJudgmentValueProof(proof) {
|
|
25
|
+
if (proof?.schema_version !== 'brainbase-judgment-value-proof-v1') {
|
|
26
|
+
throw new TypeError('unsupported judgment value proof schema');
|
|
27
|
+
}
|
|
28
|
+
requiredText(proof.intent_id, 'intent_id');
|
|
29
|
+
requiredText(proof.decision_attempt_id, 'decision_attempt_id');
|
|
30
|
+
requiredText(proof.recorded_at, 'recorded_at');
|
|
31
|
+
if (proof.interruption.resolution === 'continued_without_human') {
|
|
32
|
+
if (!optionalText(proof.decision.summary)) {
|
|
33
|
+
throw new TypeError('continued_without_human requires decision.summary');
|
|
34
|
+
}
|
|
35
|
+
if (!optionalText(proof.interruption.question_display_text)
|
|
36
|
+
&& !optionalText(proof.interruption.question_digest)) {
|
|
37
|
+
throw new TypeError('continued_without_human requires a redacted question or digest');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (proof.interruption.resolution === 'human_required') {
|
|
41
|
+
if (!proof.human_decision) {
|
|
42
|
+
throw new TypeError('human_required requires human_decision');
|
|
43
|
+
}
|
|
44
|
+
requiredText(proof.human_decision.question, 'human_decision.question');
|
|
45
|
+
requiredText(proof.human_decision.why_human, 'human_decision.why_human');
|
|
46
|
+
for (const option of proof.human_decision.options) {
|
|
47
|
+
requiredText(option.id, 'human_decision.options[].id');
|
|
48
|
+
requiredText(option.label, 'human_decision.options[].label');
|
|
49
|
+
requiredText(option.impact, 'human_decision.options[].impact');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (proof.outcome.status === 'outcome_verified') {
|
|
53
|
+
if (!optionalText(proof.outcome.summary)) {
|
|
54
|
+
throw new TypeError('outcome_verified requires outcome.summary');
|
|
55
|
+
}
|
|
56
|
+
if (!proof.outcome.evidence_refs.some((entry) => entry.status === 'verified')) {
|
|
57
|
+
throw new TypeError('outcome_verified requires verified evidence');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (proof.feedback.status !== 'none' && proof.feedback.status !== 'pending'
|
|
61
|
+
&& !proof.feedback.evidence_ref) {
|
|
62
|
+
throw new TypeError('recorded feedback requires evidence_ref');
|
|
63
|
+
}
|
|
64
|
+
return proof;
|
|
65
|
+
}
|
|
66
|
+
export function placeJudgmentValueProof(proof) {
|
|
67
|
+
validateJudgmentValueProof(proof);
|
|
68
|
+
let companionAttention = 'none';
|
|
69
|
+
if (proof.interruption.resolution === 'human_required') {
|
|
70
|
+
companionAttention = 'human_decision';
|
|
71
|
+
}
|
|
72
|
+
else if (proof.state === 'blocked' || proof.execution.status === 'blocked') {
|
|
73
|
+
companionAttention = 'blocked';
|
|
74
|
+
}
|
|
75
|
+
else if (proof.outcome.status === 'unconfirmed' && proof.execution.status === 'completed') {
|
|
76
|
+
companionAttention = 'outcome_unconfirmed';
|
|
77
|
+
}
|
|
78
|
+
else if (proof.feedback.status === 'pending') {
|
|
79
|
+
companionAttention = 'feedback_requested';
|
|
80
|
+
}
|
|
81
|
+
const behaviorChanged = proof.interruption.resolution === 'continued_without_human';
|
|
82
|
+
const completed = proof.execution.status === 'completed';
|
|
83
|
+
return {
|
|
84
|
+
agent_progress: behaviorChanged && proof.execution.status === 'executing' ? 'show' : 'silent',
|
|
85
|
+
agent_completion: behaviorChanged && completed ? 'show' : 'silent',
|
|
86
|
+
companion_attention: companionAttention,
|
|
87
|
+
web_surface: 'none',
|
|
88
|
+
weekly_digest: proof.interruption.resolution === 'not_applicable'
|
|
89
|
+
&& proof.feedback.status === 'none'
|
|
90
|
+
&& proof.state !== 'blocked'
|
|
91
|
+
&& proof.outcome.status === 'not_applicable'
|
|
92
|
+
? 'exclude'
|
|
93
|
+
: 'include'
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export function renderJudgmentValueProofProgress(proof) {
|
|
97
|
+
const placement = placeJudgmentValueProof(proof);
|
|
98
|
+
if (placement.agent_progress === 'silent')
|
|
99
|
+
return null;
|
|
100
|
+
const decision = requiredText(proof.decision.summary ?? '', 'decision.summary');
|
|
101
|
+
const execution = optionalText(proof.execution.summary) ?? '作業を続行しています';
|
|
102
|
+
return `Brainbaseが判断を代行:${decision}。確認で止めず、${execution}。`;
|
|
103
|
+
}
|
|
104
|
+
export function renderJudgmentValueProofCompletion(proof) {
|
|
105
|
+
const placement = placeJudgmentValueProof(proof);
|
|
106
|
+
if (placement.agent_completion === 'silent')
|
|
107
|
+
return null;
|
|
108
|
+
const result = optionalText(proof.outcome.summary)
|
|
109
|
+
?? optionalText(proof.execution.summary)
|
|
110
|
+
?? '実行は完了しましたが、結果の要約はありません';
|
|
111
|
+
const decision = requiredText(proof.decision.summary ?? '', 'decision.summary');
|
|
112
|
+
const impact = optionalText(proof.decision.work_impact) ?? '確認による中断を避けて作業を継続';
|
|
113
|
+
const basis = proof.decision.basis.length > 0
|
|
114
|
+
? proof.decision.basis.map((entry) => entry.application).join(' / ')
|
|
115
|
+
: '適用根拠は未確認';
|
|
116
|
+
const evidence = proof.outcome.evidence_refs.length > 0
|
|
117
|
+
? proof.outcome.evidence_refs.map(evidenceLabel).join(' / ')
|
|
118
|
+
: '成果証跡なし';
|
|
119
|
+
return [
|
|
120
|
+
'Brainbase判断レシート',
|
|
121
|
+
`結果: ${result}`,
|
|
122
|
+
`判断: ${decision}`,
|
|
123
|
+
`仕事への影響: ${impact}`,
|
|
124
|
+
`根拠: ${basis}`,
|
|
125
|
+
`状態: ${outcomeStatusLabel(proof.outcome.status)}`,
|
|
126
|
+
`証拠: ${evidence}`,
|
|
127
|
+
'修正する場合: 「判断を修正: …」または「次回は確認」と返信'
|
|
128
|
+
].join('\n');
|
|
129
|
+
}
|
|
130
|
+
export function renderJudgmentHumanDecisionRequest(proof) {
|
|
131
|
+
validateJudgmentValueProof(proof);
|
|
132
|
+
if (proof.interruption.resolution !== 'human_required' || !proof.human_decision)
|
|
133
|
+
return null;
|
|
134
|
+
const options = proof.human_decision.options.length > 0
|
|
135
|
+
? proof.human_decision.options.map((option) => (`${option.id}. ${option.label}\n 影響: ${option.impact}`)).join('\n')
|
|
136
|
+
: '選択肢はまだ整理できていません';
|
|
137
|
+
return [
|
|
138
|
+
'人間判断が必要です',
|
|
139
|
+
`判断: ${proof.human_decision.question}`,
|
|
140
|
+
`AIで決めない理由: ${proof.human_decision.why_human}`,
|
|
141
|
+
'選択肢:',
|
|
142
|
+
options
|
|
143
|
+
].join('\n');
|
|
144
|
+
}
|
|
145
|
+
export function projectJudgmentValueProofAttention(proof) {
|
|
146
|
+
const placement = placeJudgmentValueProof(proof);
|
|
147
|
+
const kind = placement.companion_attention;
|
|
148
|
+
if (kind === 'none')
|
|
149
|
+
return null;
|
|
150
|
+
if (kind === 'human_decision' && proof.human_decision) {
|
|
151
|
+
return {
|
|
152
|
+
schema_version: 'brainbase-judgment-value-proof-attention-v1',
|
|
153
|
+
intent_id: proof.intent_id,
|
|
154
|
+
decision_attempt_id: proof.decision_attempt_id,
|
|
155
|
+
kind,
|
|
156
|
+
title: '人間判断が必要',
|
|
157
|
+
summary: `${proof.human_decision.question} — ${proof.human_decision.why_human}`,
|
|
158
|
+
suggested_actions: proof.human_decision.options.map((option) => `${option.id}: ${option.label}`)
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
if (kind === 'blocked') {
|
|
162
|
+
return {
|
|
163
|
+
schema_version: 'brainbase-judgment-value-proof-attention-v1',
|
|
164
|
+
intent_id: proof.intent_id,
|
|
165
|
+
decision_attempt_id: proof.decision_attempt_id,
|
|
166
|
+
kind,
|
|
167
|
+
title: '作業が停止しています',
|
|
168
|
+
summary: optionalText(proof.execution.summary) ?? '停止理由は未確認です',
|
|
169
|
+
suggested_actions: ['停止理由を確認', '再実行条件を決める']
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (kind === 'outcome_unconfirmed') {
|
|
173
|
+
return {
|
|
174
|
+
schema_version: 'brainbase-judgment-value-proof-attention-v1',
|
|
175
|
+
intent_id: proof.intent_id,
|
|
176
|
+
decision_attempt_id: proof.decision_attempt_id,
|
|
177
|
+
kind,
|
|
178
|
+
title: '実行結果を確認できていません',
|
|
179
|
+
summary: optionalText(proof.execution.summary) ?? '実行は完了しました',
|
|
180
|
+
suggested_actions: ['正本を読み戻す', '結果未確認のまま保持']
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
schema_version: 'brainbase-judgment-value-proof-attention-v1',
|
|
185
|
+
intent_id: proof.intent_id,
|
|
186
|
+
decision_attempt_id: proof.decision_attempt_id,
|
|
187
|
+
kind: 'feedback_requested',
|
|
188
|
+
title: '判断へのフィードバックが必要',
|
|
189
|
+
summary: optionalText(proof.decision.summary) ?? '判断内容を確認してください',
|
|
190
|
+
suggested_actions: ['正しい', '判断を修正', '次回は確認']
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
export function renderJudgmentValueProofWeeklyDigest(input) {
|
|
194
|
+
const periodLabel = requiredText(input.period_label, 'period_label');
|
|
195
|
+
if (input.coverage === 'unavailable') {
|
|
196
|
+
return `${periodLabel}のBrainbase判断実績は取得できませんでした。0件としては扱いません。`;
|
|
197
|
+
}
|
|
198
|
+
const proofs = input.proofs.map(validateJudgmentValueProof);
|
|
199
|
+
const included = proofs.filter((proof) => placeJudgmentValueProof(proof).weekly_digest === 'include');
|
|
200
|
+
const verified = included.filter((proof) => proof.outcome.status === 'outcome_verified').length;
|
|
201
|
+
const humanRequired = included.filter((proof) => proof.interruption.resolution === 'human_required').length;
|
|
202
|
+
const corrected = included.filter((proof) => proof.feedback.status === 'corrected'
|
|
203
|
+
|| proof.feedback.status === 'reverted'
|
|
204
|
+
|| proof.feedback.status === 'next_time_ask').length;
|
|
205
|
+
const unconfirmed = included.filter((proof) => proof.outcome.status === 'unconfirmed').length;
|
|
206
|
+
const blocked = included.filter((proof) => proof.state === 'blocked'
|
|
207
|
+
|| proof.execution.status === 'blocked').length;
|
|
208
|
+
const continued = included.filter((proof) => proof.interruption.resolution === 'continued_without_human').length;
|
|
209
|
+
const coverage = input.coverage === 'partial' ? '(一部データのみ)' : '';
|
|
210
|
+
const representativeLimit = Math.max(0, Math.floor(input.representative_limit ?? 3));
|
|
211
|
+
const examples = included
|
|
212
|
+
.filter((proof) => proof.decision.summary || proof.outcome.summary)
|
|
213
|
+
.slice(0, representativeLimit)
|
|
214
|
+
.map((proof, index) => {
|
|
215
|
+
const decision = optionalText(proof.decision.summary) ?? '人間判断';
|
|
216
|
+
const result = optionalText(proof.outcome.summary) ?? outcomeStatusLabel(proof.outcome.status);
|
|
217
|
+
return `${index + 1}. ${decision}\n → ${result}`;
|
|
218
|
+
});
|
|
219
|
+
return [
|
|
220
|
+
`${periodLabel}、Brainbaseが仕事をどう前に進めたか${coverage}`,
|
|
221
|
+
`確認せず続行した判断: ${continued}件`,
|
|
222
|
+
`成果確認まで完了: ${verified}件`,
|
|
223
|
+
`人間判断が必要: ${humanRequired}件`,
|
|
224
|
+
`判断の訂正・取消: ${corrected}件`,
|
|
225
|
+
`結果未確認: ${unconfirmed}件`,
|
|
226
|
+
`停止中: ${blocked}件`,
|
|
227
|
+
...(examples.length > 0 ? ['', '代表例', ...examples] : [])
|
|
228
|
+
].join('\n');
|
|
229
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type PortableGraphBundle } from './portable-graph.js';
|
|
2
|
+
import type { GraphRetrievalInput, GraphRetrievalResponse } from './graph-retrieval.js';
|
|
3
|
+
/** The organization adapter is opt-in and never falls back after configuration. */
|
|
4
|
+
export declare const ORGANIZATION_GRAPH_DEFAULT_TIMEOUT_MS = 10000;
|
|
5
|
+
export declare const ORGANIZATION_GRAPH_MAX_RESPONSE_BYTES = 9000000;
|
|
6
|
+
export interface OrganizationGraphConfig {
|
|
7
|
+
/** Organization service base URL, without credentials or query parameters. */
|
|
8
|
+
readonly url: string;
|
|
9
|
+
/** Bearer token. Never include this value in a returned error or receipt. */
|
|
10
|
+
readonly token: string;
|
|
11
|
+
readonly projectCode: string;
|
|
12
|
+
readonly graphId: string;
|
|
13
|
+
readonly fetch?: OrganizationGraphFetch;
|
|
14
|
+
readonly timeoutMs?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface OrganizationGraphFetchResponse {
|
|
17
|
+
readonly status: number;
|
|
18
|
+
readonly headers?: {
|
|
19
|
+
get(name: string): string | null;
|
|
20
|
+
} | Map<string, string> | Record<string, string | undefined>;
|
|
21
|
+
readonly body?: {
|
|
22
|
+
getReader(): {
|
|
23
|
+
read(): Promise<{
|
|
24
|
+
done: boolean;
|
|
25
|
+
value?: Uint8Array;
|
|
26
|
+
}>;
|
|
27
|
+
cancel?(reason?: unknown): Promise<void>;
|
|
28
|
+
};
|
|
29
|
+
} | null;
|
|
30
|
+
text?(): Promise<string>;
|
|
31
|
+
json?(): Promise<unknown>;
|
|
32
|
+
}
|
|
33
|
+
export type OrganizationGraphFetch = (input: string, init: {
|
|
34
|
+
method: 'GET' | 'POST';
|
|
35
|
+
headers: Record<string, string>;
|
|
36
|
+
body?: string;
|
|
37
|
+
redirect: 'error';
|
|
38
|
+
signal: AbortSignal;
|
|
39
|
+
}) => Promise<OrganizationGraphFetchResponse>;
|
|
40
|
+
export interface PortableGraphReadback {
|
|
41
|
+
readonly bundle: unknown;
|
|
42
|
+
readonly digest: string;
|
|
43
|
+
}
|
|
44
|
+
export interface PortableGraphImportResult {
|
|
45
|
+
readonly status: 'imported' | 'unchanged';
|
|
46
|
+
readonly digest: string;
|
|
47
|
+
}
|
|
48
|
+
export declare class OrganizationGraphConfigError extends Error {
|
|
49
|
+
readonly code = "organization_config_invalid";
|
|
50
|
+
constructor(message: string);
|
|
51
|
+
}
|
|
52
|
+
export declare class OrganizationGraphError extends Error {
|
|
53
|
+
readonly code: string;
|
|
54
|
+
constructor(code: string, message: string);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Read the four organization settings as one all-or-nothing configuration.
|
|
58
|
+
* With no settings, OSS keeps its existing local-first behavior. A partial
|
|
59
|
+
* configuration is an error so a typo cannot silently select another store.
|
|
60
|
+
*/
|
|
61
|
+
export declare function createOrganizationGraphConfig(env?: Record<string, string | undefined>): OrganizationGraphConfig | undefined;
|
|
62
|
+
/** Create a client without making a network request. */
|
|
63
|
+
export declare function createOrganizationGraphClient(config: OrganizationGraphConfig): OrganizationGraphClient;
|
|
64
|
+
export interface OrganizationGraphClient {
|
|
65
|
+
search(input: GraphRetrievalInput): Promise<GraphRetrievalResponse>;
|
|
66
|
+
importPortableGraph(bundle: unknown): Promise<PortableGraphImportResult>;
|
|
67
|
+
readPortableGraph(): Promise<PortableGraphReadback>;
|
|
68
|
+
}
|
|
69
|
+
/** Forward the existing Graph retrieval input unchanged inside the request envelope. */
|
|
70
|
+
export declare function searchOrganizationGraph(config: OrganizationGraphConfig, input: GraphRetrievalInput): Promise<GraphRetrievalResponse>;
|
|
71
|
+
/** Import is explicit; merely configuring the remote backend never uploads data. */
|
|
72
|
+
export declare function importPortableGraph(config: OrganizationGraphConfig, bundle: unknown): Promise<PortableGraphImportResult>;
|
|
73
|
+
/** Read and verify the complete organization-side bundle and its digest. */
|
|
74
|
+
export declare function readPortableGraph(config: OrganizationGraphConfig): Promise<PortableGraphReadback>;
|
|
75
|
+
/** Validate the privacy boundary before any upload leaves the local process. */
|
|
76
|
+
export declare function assertPortableGraphBundle(bundle: unknown): asserts bundle is PortableGraphBundle;
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { portableGraphDigest, validatePortableGraph } from './portable-graph.js';
|
|
2
|
+
/** The organization adapter is opt-in and never falls back after configuration. */
|
|
3
|
+
export const ORGANIZATION_GRAPH_DEFAULT_TIMEOUT_MS = 10_000;
|
|
4
|
+
export const ORGANIZATION_GRAPH_MAX_RESPONSE_BYTES = 9_000_000;
|
|
5
|
+
const MAX_URL_LENGTH = 2_048;
|
|
6
|
+
const MAX_TOKEN_LENGTH = 4_096;
|
|
7
|
+
const MAX_PROJECT_CODE_LENGTH = 256;
|
|
8
|
+
const MAX_GRAPH_ID_LENGTH = 128;
|
|
9
|
+
const CONTROL_CHARACTER = /[\u0000-\u001f\u007f]/u;
|
|
10
|
+
export class OrganizationGraphConfigError extends Error {
|
|
11
|
+
code = 'organization_config_invalid';
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super(`${'organization_config_invalid'}: ${message}`);
|
|
14
|
+
this.name = 'OrganizationGraphConfigError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class OrganizationGraphError extends Error {
|
|
18
|
+
code;
|
|
19
|
+
constructor(code, message) {
|
|
20
|
+
super(`${code}: ${message}`);
|
|
21
|
+
this.name = 'OrganizationGraphError';
|
|
22
|
+
this.code = code;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Read the four organization settings as one all-or-nothing configuration.
|
|
27
|
+
* With no settings, OSS keeps its existing local-first behavior. A partial
|
|
28
|
+
* configuration is an error so a typo cannot silently select another store.
|
|
29
|
+
*/
|
|
30
|
+
export function createOrganizationGraphConfig(env = process.env) {
|
|
31
|
+
const rawUrl = env.BRAINBASE_ORGANIZATION_URL;
|
|
32
|
+
const rawToken = env.BRAINBASE_ORGANIZATION_TOKEN;
|
|
33
|
+
const rawProjectCode = env.BRAINBASE_ORGANIZATION_PROJECT;
|
|
34
|
+
const rawGraphId = env.BRAINBASE_ORGANIZATION_GRAPH_ID;
|
|
35
|
+
const configured = [rawUrl, rawToken, rawProjectCode, rawGraphId].some((value) => value !== undefined);
|
|
36
|
+
if (!configured)
|
|
37
|
+
return undefined;
|
|
38
|
+
const missing = [
|
|
39
|
+
['BRAINBASE_ORGANIZATION_URL', rawUrl],
|
|
40
|
+
['BRAINBASE_ORGANIZATION_TOKEN', rawToken],
|
|
41
|
+
['BRAINBASE_ORGANIZATION_PROJECT', rawProjectCode],
|
|
42
|
+
['BRAINBASE_ORGANIZATION_GRAPH_ID', rawGraphId]
|
|
43
|
+
].filter(([, value]) => typeof value !== 'string' || value.trim() === '').map(([name]) => name);
|
|
44
|
+
if (missing.length > 0) {
|
|
45
|
+
throw new OrganizationGraphConfigError(`all organization settings must be set together; missing ${missing.join(', ')}`);
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
url: validateOrganizationUrl(rawUrl),
|
|
49
|
+
token: validateToken(rawToken),
|
|
50
|
+
projectCode: validateProjectCode(rawProjectCode),
|
|
51
|
+
graphId: validateGraphId(rawGraphId)
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** Create a client without making a network request. */
|
|
55
|
+
export function createOrganizationGraphClient(config) {
|
|
56
|
+
// Validate injected configurations as well as environment-derived ones.
|
|
57
|
+
const normalized = {
|
|
58
|
+
...config,
|
|
59
|
+
url: validateOrganizationUrl(config.url),
|
|
60
|
+
token: validateToken(config.token),
|
|
61
|
+
projectCode: validateProjectCode(config.projectCode),
|
|
62
|
+
graphId: validateGraphId(config.graphId)
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
search: (input) => searchOrganizationGraph(normalized, input),
|
|
66
|
+
importPortableGraph: (bundle) => importPortableGraph(normalized, bundle),
|
|
67
|
+
readPortableGraph: () => readPortableGraph(normalized)
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/** Forward the existing Graph retrieval input unchanged inside the request envelope. */
|
|
71
|
+
export async function searchOrganizationGraph(config, input) {
|
|
72
|
+
const payload = await requestJson(config, 'POST', route(config, 'search'), {
|
|
73
|
+
project_code: config.projectCode,
|
|
74
|
+
input
|
|
75
|
+
});
|
|
76
|
+
return parseGraphRetrievalResponse(payload);
|
|
77
|
+
}
|
|
78
|
+
/** Import is explicit; merely configuring the remote backend never uploads data. */
|
|
79
|
+
export async function importPortableGraph(config, bundle) {
|
|
80
|
+
assertPortableGraphBundle(bundle);
|
|
81
|
+
const expectedDigest = await portableGraphDigest(bundle);
|
|
82
|
+
const payload = await requestJson(config, 'POST', route(config, 'import'), {
|
|
83
|
+
project_code: config.projectCode,
|
|
84
|
+
bundle
|
|
85
|
+
});
|
|
86
|
+
const result = parseImportResult(payload);
|
|
87
|
+
if (result.digest !== expectedDigest) {
|
|
88
|
+
throw new OrganizationGraphError('organization_graph_digest_mismatch', 'organization service returned an import digest that does not match the uploaded bundle');
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
/** Read and verify the complete organization-side bundle and its digest. */
|
|
93
|
+
export async function readPortableGraph(config) {
|
|
94
|
+
const payload = await requestJson(config, 'GET', route(config, undefined, { project_code: config.projectCode }));
|
|
95
|
+
const readback = parsePortableGraphReadback(payload);
|
|
96
|
+
assertPortableGraphBundle(readback.bundle);
|
|
97
|
+
const computedDigest = await portableGraphDigest(readback.bundle);
|
|
98
|
+
if (computedDigest !== readback.digest) {
|
|
99
|
+
throw new OrganizationGraphError('organization_graph_readback_mismatch', 'organization service returned a bundle whose digest does not match its contents');
|
|
100
|
+
}
|
|
101
|
+
return readback;
|
|
102
|
+
}
|
|
103
|
+
/** Validate the privacy boundary before any upload leaves the local process. */
|
|
104
|
+
export function assertPortableGraphBundle(bundle) {
|
|
105
|
+
validatePortableGraph(bundle);
|
|
106
|
+
}
|
|
107
|
+
function parseGraphRetrievalResponse(value) {
|
|
108
|
+
if (!isRecord(value)
|
|
109
|
+
|| (value.graphVersion !== 1 && value.graphVersion !== 2)
|
|
110
|
+
|| (value.schemaVersion !== 1 && value.schemaVersion !== 2)
|
|
111
|
+
|| (value.status !== 'ok' && value.status !== 'migration_required')
|
|
112
|
+
|| typeof value.migrationRequired !== 'boolean'
|
|
113
|
+
|| value.authority !== 'organization_graph'
|
|
114
|
+
|| typeof value.query !== 'string'
|
|
115
|
+
|| typeof value.asOf !== 'string'
|
|
116
|
+
|| !['semantic', 'lexical', 'seed'].includes(String(value.method))
|
|
117
|
+
|| !isRecord(value.semantic)
|
|
118
|
+
|| typeof value.semantic.available !== 'boolean'
|
|
119
|
+
|| !['semantic', 'lexical', 'seed'].includes(String(value.semantic.method))
|
|
120
|
+
|| !Array.isArray(value.candidates)
|
|
121
|
+
|| !Array.isArray(value.results)
|
|
122
|
+
|| !Array.isArray(value.observedRelations)
|
|
123
|
+
|| !Array.isArray(value.observedRelationCatalog)
|
|
124
|
+
|| !isRecord(value.traversal)
|
|
125
|
+
|| !Array.isArray(value.traversal.seedIds)
|
|
126
|
+
|| !Array.isArray(value.traversal.steps)
|
|
127
|
+
|| !Number.isInteger(value.traversal.maxSeeds)
|
|
128
|
+
|| !Number.isInteger(value.traversal.maxSteps)
|
|
129
|
+
|| !Number.isInteger(value.traversal.maxLimit)
|
|
130
|
+
|| !['complete', 'partial', 'unknown'].includes(String(value.coverage))
|
|
131
|
+
|| !Array.isArray(value.partialReasons)
|
|
132
|
+
|| !Array.isArray(value.missingEvidence)
|
|
133
|
+
|| !Array.isArray(value.evidence)
|
|
134
|
+
|| !['needs_model_verification', 'insufficient'].includes(String(value.sufficiency))
|
|
135
|
+
|| value.absenceConfirmed !== false) {
|
|
136
|
+
throw new OrganizationGraphError('organization_graph_response_invalid', 'organization service returned a response outside the OSS GraphRetrievalResponse contract');
|
|
137
|
+
}
|
|
138
|
+
return value;
|
|
139
|
+
}
|
|
140
|
+
function parseImportResult(value) {
|
|
141
|
+
if (!isRecord(value)
|
|
142
|
+
|| (value.status !== 'imported' && value.status !== 'unchanged')
|
|
143
|
+
|| typeof value.digest !== 'string'
|
|
144
|
+
|| value.digest.trim() === '') {
|
|
145
|
+
throw new OrganizationGraphError('organization_graph_response_invalid', 'organization service returned an invalid import response');
|
|
146
|
+
}
|
|
147
|
+
return { status: value.status, digest: value.digest };
|
|
148
|
+
}
|
|
149
|
+
function parsePortableGraphReadback(value) {
|
|
150
|
+
if (!isRecord(value) || !('bundle' in value) || typeof value.digest !== 'string' || value.digest.trim() === '') {
|
|
151
|
+
throw new OrganizationGraphError('organization_graph_response_invalid', 'organization service returned an invalid Graph readback');
|
|
152
|
+
}
|
|
153
|
+
return { bundle: value.bundle, digest: value.digest };
|
|
154
|
+
}
|
|
155
|
+
async function requestJson(config, method, url, body) {
|
|
156
|
+
const fetchImpl = config.fetch ?? defaultFetch;
|
|
157
|
+
const startedAt = Date.now();
|
|
158
|
+
const controller = new AbortController();
|
|
159
|
+
const timeoutMs = validateTimeout(config.timeoutMs ?? ORGANIZATION_GRAPH_DEFAULT_TIMEOUT_MS);
|
|
160
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
161
|
+
if (typeof timer === 'object' && timer !== null && 'unref' in timer && typeof timer.unref === 'function')
|
|
162
|
+
timer.unref();
|
|
163
|
+
const headers = {
|
|
164
|
+
accept: 'application/json',
|
|
165
|
+
authorization: `Bearer ${config.token}`
|
|
166
|
+
};
|
|
167
|
+
if (body !== undefined)
|
|
168
|
+
headers['content-type'] = 'application/json';
|
|
169
|
+
let response;
|
|
170
|
+
try {
|
|
171
|
+
response = await withTimeout(fetchImpl(url, {
|
|
172
|
+
method,
|
|
173
|
+
headers,
|
|
174
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
175
|
+
redirect: 'error',
|
|
176
|
+
signal: controller.signal
|
|
177
|
+
}), controller, timeoutMs);
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
clearTimeout(timer);
|
|
181
|
+
if (error instanceof OrganizationGraphError)
|
|
182
|
+
throw error;
|
|
183
|
+
if (controller.signal.aborted) {
|
|
184
|
+
throw new OrganizationGraphError('organization_graph_timeout', `organization service did not respond within ${timeoutMs}ms`);
|
|
185
|
+
}
|
|
186
|
+
throw new OrganizationGraphError('organization_graph_unavailable', 'organization service request failed');
|
|
187
|
+
}
|
|
188
|
+
clearTimeout(timer);
|
|
189
|
+
if (!Number.isInteger(response.status)) {
|
|
190
|
+
throw new OrganizationGraphError('organization_graph_unavailable', 'organization service returned an invalid HTTP status');
|
|
191
|
+
}
|
|
192
|
+
if (response.status >= 300 && response.status < 400) {
|
|
193
|
+
throw new OrganizationGraphError('organization_graph_redirect_rejected', 'organization service redirects are not accepted');
|
|
194
|
+
}
|
|
195
|
+
if (response.status === 401 || response.status === 403) {
|
|
196
|
+
throw new OrganizationGraphError('organization_graph_unauthorized', 'organization service rejected the configured credentials');
|
|
197
|
+
}
|
|
198
|
+
if (response.status < 200 || response.status >= 300) {
|
|
199
|
+
throw new OrganizationGraphError('organization_graph_request_failed', `organization service returned HTTP ${response.status}`);
|
|
200
|
+
}
|
|
201
|
+
let raw;
|
|
202
|
+
try {
|
|
203
|
+
raw = await withTimeout(readResponseText(response, controller, timeoutMs), controller, Math.max(1, timeoutMs - (Date.now() - startedAt)));
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
if (error instanceof OrganizationGraphError)
|
|
207
|
+
throw error;
|
|
208
|
+
throw new OrganizationGraphError('organization_graph_response_invalid', 'organization service response could not be read');
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
return JSON.parse(raw);
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
throw new OrganizationGraphError('organization_graph_response_invalid', 'organization service returned invalid JSON');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async function readResponseText(response, controller, timeoutMs) {
|
|
218
|
+
if (response.body) {
|
|
219
|
+
const reader = response.body.getReader();
|
|
220
|
+
const chunks = [];
|
|
221
|
+
let bytes = 0;
|
|
222
|
+
while (true) {
|
|
223
|
+
const part = await withTimeout(reader.read(), controller, timeoutMs);
|
|
224
|
+
if (part.done)
|
|
225
|
+
break;
|
|
226
|
+
const chunk = Buffer.from(part.value ?? new Uint8Array());
|
|
227
|
+
bytes += chunk.byteLength;
|
|
228
|
+
if (bytes > ORGANIZATION_GRAPH_MAX_RESPONSE_BYTES) {
|
|
229
|
+
await reader.cancel?.('response too large');
|
|
230
|
+
throw new OrganizationGraphError('organization_graph_response_too_large', 'organization service response exceeds the maximum size');
|
|
231
|
+
}
|
|
232
|
+
chunks.push(chunk);
|
|
233
|
+
}
|
|
234
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
235
|
+
}
|
|
236
|
+
if (response.text) {
|
|
237
|
+
const raw = await withTimeout(response.text(), controller, timeoutMs);
|
|
238
|
+
if (Buffer.byteLength(raw, 'utf8') > ORGANIZATION_GRAPH_MAX_RESPONSE_BYTES) {
|
|
239
|
+
throw new OrganizationGraphError('organization_graph_response_too_large', 'organization service response exceeds the maximum size');
|
|
240
|
+
}
|
|
241
|
+
return raw;
|
|
242
|
+
}
|
|
243
|
+
if (response.json) {
|
|
244
|
+
const value = await withTimeout(response.json(), controller, timeoutMs);
|
|
245
|
+
const raw = JSON.stringify(value);
|
|
246
|
+
if (Buffer.byteLength(raw, 'utf8') > ORGANIZATION_GRAPH_MAX_RESPONSE_BYTES) {
|
|
247
|
+
throw new OrganizationGraphError('organization_graph_response_too_large', 'organization service response exceeds the maximum size');
|
|
248
|
+
}
|
|
249
|
+
return raw;
|
|
250
|
+
}
|
|
251
|
+
throw new OrganizationGraphError('organization_graph_response_invalid', 'organization service response has no readable body');
|
|
252
|
+
}
|
|
253
|
+
async function withTimeout(operation, controller, timeoutMs) {
|
|
254
|
+
let timer;
|
|
255
|
+
const timeout = new Promise((_, reject) => {
|
|
256
|
+
timer = setTimeout(() => {
|
|
257
|
+
controller.abort();
|
|
258
|
+
reject(new OrganizationGraphError('organization_graph_timeout', `organization service did not respond within ${timeoutMs}ms`));
|
|
259
|
+
}, timeoutMs);
|
|
260
|
+
if (typeof timer === 'object' && timer !== null && 'unref' in timer && typeof timer.unref === 'function')
|
|
261
|
+
timer.unref();
|
|
262
|
+
});
|
|
263
|
+
try {
|
|
264
|
+
return await Promise.race([operation, timeout]);
|
|
265
|
+
}
|
|
266
|
+
finally {
|
|
267
|
+
if (timer !== undefined)
|
|
268
|
+
clearTimeout(timer);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function route(config, suffix, query) {
|
|
272
|
+
const base = new URL(config.url);
|
|
273
|
+
const prefix = base.pathname.replace(/\/+$/u, '');
|
|
274
|
+
const routePath = `${prefix}/api/info/graph/portable/${encodeURIComponent(config.graphId)}${suffix ? `/${suffix}` : ''}`;
|
|
275
|
+
const endpoint = new URL(routePath || '/', base.origin);
|
|
276
|
+
for (const [key, value] of Object.entries(query ?? {}))
|
|
277
|
+
endpoint.searchParams.set(key, value);
|
|
278
|
+
return endpoint.href;
|
|
279
|
+
}
|
|
280
|
+
function validateOrganizationUrl(raw) {
|
|
281
|
+
if (typeof raw !== 'string' || raw.trim() === '' || raw.length > MAX_URL_LENGTH) {
|
|
282
|
+
throw new OrganizationGraphConfigError('BRAINBASE_ORGANIZATION_URL must be a valid base URL');
|
|
283
|
+
}
|
|
284
|
+
let endpoint;
|
|
285
|
+
try {
|
|
286
|
+
endpoint = new URL(raw.trim());
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
throw new OrganizationGraphConfigError('BRAINBASE_ORGANIZATION_URL must be a valid base URL');
|
|
290
|
+
}
|
|
291
|
+
if (endpoint.username || endpoint.password || endpoint.search || endpoint.hash) {
|
|
292
|
+
throw new OrganizationGraphConfigError('BRAINBASE_ORGANIZATION_URL must not contain credentials, query parameters, or a fragment');
|
|
293
|
+
}
|
|
294
|
+
const host = endpoint.hostname.toLowerCase().replace(/^\[|\]$/gu, '');
|
|
295
|
+
const loopback = host === 'localhost' || host === '127.0.0.1' || host === '::1';
|
|
296
|
+
if (endpoint.protocol !== 'https:' && !(endpoint.protocol === 'http:' && loopback)) {
|
|
297
|
+
throw new OrganizationGraphConfigError('BRAINBASE_ORGANIZATION_URL must use HTTPS; HTTP is allowed only for loopback services');
|
|
298
|
+
}
|
|
299
|
+
endpoint.pathname = endpoint.pathname.replace(/\/+$/u, '') || '/';
|
|
300
|
+
return endpoint.href;
|
|
301
|
+
}
|
|
302
|
+
function validateToken(raw) {
|
|
303
|
+
if (typeof raw !== 'string' || raw.trim() === '' || raw.length > MAX_TOKEN_LENGTH || CONTROL_CHARACTER.test(raw)) {
|
|
304
|
+
throw new OrganizationGraphConfigError('BRAINBASE_ORGANIZATION_TOKEN is invalid');
|
|
305
|
+
}
|
|
306
|
+
return raw;
|
|
307
|
+
}
|
|
308
|
+
function validateProjectCode(raw) {
|
|
309
|
+
if (typeof raw !== 'string' || raw.trim() === '' || raw.length > MAX_PROJECT_CODE_LENGTH || CONTROL_CHARACTER.test(raw)) {
|
|
310
|
+
throw new OrganizationGraphConfigError('BRAINBASE_ORGANIZATION_PROJECT is invalid');
|
|
311
|
+
}
|
|
312
|
+
return raw.trim();
|
|
313
|
+
}
|
|
314
|
+
function validateGraphId(raw) {
|
|
315
|
+
if (typeof raw !== 'string' || raw.trim() === '' || raw.length > MAX_GRAPH_ID_LENGTH || !/^[A-Za-z0-9][A-Za-z0-9._~-]*$/u.test(raw)) {
|
|
316
|
+
throw new OrganizationGraphConfigError('BRAINBASE_ORGANIZATION_GRAPH_ID must be an opaque path-safe ID');
|
|
317
|
+
}
|
|
318
|
+
return raw;
|
|
319
|
+
}
|
|
320
|
+
function validateTimeout(value) {
|
|
321
|
+
if (!Number.isInteger(value) || value < 1 || value > 60_000) {
|
|
322
|
+
throw new OrganizationGraphConfigError('organization graph timeout must be an integer between 1 and 60000 milliseconds');
|
|
323
|
+
}
|
|
324
|
+
return value;
|
|
325
|
+
}
|
|
326
|
+
function isRecord(value) {
|
|
327
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
328
|
+
}
|
|
329
|
+
function defaultFetch(input, init) {
|
|
330
|
+
return fetch(input, init);
|
|
331
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type GraphRetrievalInput, type GraphRetrievalResponse } from './graph-retrieval.js';
|
|
2
|
+
import type { EmbeddingProvider } from './embedding-provider.js';
|
|
3
|
+
import type { DecisionRecord, GraphFileV2, PersonalOs } from './types.js';
|
|
4
|
+
/** The public, read-only interchange format for a canonical Graph v2 snapshot. */
|
|
5
|
+
export declare const PORTABLE_GRAPH_SCHEMA_VERSION: 1;
|
|
6
|
+
/** Bounds keep an imported snapshot finite before it reaches a runtime or model. */
|
|
7
|
+
export declare const PORTABLE_GRAPH_MAX_ENTITIES = 10000;
|
|
8
|
+
export declare const PORTABLE_GRAPH_MAX_EDGES = 25000;
|
|
9
|
+
export declare const PORTABLE_GRAPH_MAX_DECISIONS = 10000;
|
|
10
|
+
export declare const PORTABLE_GRAPH_MAX_JSON_BYTES = 8000000;
|
|
11
|
+
export declare const PORTABLE_GRAPH_MAX_JSON_DEPTH = 64;
|
|
12
|
+
export interface PortableGraphBundle {
|
|
13
|
+
schemaVersion: typeof PORTABLE_GRAPH_SCHEMA_VERSION;
|
|
14
|
+
graph: GraphFileV2;
|
|
15
|
+
decisions: DecisionRecord[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Create a detached Graph v2 bundle for transfer between the OSS and
|
|
19
|
+
* organization runtimes. Personal KG, legacy relationships, source files, and
|
|
20
|
+
* the local data directory are deliberately outside this contract.
|
|
21
|
+
*/
|
|
22
|
+
export declare function createPortableGraph(os: PersonalOs): PortableGraphBundle;
|
|
23
|
+
/**
|
|
24
|
+
* Validate the exact portable contract. Unknown top-level fields are rejected
|
|
25
|
+
* so an organization runtime cannot silently ignore a new capability. Unknown
|
|
26
|
+
* fields inside Graph records are retained after the canonical Graph validator
|
|
27
|
+
* accepts them, which keeps forward-compatible metadata lossless.
|
|
28
|
+
*/
|
|
29
|
+
export declare function validatePortableGraph(value: unknown): asserts value is PortableGraphBundle;
|
|
30
|
+
/** Rehydrate a validated bundle into the shared retrieval input shape. */
|
|
31
|
+
export declare function hydratePortableGraph(bundle: PortableGraphBundle): PersonalOs;
|
|
32
|
+
/** Use exactly the OSS retrieval implementation against a portable bundle. */
|
|
33
|
+
export declare function retrievePortableGraph(bundle: PortableGraphBundle, input: GraphRetrievalInput, provider?: EmbeddingProvider): Promise<GraphRetrievalResponse>;
|
|
34
|
+
/** Canonical JSON with recursively sorted object keys and preserved array order. */
|
|
35
|
+
export declare function canonicalPortableJson(value: unknown): string;
|
|
36
|
+
/** Stable content identity for a portable Graph bundle. */
|
|
37
|
+
export declare function portableGraphDigest(value: PortableGraphBundle): string;
|
|
38
|
+
/** Descriptive alias for callers that prefer the content-identity wording. */
|
|
39
|
+
export declare const digestPortableGraph: typeof portableGraphDigest;
|