@unson/brainbase-mcp 0.12.0 → 0.14.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.
@@ -8,8 +8,9 @@ import type { FoundationDefinition, FoundationScope, FoundationType } from './on
8
8
  * principal, selected project, and current revision.
9
9
  */
10
10
  export declare const FOUNDATION_GRAPH_WRITE_CONTRACT_VERSION: "foundation-graph-draft-write.v1";
11
+ export declare const FOUNDATION_GRAPH_ADOPTION_CONTRACT_VERSION: "foundation-graph-adoption.v1";
11
12
  export type FoundationGraphWriteOperation = 'create' | 'update';
12
- export type FoundationGraphWriteIssueCode = 'INVALID_INPUT' | 'INVALID_IDENTITY' | 'ROW_DEFINITION_MISMATCH' | 'REVISION_CONFLICT' | 'CURRENT_DEFINITION_REQUIRED' | 'DEFINITION_INVALID' | 'DRAFT_ADOPTION_REQUIRED' | 'CANDIDATE_STORAGE_REQUIRED' | 'DRAFT_AUTHORIZATION_REQUIRED' | 'PROVENANCE_REQUIRED' | 'OWNER_MISMATCH' | 'ACL_CHANGE_FORBIDDEN' | 'WRITER_NOT_AUTHORIZED' | 'SCOPE_VIOLATION' | 'PROJECT_MISMATCH';
13
+ export type FoundationGraphWriteIssueCode = 'INVALID_INPUT' | 'INVALID_IDENTITY' | 'ROW_DEFINITION_MISMATCH' | 'REVISION_CONFLICT' | 'CURRENT_DEFINITION_REQUIRED' | 'DEFINITION_INVALID' | 'DRAFT_ADOPTION_REQUIRED' | 'CANDIDATE_STORAGE_REQUIRED' | 'DRAFT_AUTHORIZATION_REQUIRED' | 'PROVENANCE_REQUIRED' | 'OWNER_MISMATCH' | 'ACL_CHANGE_FORBIDDEN' | 'WRITER_NOT_AUTHORIZED' | 'SCOPE_VIOLATION' | 'PROJECT_MISMATCH' | 'ADOPTION_TARGET_CHANGED' | 'ADOPTER_IS_OWNER' | 'ALREADY_ADOPTED' | 'DEFINITION_NOT_READY' | 'AUTHORITY_REQUIRED';
13
14
  export interface FoundationGraphWriteIssue {
14
15
  readonly code: FoundationGraphWriteIssueCode;
15
16
  readonly path: string;
@@ -88,3 +89,62 @@ export declare function validateFoundationGraphWrite(input: unknown): Foundation
88
89
  export declare function normalizeFoundationGraphWrite(input: unknown): FoundationGraphWriteResult;
89
90
  /** Descriptive alias for callers that use the validation/normalization pair. */
90
91
  export declare const validateAndNormalizeFoundationGraphWrite: typeof normalizeFoundationGraphWrite;
92
+ /**
93
+ * ADR-015: adopt the current Foundation draft for judgment and evaluation.
94
+ *
95
+ * The adopter names the revision and digest they reviewed. The contract only
96
+ * succeeds when that is the current Graph revision, the adopter is not the
97
+ * draft's owner, and the unchanged definition passes the judgment and
98
+ * evaluation checks. The adopted definition is the next revision with the same
99
+ * content; only the adoption state, authorized uses, storage class and an
100
+ * appended decision provenance entry change. Whether the adopter holds the
101
+ * required organization authority is decided by the host before this call and
102
+ * passed in as `adoption.authorityRef`.
103
+ */
104
+ export interface FoundationGraphAdoptionInput {
105
+ readonly context: FoundationGraphWriteContext;
106
+ /** The current Graph row, as read under the adopter's access in the same transaction. */
107
+ readonly row: FoundationGraphWriteRow;
108
+ /** What the adopter reviewed. */
109
+ readonly expected: {
110
+ readonly revision: string;
111
+ readonly digest: string;
112
+ };
113
+ readonly adoption: {
114
+ readonly authorityRef: string;
115
+ readonly adoptedAt: string;
116
+ };
117
+ }
118
+ export interface FoundationGraphAdoptionRecord {
119
+ readonly adoptedBy: string;
120
+ readonly authorityRef: string;
121
+ readonly adoptedRevision: string;
122
+ readonly adoptedDigest: string;
123
+ readonly adoptedAt: string;
124
+ }
125
+ export interface FoundationGraphAdoptionResult {
126
+ readonly contractVersion: typeof FOUNDATION_GRAPH_ADOPTION_CONTRACT_VERSION;
127
+ readonly operation: 'adopt';
128
+ readonly projectCode: string;
129
+ readonly row: {
130
+ readonly id: string;
131
+ readonly type: FoundationType;
132
+ readonly revision: string;
133
+ readonly projectCode: string;
134
+ readonly payload: {
135
+ readonly foundation: FoundationDefinition;
136
+ readonly adoption: FoundationGraphAdoptionRecord;
137
+ };
138
+ };
139
+ readonly definition: FoundationDefinition;
140
+ readonly adoption: FoundationGraphAdoptionRecord;
141
+ }
142
+ export interface FoundationGraphAdoptionValidationResult {
143
+ readonly valid: boolean;
144
+ readonly status: 'valid' | 'invalid';
145
+ readonly issues: readonly FoundationGraphWriteIssue[];
146
+ readonly normalized?: FoundationGraphAdoptionResult;
147
+ }
148
+ export declare function validateFoundationGraphAdoption(input: unknown): FoundationGraphAdoptionValidationResult;
149
+ /** Normalize a valid adoption or throw with the complete validation issues. */
150
+ export declare function normalizeFoundationGraphAdoption(input: unknown): FoundationGraphAdoptionResult;
@@ -1,4 +1,4 @@
1
- import { cloneFoundationDefinition, nextFoundationRevision } from './foundation-catalog.js';
1
+ import { cloneFoundationDefinition, digestFoundationDefinition, nextFoundationRevision } from './foundation-catalog.js';
2
2
  import { validateFoundationDefinition } from './ontology-foundation.js';
3
3
  /**
4
4
  * Contract for preparing a Foundation draft for the canonical Graph writer.
@@ -12,6 +12,10 @@ export const FOUNDATION_GRAPH_WRITE_CONTRACT_VERSION = 'foundation-graph-draft-w
12
12
  const FOUNDATION_TYPES = ['objective', 'variable', 'model', 'constraint'];
13
13
  const REVISION_PATTERN = /^[1-9]\d*$/u;
14
14
  const FOUNDATION_USES_DRAFT = ['draft'];
15
+ /** ADR-015 F2: an adopted definition may be used for judgment and evaluation, never execution. */
16
+ const FOUNDATION_USES_ADOPTED = ['draft', 'judgment', 'evaluation'];
17
+ const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/u;
18
+ export const FOUNDATION_GRAPH_ADOPTION_CONTRACT_VERSION = 'foundation-graph-adoption.v1';
15
19
  /**
16
20
  * Error thrown by the throwing normalizer. The non-throwing validator should
17
21
  * be used at HTTP/MCP boundaries when callers need to render all issues.
@@ -207,6 +211,117 @@ export function normalizeFoundationGraphWrite(input) {
207
211
  }
208
212
  /** Descriptive alias for callers that use the validation/normalization pair. */
209
213
  export const validateAndNormalizeFoundationGraphWrite = normalizeFoundationGraphWrite;
214
+ export function validateFoundationGraphAdoption(input) {
215
+ const issues = [];
216
+ const fail = () => ({ valid: false, status: 'invalid', issues });
217
+ if (!isRecord(input) || !isRecord(input.context) || !isRecord(input.row) || !isRecord(input.expected) || !isRecord(input.adoption)) {
218
+ issues.push({ code: 'INVALID_INPUT', path: 'input', message: 'Adoption needs context, row, expected and adoption objects.' });
219
+ return fail();
220
+ }
221
+ const context = input.context;
222
+ const row = input.row;
223
+ const expected = input.expected;
224
+ const adoption = input.adoption;
225
+ validateContext(context, issues);
226
+ validateGraphRow(row, issues);
227
+ if (isNonEmptyString(row.projectCode) && isNonEmptyString(context.projectCode) && row.projectCode !== context.projectCode) {
228
+ issues.push({ code: 'PROJECT_MISMATCH', path: 'row.projectCode', message: 'Graph row projectCode must match the trusted selected project.' });
229
+ }
230
+ if (!isCanonicalRevision(expected.revision) || typeof expected.digest !== 'string' || !DIGEST_PATTERN.test(expected.digest)) {
231
+ issues.push({ code: 'INVALID_INPUT', path: 'expected', message: 'expected needs the reviewed revision and its sha256 digest.' });
232
+ }
233
+ if (!isNonEmptyString(adoption.authorityRef)) {
234
+ issues.push({ code: 'AUTHORITY_REQUIRED', path: 'adoption.authorityRef', message: 'Adoption requires the evidence of the adopter\'s organization authority.' });
235
+ }
236
+ if (typeof adoption.adoptedAt !== 'string' || !isRfc3339(adoption.adoptedAt)) {
237
+ issues.push({ code: 'INVALID_INPUT', path: 'adoption.adoptedAt', message: 'adoption.adoptedAt must be an RFC 3339 timestamp.' });
238
+ }
239
+ if (issues.length > 0)
240
+ return fail();
241
+ const rowType = resolveRowType(row);
242
+ const current = unwrapFoundation(row.payload.foundation);
243
+ const currentResult = validateFoundationDefinition(current, { use: 'draft' });
244
+ appendDefinitionIssues(issues, 'row.payload.foundation', currentResult.issues);
245
+ if (!currentResult.valid || !isFoundationDefinitionRecord(current) || !rowType)
246
+ return fail();
247
+ if (current.id !== row.id || current.type !== rowType || current.revision !== row.revision) {
248
+ issues.push({ code: 'ROW_DEFINITION_MISMATCH', path: 'row.payload.foundation', message: 'The current definition must match the Graph row identity and revision.' });
249
+ return fail();
250
+ }
251
+ const currentDefinition = current;
252
+ const currentDigest = digestFoundationDefinition(currentDefinition);
253
+ if (expected.revision !== current.revision || expected.digest !== currentDigest) {
254
+ issues.push({
255
+ code: 'ADOPTION_TARGET_CHANGED',
256
+ path: 'expected',
257
+ message: `The reviewed revision ${String(expected.revision)} is not the current revision ${current.revision} with the same content.`
258
+ });
259
+ }
260
+ if (current.adoptionState !== 'draft') {
261
+ issues.push({ code: 'ALREADY_ADOPTED', path: 'row.payload.foundation.adoptionState', message: `Only a draft can be adopted; the current revision is ${String(current.adoptionState)}.` });
262
+ }
263
+ if (current.acl.ownerId === context.principal) {
264
+ issues.push({ code: 'ADOPTER_IS_OWNER', path: 'context.principal', message: 'The owner of a draft cannot adopt it.' });
265
+ }
266
+ validateCandidateScope(current, context, issues);
267
+ if (issues.length > 0)
268
+ return fail();
269
+ let revision;
270
+ try {
271
+ revision = nextFoundationRevision(current.revision);
272
+ }
273
+ catch {
274
+ issues.push({ code: 'REVISION_CONFLICT', path: 'row.revision', message: 'The next revision cannot be derived from the current revision.' });
275
+ return fail();
276
+ }
277
+ const authorityRef = String(adoption.authorityRef);
278
+ const adopted = cloneFoundationDefinition(currentDefinition);
279
+ adopted.revision = revision;
280
+ adopted.adoptionState = 'approved';
281
+ adopted.storage = 'ontology';
282
+ adopted.authorizedUses = [...FOUNDATION_USES_ADOPTED];
283
+ adopted.provenance = [
284
+ ...adopted.provenance,
285
+ { sourceId: `foundation-adoption:${current.id}@${current.revision}`, sourceKind: 'decision', evidenceIds: [authorityRef] }
286
+ ];
287
+ for (const use of ['judgment', 'evaluation']) {
288
+ const ready = validateFoundationDefinition(adopted, { use });
289
+ if (!ready.valid) {
290
+ for (const issue of ready.issues) {
291
+ issues.push({ code: 'DEFINITION_NOT_READY', path: `row.payload.foundation.${issue.path}`, message: `Not ready for ${use}: ${issue.message}` });
292
+ }
293
+ }
294
+ }
295
+ if (issues.length > 0)
296
+ return fail();
297
+ const record = {
298
+ adoptedBy: context.principal,
299
+ authorityRef,
300
+ adoptedRevision: current.revision,
301
+ adoptedDigest: currentDigest,
302
+ adoptedAt: String(adoption.adoptedAt)
303
+ };
304
+ return {
305
+ valid: true,
306
+ status: 'valid',
307
+ issues: [],
308
+ normalized: {
309
+ contractVersion: FOUNDATION_GRAPH_ADOPTION_CONTRACT_VERSION,
310
+ operation: 'adopt',
311
+ projectCode: context.projectCode,
312
+ row: { id: current.id, type: rowType, revision, projectCode: context.projectCode, payload: { foundation: adopted, adoption: record } },
313
+ definition: adopted,
314
+ adoption: record
315
+ }
316
+ };
317
+ }
318
+ /** Normalize a valid adoption or throw with the complete validation issues. */
319
+ export function normalizeFoundationGraphAdoption(input) {
320
+ const result = validateFoundationGraphAdoption(input);
321
+ if (!result.valid || !result.normalized)
322
+ throw new FoundationGraphWriteError(result.issues);
323
+ return result.normalized;
324
+ }
210
325
  function validateContext(context, issues) {
211
326
  if (!isNonEmptyString(context.principal)) {
212
327
  issues.push({ code: 'INVALID_INPUT', path: 'context.principal', message: 'context.principal must be a non-empty trusted identifier.' });
@@ -0,0 +1,40 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { type JudgmentProblemReferenceProvider, type JudgmentProblemSnapshotAccessProvider } from './judgment-problem-snapshot.js';
3
+ type MaybePromise<T> = T | Promise<T>;
4
+ /** Identity resolved by the host after authentication. Request bodies never supply it. */
5
+ export interface TrustedJudgmentProblemSnapshotRequestContext {
6
+ principal: string;
7
+ /** A non-empty value means that the host already verified the mutation origin. */
8
+ verifiedMutationOrigin?: string;
9
+ }
10
+ /**
11
+ * The host binds the reference resolution (for example the Foundation store
12
+ * and its own resolvers for authority, resource and deadline) to the trusted
13
+ * request context. The access provider defaults to the snapshot read policy.
14
+ */
15
+ export type JudgmentProblemSnapshotProviderFactory = (context: TrustedJudgmentProblemSnapshotRequestContext) => MaybePromise<{
16
+ referenceProvider: JudgmentProblemReferenceProvider;
17
+ accessProvider?: JudgmentProblemSnapshotAccessProvider;
18
+ }>;
19
+ export interface JudgmentProblemSnapshotHttpOptions {
20
+ /** Artifact root of the immutable snapshot store. */
21
+ root: string;
22
+ providerFactory: JudgmentProblemSnapshotProviderFactory;
23
+ basePath?: string;
24
+ bodyLimitBytes?: number;
25
+ /** Required for hosts that do not attach verifiedMutationOrigin to context. */
26
+ verifyMutationRequest?: (input: {
27
+ request: IncomingMessage;
28
+ context: TrustedJudgmentProblemSnapshotRequestContext;
29
+ }) => MaybePromise<boolean>;
30
+ }
31
+ /** A composable route handler. `false` means that the request is outside this route. */
32
+ export type JudgmentProblemSnapshotHttpHandler = (request: IncomingMessage, response: ServerResponse, context: TrustedJudgmentProblemSnapshotRequestContext | null) => Promise<boolean>;
33
+ /**
34
+ * Saves and reads immutable judgment problem snapshots over HTTP. The host
35
+ * authenticates, binds the principal, and supplies the reference resolution;
36
+ * this handler never takes identity from the request body.
37
+ */
38
+ export declare function createJudgmentProblemSnapshotHttpHandler(options: JudgmentProblemSnapshotHttpOptions): JudgmentProblemSnapshotHttpHandler;
39
+ export declare const createJudgmentProblemSnapshotRoute: typeof createJudgmentProblemSnapshotHttpHandler;
40
+ export {};
@@ -0,0 +1,255 @@
1
+ import { JudgmentProblemSnapshotError, loadJudgmentProblemSnapshot, saveJudgmentProblemSnapshot, } from './judgment-problem-snapshot.js';
2
+ const DEFAULT_BODY_LIMIT_BYTES = 1024 * 1024;
3
+ const DEFAULT_BASE_PATH = '/judgment-problem-snapshots';
4
+ const snapshotIdPattern = /^sha256:[0-9a-f]{64}$/u;
5
+ class HttpInputError extends Error {
6
+ }
7
+ class BodyLimitError extends Error {
8
+ constructor() {
9
+ super('Request body exceeds the configured limit');
10
+ }
11
+ }
12
+ function requestPath(request) {
13
+ try {
14
+ return new URL(request.url ?? '/', 'http://localhost').pathname;
15
+ }
16
+ catch {
17
+ return '/';
18
+ }
19
+ }
20
+ function headerValue(request, name) {
21
+ const value = request.headers[name.toLowerCase()];
22
+ return Array.isArray(value) ? value[0] : value;
23
+ }
24
+ function writeJson(response, statusCode, body, headers = {}) {
25
+ if (response.headersSent)
26
+ return;
27
+ response.statusCode = statusCode;
28
+ response.setHeader('Content-Type', 'application/json');
29
+ for (const [name, value] of Object.entries(headers))
30
+ response.setHeader(name, value);
31
+ response.end(JSON.stringify(body));
32
+ }
33
+ function writeError(response, statusCode, code, message, headers = {}) {
34
+ writeJson(response, statusCode, { error: { code, message } }, headers);
35
+ }
36
+ async function readJsonBody(request, limitBytes) {
37
+ const contentLength = Number(headerValue(request, 'content-length'));
38
+ if (Number.isFinite(contentLength) && contentLength > limitBytes) {
39
+ request.resume();
40
+ throw new BodyLimitError();
41
+ }
42
+ const chunks = [];
43
+ let totalBytes = 0;
44
+ await new Promise((resolve, reject) => {
45
+ let settled = false;
46
+ const finish = (error) => {
47
+ if (settled)
48
+ return;
49
+ settled = true;
50
+ request.removeListener('data', onData);
51
+ request.removeListener('end', onEnd);
52
+ request.removeListener('error', onError);
53
+ request.removeListener('aborted', onError);
54
+ if (error)
55
+ reject(error);
56
+ else
57
+ resolve();
58
+ };
59
+ const onData = (chunk) => {
60
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
61
+ totalBytes += buffer.byteLength;
62
+ if (totalBytes > limitBytes) {
63
+ request.resume();
64
+ finish(new BodyLimitError());
65
+ return;
66
+ }
67
+ chunks.push(buffer);
68
+ };
69
+ const onEnd = () => finish();
70
+ const onError = () => finish(new HttpInputError('Request body could not be read'));
71
+ request.on('data', onData);
72
+ request.once('end', onEnd);
73
+ request.once('error', onError);
74
+ request.once('aborted', onError);
75
+ });
76
+ if (chunks.length === 0)
77
+ throw new HttpInputError('Request body must be a JSON object');
78
+ try {
79
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'));
80
+ }
81
+ catch {
82
+ throw new HttpInputError('Request body must contain valid JSON');
83
+ }
84
+ }
85
+ function normalizeBasePath(basePath) {
86
+ const value = basePath ?? DEFAULT_BASE_PATH;
87
+ if (!value.startsWith('/'))
88
+ throw new TypeError('basePath must be an absolute URL path');
89
+ const normalized = value.replace(/\/+$/u, '');
90
+ if (normalized.length === 0)
91
+ throw new TypeError('basePath must not be root');
92
+ return normalized;
93
+ }
94
+ function matchRoute(pathname, basePath) {
95
+ const normalized = pathname.replace(/\/+$/u, '') || '/';
96
+ if (normalized === basePath)
97
+ return { operation: 'save' };
98
+ if (!normalized.startsWith(`${basePath}/`))
99
+ return null;
100
+ const rest = normalized.slice(basePath.length + 1);
101
+ if (rest.length === 0 || rest.includes('/'))
102
+ return { operation: 'unknown' };
103
+ try {
104
+ return { operation: 'read', snapshotId: decodeURIComponent(rest) };
105
+ }
106
+ catch {
107
+ return { operation: 'read', snapshotId: null };
108
+ }
109
+ }
110
+ function isPlainRecord(value) {
111
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
112
+ && Object.getPrototypeOf(value) === Object.prototype;
113
+ }
114
+ function parseSaveBody(body) {
115
+ if (!isPlainRecord(body))
116
+ throw new HttpInputError('Request body must be a JSON object');
117
+ const keys = Object.keys(body);
118
+ if (keys.length !== 1 || keys[0] !== 'snapshot')
119
+ throw new HttpInputError('Request body must contain only snapshot');
120
+ if (!isPlainRecord(body.snapshot))
121
+ throw new HttpInputError('snapshot must be a JSON object');
122
+ return body.snapshot;
123
+ }
124
+ const STATUS_BY_CODE = {
125
+ invalid_request: 400,
126
+ missing_reference: 422,
127
+ not_applicable: 422,
128
+ unresolved_constraint: 422,
129
+ unauthorized: 403,
130
+ not_found: 404,
131
+ conflict: 409,
132
+ integrity_mismatch: 409,
133
+ storage_io_error: 503,
134
+ };
135
+ function snapshotError(error) {
136
+ if (error instanceof JudgmentProblemSnapshotError) {
137
+ return { status: STATUS_BY_CODE[error.code] ?? 500, code: error.code, message: error.message };
138
+ }
139
+ return { status: 500, code: 'internal_error', message: 'Judgment problem snapshot request failed' };
140
+ }
141
+ function assertContext(context) {
142
+ if (typeof context.principal !== 'string' || context.principal.trim().length === 0 || context.principal.length > 512) {
143
+ throw new HttpInputError('Authenticated principal is required');
144
+ }
145
+ }
146
+ async function mutationVerified(options, request, context) {
147
+ if (typeof context.verifiedMutationOrigin === 'string' && context.verifiedMutationOrigin.trim().length > 0)
148
+ return true;
149
+ if (!options.verifyMutationRequest)
150
+ return false;
151
+ try {
152
+ return (await options.verifyMutationRequest({ request, context })) === true;
153
+ }
154
+ catch {
155
+ return false;
156
+ }
157
+ }
158
+ /**
159
+ * Saves and reads immutable judgment problem snapshots over HTTP. The host
160
+ * authenticates, binds the principal, and supplies the reference resolution;
161
+ * this handler never takes identity from the request body.
162
+ */
163
+ export function createJudgmentProblemSnapshotHttpHandler(options) {
164
+ if (!options || typeof options !== 'object')
165
+ throw new TypeError('options are required');
166
+ if (typeof options.root !== 'string' || options.root.trim().length === 0)
167
+ throw new TypeError('root must be a non-empty path');
168
+ if (typeof options.providerFactory !== 'function')
169
+ throw new TypeError('providerFactory is required');
170
+ const basePath = normalizeBasePath(options.basePath);
171
+ const bodyLimitBytes = options.bodyLimitBytes ?? DEFAULT_BODY_LIMIT_BYTES;
172
+ if (!Number.isInteger(bodyLimitBytes) || bodyLimitBytes <= 0)
173
+ throw new TypeError('bodyLimitBytes must be a positive integer');
174
+ return async (request, response, context) => {
175
+ const route = matchRoute(requestPath(request), basePath);
176
+ if (!route)
177
+ return false;
178
+ if (!context) {
179
+ writeError(response, 401, 'unauthorized', 'Authenticated judgment problem snapshot context is required');
180
+ return true;
181
+ }
182
+ try {
183
+ assertContext(context);
184
+ }
185
+ catch (error) {
186
+ writeError(response, 401, 'unauthorized', error instanceof Error ? error.message : 'Authenticated principal is required');
187
+ return true;
188
+ }
189
+ if (route.operation === 'unknown') {
190
+ writeError(response, 404, 'not_found', 'Judgment problem snapshot route was not found');
191
+ return true;
192
+ }
193
+ const method = (request.method ?? 'GET').toUpperCase();
194
+ const allowed = route.operation === 'save' ? 'POST' : 'GET';
195
+ if (method !== allowed) {
196
+ writeError(response, 405, 'method_not_allowed', 'Method is not allowed for this judgment problem snapshot route', { Allow: allowed });
197
+ return true;
198
+ }
199
+ if (route.operation === 'read') {
200
+ if (!route.snapshotId || !snapshotIdPattern.test(route.snapshotId)) {
201
+ writeError(response, 400, 'invalid_request', 'snapshot_id must be sha256:<64 lowercase hex>');
202
+ return true;
203
+ }
204
+ try {
205
+ const providers = await options.providerFactory(context);
206
+ const snapshot = await loadJudgmentProblemSnapshot({
207
+ root: options.root,
208
+ snapshot_id: route.snapshotId,
209
+ access: { principal: context.principal },
210
+ reference_resolution: 'current',
211
+ referenceProvider: providers.referenceProvider,
212
+ ...(providers.accessProvider ? { accessProvider: providers.accessProvider } : {}),
213
+ });
214
+ writeJson(response, 200, { action: 'read', snapshotId: route.snapshotId, result: snapshot });
215
+ }
216
+ catch (error) {
217
+ const failure = snapshotError(error);
218
+ writeError(response, failure.status, failure.code, failure.message);
219
+ }
220
+ return true;
221
+ }
222
+ let snapshot;
223
+ try {
224
+ snapshot = parseSaveBody(await readJsonBody(request, bodyLimitBytes));
225
+ }
226
+ catch (error) {
227
+ if (error instanceof BodyLimitError)
228
+ writeError(response, 413, 'body_too_large', error.message);
229
+ else
230
+ writeError(response, 400, 'invalid_request', error instanceof Error ? error.message : 'Request body is invalid');
231
+ return true;
232
+ }
233
+ if (!await mutationVerified(options, request, context)) {
234
+ writeError(response, 403, 'mutation_origin_unverified', 'Mutation origin could not be verified');
235
+ return true;
236
+ }
237
+ try {
238
+ const providers = await options.providerFactory(context);
239
+ const receipt = await saveJudgmentProblemSnapshot({
240
+ root: options.root,
241
+ snapshot,
242
+ access: { principal: context.principal },
243
+ referenceProvider: providers.referenceProvider,
244
+ ...(providers.accessProvider ? { accessProvider: providers.accessProvider } : {}),
245
+ });
246
+ writeJson(response, receipt.status === 'created' ? 201 : 200, { action: 'save', result: receipt });
247
+ }
248
+ catch (error) {
249
+ const failure = snapshotError(error);
250
+ writeError(response, failure.status, failure.code, failure.message);
251
+ }
252
+ return true;
253
+ };
254
+ }
255
+ export const createJudgmentProblemSnapshotRoute = createJudgmentProblemSnapshotHttpHandler;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unson/brainbase-mcp",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "Local-first Brainbase judgment OS that lets supported AI agents reuse a user's approved goals, priorities, decisions, and judgment criteria.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -155,6 +155,10 @@
155
155
  "types": "./dist/judgment-problem-snapshot.d.ts",
156
156
  "import": "./dist/judgment-problem-snapshot.js"
157
157
  },
158
+ "./judgment-problem-snapshot-http": {
159
+ "types": "./dist/judgment-problem-snapshot-http.d.ts",
160
+ "import": "./dist/judgment-problem-snapshot-http.js"
161
+ },
158
162
  "./personal-knowledge": {
159
163
  "types": "./dist/personal-knowledge.d.ts",
160
164
  "import": "./dist/personal-knowledge.js"
@@ -348,5 +352,5 @@
348
352
  "engines": {
349
353
  "node": ">=20"
350
354
  },
351
- "gitHead": "084154257b751ba7bf6b303aa889c42ac551c9b9"
355
+ "gitHead": "4c9f27c5bc49aed6cb9132c442739865a41f8cc5"
352
356
  }