@unson/brainbase-mcp 0.11.0 → 0.13.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.
@@ -154,7 +154,25 @@
154
154
  "entity_id": { "type": "string", "minLength": 1 },
155
155
  "application": { "type": "string", "minLength": 1 },
156
156
  "layer": { "enum": ["philosophy", "objective", "world_model", "method", "constraint", "other"] },
157
- "version": { "type": ["string", "null"] }
157
+ "version": { "type": ["string", "null"] },
158
+ "source": {
159
+ "oneOf": [
160
+ { "type": "null" },
161
+ { "$ref": "#/$defs/basis_source" }
162
+ ]
163
+ }
164
+ }
165
+ },
166
+ "basis_source": {
167
+ "type": "object",
168
+ "additionalProperties": false,
169
+ "required": ["kind", "entity_id", "entity_type"],
170
+ "properties": {
171
+ "kind": { "const": "local_graph" },
172
+ "entity_id": { "type": "string", "minLength": 1 },
173
+ "entity_type": { "enum": ["person", "org", "project", "decision"] },
174
+ "version": { "type": ["string", "null"] },
175
+ "digest": { "type": ["string", "null"] }
158
176
  }
159
177
  },
160
178
  "inheritance_source": {
@@ -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;
@@ -14,12 +14,27 @@ export interface JudgmentValueProofArtifactRef {
14
14
  }
15
15
  /** Which part of the organization's judgment a basis came from. Absent means not recorded. */
16
16
  export type JudgmentValueProofBasisLayer = 'philosophy' | 'objective' | 'world_model' | 'method' | 'constraint' | 'other';
17
+ /**
18
+ * A source that the local Web host can read and open. This is deliberately
19
+ * not a URL: the host owns the reader and keeps navigation same-origin.
20
+ */
21
+ export type JudgmentValueProofBasisSourceKind = 'local_graph';
22
+ export type JudgmentValueProofBasisSourceEntityType = 'person' | 'org' | 'project' | 'decision';
23
+ export interface JudgmentValueProofBasisSource {
24
+ kind: JudgmentValueProofBasisSourceKind;
25
+ entity_id: string;
26
+ entity_type: JudgmentValueProofBasisSourceEntityType;
27
+ version?: string | null;
28
+ digest?: string | null;
29
+ }
17
30
  export interface JudgmentValueProofBasis {
18
31
  entity_id: string;
19
32
  application: string;
20
33
  layer?: JudgmentValueProofBasisLayer;
21
34
  /** Version of the referenced entity used for this judgment. */
22
35
  version?: string | null;
36
+ /** Optional, typed read contract for a source-bearing proof. */
37
+ source?: JudgmentValueProofBasisSource | null;
23
38
  }
24
39
  export interface JudgmentValueProofInheritanceSource {
25
40
  kind: 'judgment' | 'method' | 'decision' | 'other';
@@ -81,6 +81,23 @@ export function validateJudgmentValueProof(proof) {
81
81
  if (entry.layer !== undefined && !layers.includes(entry.layer)) {
82
82
  throw new TypeError(`unsupported decision.basis[].layer: ${String(entry.layer)}`);
83
83
  }
84
+ const source = entry.source;
85
+ if (source === undefined || source === null)
86
+ continue;
87
+ if (source.kind !== 'local_graph') {
88
+ throw new TypeError('decision.basis[].source.kind must be local_graph');
89
+ }
90
+ requiredText(source.entity_id, 'decision.basis[].source.entity_id');
91
+ if (source.entity_id !== entry.entity_id) {
92
+ throw new TypeError('decision.basis[].source.entity_id must match entity_id');
93
+ }
94
+ if (!['person', 'org', 'project', 'decision'].includes(source.entity_type)) {
95
+ throw new TypeError(`unsupported decision.basis[].source.entity_type: ${String(source.entity_type)}`);
96
+ }
97
+ if (source.version !== undefined && source.version !== null)
98
+ requiredText(source.version, 'decision.basis[].source.version');
99
+ if (source.digest !== undefined && source.digest !== null)
100
+ requiredText(source.digest, 'decision.basis[].source.digest');
84
101
  }
85
102
  const kind = proof.decision.judgment_kind;
86
103
  if (kind !== undefined && kind !== null) {
@@ -3,7 +3,7 @@ export declare const KNOWLEDGE_PROMOTION_CONTRACT_VERSION: "knowledge_promotion.
3
3
  export declare const NORMALIZED_PROMOTION_SCHEMA_VERSION: "personal_knowledge_normalized.v1";
4
4
  export type PromotionApprovalKind = 'personal_share' | 'organization_candidate';
5
5
  export type PromotionSourceKind = 'personal_knowledge_v1' | 'organization_candidate';
6
- export type PromotionStatus = 'pending_owner_approval' | 'pending_org_review' | 'owner_rejected' | 'org_rejected' | 'org_accepted' | 'source_stale';
6
+ export type PromotionStatus = 'pending_owner_approval' | 'pending_org_review' | 'owner_rejected' | 'org_rejected' | 'org_accepted' | 'source_stale' | 'source_withdrawn';
7
7
  export type PromotionDecision = 'approve' | 'reject';
8
8
  export type PromotionReviewAction = 'request' | 'owner_consent' | 'organization_review';
9
9
  export declare class KnowledgePromotionError extends Error {
@@ -72,8 +72,8 @@ export interface PromotionSourceRef {
72
72
  evidence_hash: string;
73
73
  }
74
74
  export interface PromotionDecisionRecord {
75
- action: 'owner_consent' | 'organization_review';
76
- decision: PromotionDecision;
75
+ action: 'owner_consent' | 'organization_review' | 'source_withdrawal';
76
+ decision: PromotionDecision | 'withdraw';
77
77
  actor_person_id: string;
78
78
  decided_at: string;
79
79
  receipt_id: string | null;
@@ -181,6 +181,36 @@ export interface KnowledgePromotionLineage {
181
181
  };
182
182
  created_at: string;
183
183
  }
184
+ /** Append-only record of a share cancelled because the owner retracted the source memory. */
185
+ export interface KnowledgePromotionWithdrawal {
186
+ withdrawal_id: string;
187
+ contract_version: typeof KNOWLEDGE_PROMOTION_CONTRACT_VERSION;
188
+ approval_kind: PromotionApprovalKind;
189
+ promotion_request_id: string;
190
+ source: PromotionSourceRef;
191
+ owner_person_id: string | null;
192
+ organization_id: string;
193
+ project_code: string;
194
+ previous_status: PromotionStatus;
195
+ organization_event_id: string | null;
196
+ graph_entity_id: string | null;
197
+ graph_withdrawn: boolean;
198
+ graph_outcome: 'not_promoted' | 'retracted' | 'projection_replaced';
199
+ withdrawn_by_person_id: string;
200
+ reason: string | null;
201
+ created_at: string;
202
+ }
203
+ export interface PromotedEntityWithdrawalInput {
204
+ request_id: string;
205
+ graph_entity_id: string;
206
+ organization_event_id: string | null;
207
+ organization_id: string;
208
+ project_code: string;
209
+ normalized_payload_hash: string;
210
+ withdrawal_id: string;
211
+ withdrawn_at: string;
212
+ reason: string | null;
213
+ }
184
214
  export interface KnowledgePromotionStore {
185
215
  transaction<T>(work: (tx: unknown) => Promise<T>, options?: {
186
216
  principal: KnowledgePromotionPrincipal;
@@ -202,6 +232,9 @@ export interface KnowledgePromotionStore {
202
232
  organization_id: string;
203
233
  project_code: string;
204
234
  }, ctx: PromotionContext): Promise<void>;
235
+ /** Requests of one source visible to the caller, locked so a concurrent decision is ordered with the withdrawal. */
236
+ listRequestsBySource?(sourceKind: PromotionSourceKind, sourceId: string, ctx: PromotionContext): Promise<KnowledgePromotionRequest[]>;
237
+ recordWithdrawal?(withdrawal: KnowledgePromotionWithdrawal, ctx: PromotionContext): Promise<void>;
205
238
  }
206
239
  export interface KnowledgePromotionSources {
207
240
  readPersonalKnowledgeEvent?(eventId: string, ctx: PromotionContext): Promise<PersonalKnowledgeSourceRecord | null>;
@@ -249,6 +282,14 @@ export interface KnowledgePromotionGraphWriter {
249
282
  commitNormalizedPromotion(mutation: NormalizedGraphMutation, ctx: PromotionContext): Promise<{
250
283
  id: string;
251
284
  } | null>;
285
+ /**
286
+ * Mark the fact promoted by the request as retracted and out of retrieval. `projection_replaced` means
287
+ * another write has replaced the fact since, so it was left as it is.
288
+ */
289
+ withdrawPromotedEntity?(input: PromotedEntityWithdrawalInput, ctx: PromotionContext): Promise<{
290
+ id: string;
291
+ outcome: 'retracted' | 'projection_replaced';
292
+ } | null>;
252
293
  }
253
294
  export interface KnowledgePromotionPorts {
254
295
  store: KnowledgePromotionStore;
@@ -400,6 +441,23 @@ export declare class KnowledgePromotionService {
400
441
  principal: KnowledgePromotionPrincipal;
401
442
  authority?: KnowledgePromotionAuthority | null;
402
443
  }): Promise<KnowledgePromotionRequest>;
444
+ /**
445
+ * D4: the owner retracted a Personal Knowledge v1 memory. Cancel its shares that wait for a decision and
446
+ * withdraw an already promoted Graph fact from future retrieval. The approval lineage, the organization
447
+ * event, and the receipts stay. Retraction is the owner's right, so the organization review policy is
448
+ * not consulted and lost project access does not block it. Pass `tx` to commit with the retraction itself.
449
+ */
450
+ withdrawPersonalShares(input: {
451
+ personal_event_id: string;
452
+ reason?: string | null;
453
+ }, context: {
454
+ principal: KnowledgePromotionPrincipal;
455
+ tx?: unknown;
456
+ }): Promise<{
457
+ personal_event_id: string;
458
+ withdrawals: KnowledgePromotionWithdrawal[];
459
+ }>;
460
+ private withdrawRequest;
403
461
  private requirePolicy;
404
462
  private draftRequest;
405
463
  private authorize;
@@ -10,6 +10,10 @@
10
10
  * task candidate, ...) → organization approval → Graph. It never takes the
11
11
  * shape of an owner consent.
12
12
  *
13
+ * When the owner retracts the source memory, `withdrawPersonalShares` cancels
14
+ * the shares still waiting for a decision and withdraws an already promoted
15
+ * Graph fact from future retrieval, keeping the approval lineage and audit.
16
+ *
13
17
  * This module owns normalization, receipts, evidence, lineage, the Graph
14
18
  * mutation, and the state machine. Storage, the source readers, the knowledge
15
19
  * event kernel, the Graph writer, and the organization review policy (roles,
@@ -275,7 +279,8 @@ export function candidateApprovalReceipt(request, reviewerPersonId, reviewedAt)
275
279
  }).slice(0, 24)}`;
276
280
  }
277
281
  const DECISIONS = new Set(['approve', 'reject']);
278
- const TERMINAL = new Set(['owner_rejected', 'org_rejected', 'org_accepted', 'source_stale']);
282
+ const TERMINAL = new Set(['owner_rejected', 'org_rejected', 'org_accepted', 'source_stale', 'source_withdrawn']);
283
+ const WITHDRAWABLE = new Set(['pending_owner_approval', 'pending_org_review', 'org_accepted']);
279
284
  function sanitizePreview(value) {
280
285
  const text = String(value ?? '').trim().slice(0, 2000);
281
286
  if (!text || SECRET_OR_PRIVATE.test(text))
@@ -317,6 +322,11 @@ function requireHuman(principal) {
317
322
  function actorOf(principal) {
318
323
  return principal.actorPersonId || principal.personId;
319
324
  }
325
+ function rejectWithdrawn(request) {
326
+ if (request.status === 'source_withdrawn') {
327
+ throw new KnowledgePromotionError('knowledge_promotion_source_withdrawn', 409, { request_id: request.request_id });
328
+ }
329
+ }
320
330
  function requireProjectAccess(principal, projectCode) {
321
331
  if (!Array.isArray(principal.projectCodes) || !principal.projectCodes.includes(projectCode)) {
322
332
  throw new KnowledgePromotionError('knowledge_promotion_project_access_denied', 403);
@@ -572,6 +582,7 @@ export class KnowledgePromotionService {
572
582
  throw new KnowledgePromotionError('knowledge_promotion_request_not_found', 404);
573
583
  }
574
584
  requireProjectAccess(principal, request.project_code);
585
+ rejectWithdrawn(request);
575
586
  const normalized = normalizePromotionPayload(request.normalized_payload).normalized;
576
587
  await this.authorize(policy, 'owner_consent', request, normalized, principal, context.authority ?? null, ctx);
577
588
  await this.claimAuthority(context.authority ?? null, request, 'owner_consent', ctx);
@@ -629,6 +640,8 @@ export class KnowledgePromotionService {
629
640
  if (request.approval_kind === 'personal_share' && request.owner_person_id === actor) {
630
641
  throw new KnowledgePromotionError('knowledge_promotion_distinct_reviewer_required', 403);
631
642
  }
643
+ // The owner retracted the source memory; neither an approval nor a retried decision may pass.
644
+ rejectWithdrawn(request);
632
645
  if (request.status === 'pending_owner_approval' || request.status === 'owner_rejected') {
633
646
  throw new KnowledgePromotionError('knowledge_promotion_owner_consent_required', 409);
634
647
  }
@@ -739,6 +752,104 @@ export class KnowledgePromotionService {
739
752
  throw new KnowledgePromotionError('knowledge_promotion_source_stale', 409, { request_id: requestId });
740
753
  return result;
741
754
  }
755
+ /**
756
+ * D4: the owner retracted a Personal Knowledge v1 memory. Cancel its shares that wait for a decision and
757
+ * withdraw an already promoted Graph fact from future retrieval. The approval lineage, the organization
758
+ * event, and the receipts stay. Retraction is the owner's right, so the organization review policy is
759
+ * not consulted and lost project access does not block it. Pass `tx` to commit with the retraction itself.
760
+ */
761
+ async withdrawPersonalShares(input, context) {
762
+ const principal = requireHuman(context.principal);
763
+ const read = this.ports.sources.readPersonalKnowledgeEvent;
764
+ if (typeof read !== 'function')
765
+ throw new KnowledgePromotionError('knowledge_promotion_personal_source_unavailable', 503);
766
+ const { listRequestsBySource, recordWithdrawal } = this.ports.store;
767
+ if (typeof listRequestsBySource !== 'function' || typeof recordWithdrawal !== 'function') {
768
+ throw new KnowledgePromotionError('knowledge_promotion_withdrawal_unavailable', 503);
769
+ }
770
+ const eventId = requireText(input?.personal_event_id, 'knowledge_promotion_source_id_required');
771
+ const reason = sanitizeReason(input?.reason);
772
+ const work = async (tx) => {
773
+ const ctx = { tx, principal };
774
+ const event = await read.call(this.ports.sources, eventId, ctx);
775
+ if (!event || event.owner_person_id !== principal.personId || event.organization_id !== principal.organizationId) {
776
+ throw new KnowledgePromotionError('knowledge_promotion_source_not_found', 404);
777
+ }
778
+ if (event.active)
779
+ throw new KnowledgePromotionError('knowledge_promotion_source_still_active', 409);
780
+ const requests = (await listRequestsBySource.call(this.ports.store, 'personal_knowledge_v1', eventId, ctx))
781
+ .filter((request) => request.approval_kind === 'personal_share'
782
+ && request.owner_person_id === principal.personId
783
+ && request.organization_id === principal.organizationId
784
+ && WITHDRAWABLE.has(request.status))
785
+ .sort((left, right) => left.request_id.localeCompare(right.request_id));
786
+ const withdrawals = [];
787
+ for (const request of requests) {
788
+ withdrawals.push(await this.withdrawRequest(request, reason, ctx));
789
+ }
790
+ return { personal_event_id: eventId, withdrawals };
791
+ };
792
+ return context.tx === undefined
793
+ ? this.ports.store.transaction(work, { principal })
794
+ : work(context.tx);
795
+ }
796
+ async withdrawRequest(request, reason, ctx) {
797
+ const withdrawnAt = this.now().toISOString();
798
+ const actor = actorOf(ctx.principal);
799
+ const withdrawalId = `kpw_${promotionSha256(`${request.request_id}:${request.source.version}:withdrawal`).slice(0, 24)}`;
800
+ let graphOutcome = 'not_promoted';
801
+ if (request.status === 'org_accepted') {
802
+ const withdraw = this.ports.graph.withdrawPromotedEntity;
803
+ if (typeof withdraw !== 'function')
804
+ throw new KnowledgePromotionError('knowledge_promotion_withdrawal_unavailable', 503);
805
+ const result = await withdraw.call(this.ports.graph, {
806
+ request_id: request.request_id,
807
+ graph_entity_id: requireText(request.graph_entity_id, 'knowledge_promotion_graph_entity_required'),
808
+ organization_event_id: request.organization_event_id,
809
+ organization_id: request.organization_id,
810
+ project_code: request.project_code,
811
+ normalized_payload_hash: request.normalized_payload_hash,
812
+ withdrawal_id: withdrawalId,
813
+ withdrawn_at: withdrawnAt,
814
+ reason,
815
+ }, ctx);
816
+ if (!result?.id || (result.outcome !== 'retracted' && result.outcome !== 'projection_replaced')) {
817
+ throw new KnowledgePromotionError('knowledge_promotion_graph_withdrawal_failed', 409, { request_id: request.request_id });
818
+ }
819
+ graphOutcome = result.outcome;
820
+ }
821
+ const updated = await this.ports.store.updateRequest(request.request_id, {
822
+ status: 'source_withdrawn',
823
+ decisions: [...request.decisions, {
824
+ action: 'source_withdrawal', decision: 'withdraw', actor_person_id: actor, decided_at: withdrawnAt,
825
+ receipt_id: withdrawalId, reason,
826
+ }],
827
+ updated_at: withdrawnAt,
828
+ }, { expectedRevision: request.revision }, ctx);
829
+ if (!updated)
830
+ throw new KnowledgePromotionError('knowledge_promotion_state_conflict', 409);
831
+ const withdrawal = {
832
+ withdrawal_id: withdrawalId,
833
+ contract_version: KNOWLEDGE_PROMOTION_CONTRACT_VERSION,
834
+ approval_kind: request.approval_kind,
835
+ promotion_request_id: request.request_id,
836
+ source: request.source,
837
+ owner_person_id: request.owner_person_id,
838
+ organization_id: request.organization_id,
839
+ project_code: request.project_code,
840
+ previous_status: request.status,
841
+ organization_event_id: request.organization_event_id,
842
+ graph_entity_id: request.graph_entity_id,
843
+ graph_withdrawn: graphOutcome === 'retracted',
844
+ graph_outcome: graphOutcome,
845
+ withdrawn_by_person_id: actor,
846
+ reason,
847
+ created_at: withdrawnAt,
848
+ };
849
+ await this.ports.store.recordWithdrawal
850
+ .call(this.ports.store, withdrawal, ctx);
851
+ return withdrawal;
852
+ }
742
853
  requirePolicy() {
743
854
  if (!this.ports.reviewPolicy || typeof this.ports.reviewPolicy.authorize !== 'function') {
744
855
  throw new KnowledgePromotionError('knowledge_promotion_review_policy_unavailable', 503);
@@ -100,10 +100,10 @@ export declare function createLocalObjectiveDefinitionBuilder(now: () => Date):
100
100
  */
101
101
  export declare function createObjectiveFoundationModule(context: LocalWebModuleContext): LocalWebModule;
102
102
  /**
103
- * Read-only World Model routes. Writes (observations, corrections, model
104
- * adoption) are not on the Web in v1. Without an approval reader an approved
105
- * adoption cannot be verified, so only the adoptions route reports
106
- * `approval_reference_unresolved`; the other routes stay readable.
103
+ * World Model routes. Reads retain the explicit absence/partial contract;
104
+ * the first-user Web entry writes only a trusted Variable definition and an
105
+ * initial Observation. Models, adoption and corrections stay out of this
106
+ * focused route until their own authority and review contract exists.
107
107
  */
108
108
  export declare function createWorldModelReadModule(context: LocalWebModuleContext): LocalWebModule;
109
109
  /**