@unson/brainbase-mcp 0.11.0 → 0.12.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": {
@@ -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
  /**
@@ -10,7 +10,7 @@ import { createFoundationRevisionStore, FoundationStoreError } from './foundatio
10
10
  import { createGraphWebHttpHandler } from './graph-web-http.js';
11
11
  import { defaultJudgmentJournalRoot } from './judgment-value-proof-review.js';
12
12
  import { nodeRequestToFetch, writeFetchResponse } from './local-web-fetch-bridge.js';
13
- import { assertSameOrigin, isLoopbackHost, LOCAL_WEB_CONTENT_SECURITY_POLICY, LocalWebHttpError, requestUrl, tokenMatches, writeHttpError, writeJson } from './local-web-security.js';
13
+ import { assertSameOrigin, isLoopbackHost, LOCAL_WEB_CONTENT_SECURITY_POLICY, LocalWebHttpError, readJsonObjectBody, requestUrl, tokenMatches, writeHttpError, writeJson } from './local-web-security.js';
14
14
  import { resolveDataDir } from './paths.js';
15
15
  import { loadPersonalOs, readPersonalOsSidecar } from './ssot.js';
16
16
  import { createValueProofReviewHttpHandler, VALUE_PROOF_REVIEW_TOKEN_HEADER } from './value-proof-review-http.js';
@@ -437,7 +437,7 @@ function worldModelError(error) {
437
437
  }
438
438
  if (error instanceof LocalWebHttpError)
439
439
  return error;
440
- return new LocalWebHttpError(500, 'internal_error', 'World model read failed');
440
+ return new LocalWebHttpError(500, 'internal_error', 'World model request failed');
441
441
  }
442
442
  async function listReadableSidecarRecords(input) {
443
443
  try {
@@ -473,11 +473,112 @@ async function listReadableSidecarRecords(input) {
473
473
  }
474
474
  return { records, unreadable: { count, codes: [...codes] } };
475
475
  }
476
+ const LOCAL_WEB_WORLD_MODEL_SCOPE_START = '1970-01-01T00:00:00.000Z';
477
+ const WORLD_MODEL_VALUE_KINDS = new Set(['number', 'boolean', 'string', 'state']);
478
+ const WORLD_MODEL_AGGREGATIONS = new Set(['none', 'sum', 'average', 'count', 'min', 'max', 'last', 'custom']);
479
+ const WORLD_MODEL_VARIABLE_FIELDS = new Set([
480
+ 'meaning', 'subject', 'valueKind', 'unit', 'aggregation', 'granularity', 'measurementMethod'
481
+ ]);
482
+ const WORLD_MODEL_OBSERVATION_FIELDS = new Set(['variableRef', 'value']);
483
+ const WORLD_MODEL_AUTHORITY_FIELDS = new Set([
484
+ 'acl', 'scope', 'storage', 'provenance', 'authorizedUses', 'ownerId', 'readerIds', 'writerIds',
485
+ 'principal', 'subjectId', 'recordedAt', 'occurredAt', 'period', 'id', 'revision', 'type'
486
+ ]);
487
+ function worldModelInvalid(message) {
488
+ return new LocalWebHttpError(400, 'invalid_input', message);
489
+ }
490
+ function assertWorldModelBodyFields(body, allowed) {
491
+ for (const key of Object.keys(body)) {
492
+ if (allowed.has(key))
493
+ continue;
494
+ if (WORLD_MODEL_AUTHORITY_FIELDS.has(key)) {
495
+ throw new LocalWebHttpError(400, 'authority_field_in_body', `${key} is decided by the local Web host`);
496
+ }
497
+ throw worldModelInvalid(`Unknown World Model field ${key}`);
498
+ }
499
+ }
500
+ function requiredWorldModelText(body, key) {
501
+ const value = body[key];
502
+ if (typeof value !== 'string' || value.trim() === '')
503
+ throw worldModelInvalid(`${key} is required`);
504
+ return value.trim();
505
+ }
506
+ function worldModelVariableFromBody(body, principal) {
507
+ assertWorldModelBodyFields(body, WORLD_MODEL_VARIABLE_FIELDS);
508
+ const valueKind = requiredWorldModelText(body, 'valueKind');
509
+ if (!WORLD_MODEL_VALUE_KINDS.has(valueKind))
510
+ throw worldModelInvalid(`Unknown valueKind ${valueKind}`);
511
+ const aggregation = requiredWorldModelText(body, 'aggregation');
512
+ if (!WORLD_MODEL_AGGREGATIONS.has(aggregation))
513
+ throw worldModelInvalid(`Unknown aggregation ${aggregation}`);
514
+ const unit = body.unit === undefined ? undefined : requiredWorldModelText(body, 'unit');
515
+ if (valueKind === 'number' && !unit)
516
+ throw worldModelInvalid('unit is required for number variables');
517
+ return {
518
+ id: `web.variable.${randomBytes(10).toString('hex')}`,
519
+ type: 'variable',
520
+ revision: '1',
521
+ meaning: requiredWorldModelText(body, 'meaning'),
522
+ epistemicState: 'unverified',
523
+ adoptionState: 'draft',
524
+ authorizedUses: ['draft', 'judgment', 'evaluation'],
525
+ acl: { ownerId: principal, visibility: 'private', readerIds: [], writerIds: [] },
526
+ storage: 'ontology',
527
+ provenance: [{ sourceId: LOCAL_WEB_PROVENANCE_SOURCE_ID, sourceKind: 'observation', evidenceIds: [] }],
528
+ scope: { subjectIds: [principal], validFrom: LOCAL_WEB_WORLD_MODEL_SCOPE_START },
529
+ subject: requiredWorldModelText(body, 'subject'),
530
+ valueKind: valueKind,
531
+ ...(unit ? { unit } : {}),
532
+ aggregation: aggregation,
533
+ granularity: requiredWorldModelText(body, 'granularity'),
534
+ measurementMethod: requiredWorldModelText(body, 'measurementMethod'),
535
+ // The local host stamps the current date into observations; a broad owner-only
536
+ // validity window avoids asking the first-time user for hidden scope fields.
537
+ };
538
+ }
539
+ function worldModelReference(value) {
540
+ if (!value || typeof value !== 'object' || Array.isArray(value))
541
+ throw worldModelInvalid('variableRef is required');
542
+ const reference = value;
543
+ if (reference.type !== 'variable' || typeof reference.id !== 'string' || reference.id.trim() === ''
544
+ || typeof reference.revision !== 'string' || !/^[1-9]\d*$/u.test(reference.revision)) {
545
+ throw worldModelInvalid('variableRef must reference a variable revision');
546
+ }
547
+ if (Object.keys(reference).some((key) => !['id', 'type', 'revision'].includes(key))) {
548
+ throw worldModelInvalid('variableRef contains an unsupported field');
549
+ }
550
+ return { id: reference.id.trim(), type: 'variable', revision: reference.revision };
551
+ }
552
+ function worldModelObservationFromBody(body, principal, now) {
553
+ assertWorldModelBodyFields(body, WORLD_MODEL_OBSERVATION_FIELDS);
554
+ if (!Object.hasOwn(body, 'value') || body.value === null || body.value === undefined)
555
+ throw worldModelInvalid('value is required');
556
+ const value = body.value;
557
+ if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
558
+ throw worldModelInvalid('value must be a string, number, or boolean');
559
+ }
560
+ if (typeof value === 'number' && !Number.isFinite(value)) {
561
+ throw worldModelInvalid('value must be a finite number');
562
+ }
563
+ const nowIso = now.toISOString();
564
+ const day = nowIso.slice(0, 10);
565
+ const observationId = `web.observation.${randomBytes(10).toString('hex')}`;
566
+ return {
567
+ id: observationId,
568
+ variableRef: worldModelReference(body.variableRef),
569
+ subjectId: principal,
570
+ value,
571
+ occurredAt: nowIso,
572
+ period: { from: `${day}T00:00:00.000Z`, until: `${day}T23:59:59.999Z` },
573
+ recordedAt: nowIso,
574
+ sourceRef: { sourceId: LOCAL_WEB_PROVENANCE_SOURCE_ID, sourceKind: 'observation', evidenceIds: [observationId] }
575
+ };
576
+ }
476
577
  /**
477
- * Read-only World Model routes. Writes (observations, corrections, model
478
- * adoption) are not on the Web in v1. Without an approval reader an approved
479
- * adoption cannot be verified, so only the adoptions route reports
480
- * `approval_reference_unresolved`; the other routes stay readable.
578
+ * World Model routes. Reads retain the explicit absence/partial contract;
579
+ * the first-user Web entry writes only a trusted Variable definition and an
580
+ * initial Observation. Models, adoption and corrections stay out of this
581
+ * focused route until their own authority and review contract exists.
481
582
  */
482
583
  export function createWorldModelReadModule(context) {
483
584
  const foundationStore = createFoundationRevisionStore({ dataDir: context.dataDir });
@@ -519,17 +620,39 @@ export function createWorldModelReadModule(context) {
519
620
  writeJson(response, 404, { error: { code: 'not_found', message: 'Not found' } });
520
621
  return true;
521
622
  }
522
- if (request.method !== 'GET') {
523
- writeJson(response, 405, { error: { code: 'method_not_allowed', message: 'The world model is read-only here' } });
524
- return true;
525
- }
526
623
  const graph = await readLocalGraphState(context.dataDir);
527
624
  if (graph.status !== 'ready' || !graph.ownerId) {
528
625
  writeGraphUnavailable(response, graph, context.dataDir);
529
626
  return true;
530
627
  }
628
+ const principal = { principal: graph.ownerId };
629
+ if (request.method === 'POST' && (resource === 'variables' || resource === 'observations')) {
630
+ try {
631
+ const body = await readJsonObjectBody(request, LOCAL_WEB_FOUNDATION_BODY_LIMIT_BYTES);
632
+ if (resource === 'variables') {
633
+ const definition = worldModelVariableFromBody(body, graph.ownerId);
634
+ const reference = await worldModel.createVariable(definition, principal);
635
+ writeJson(response, 201, {
636
+ reference: { id: reference.id, type: reference.type, revision: reference.revision },
637
+ digest: reference.digest
638
+ });
639
+ }
640
+ else {
641
+ const observation = await worldModel.saveObservation(worldModelObservationFromBody(body, graph.ownerId, context.now()), principal);
642
+ writeJson(response, 201, { observation });
643
+ }
644
+ }
645
+ catch (error) {
646
+ writeHttpError(response, worldModelError(error));
647
+ }
648
+ return true;
649
+ }
650
+ if (request.method !== 'GET') {
651
+ writeJson(response, 405, { error: { code: 'method_not_allowed', message: 'この入口では変数と観測値を登録できます' } });
652
+ return true;
653
+ }
531
654
  try {
532
- writeJson(response, 200, await read(resource, { principal: graph.ownerId }));
655
+ writeJson(response, 200, await read(resource, principal));
533
656
  }
534
657
  catch (error) {
535
658
  writeHttpError(response, worldModelError(error));
@@ -619,7 +742,9 @@ if (extensionScreens.some((screen, index) => screen?.id !== extensionIds[index]
619
742
  throw new Error('Invalid extension screen id');
620
743
  }
621
744
  const meta = (name) => document.querySelector(\`meta[name="\${name}"]\`)?.getAttribute('content') ?? '';
622
- const screenFromHash = () => decodeURIComponent(location.hash.replace(/^#/, '')) || undefined;
745
+ // Keep the query portion intact; the shell parses the Graph entity query and
746
+ // only treats the entity id as a target of the Graph screen.
747
+ const screenFromHash = () => location.hash.replace(/^#/, '') || undefined;
623
748
  const shell = createLocalWebShell({
624
749
  root: document.getElementById('brainbase-local-web'),
625
750
  token: meta('brainbase-web-token'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unson/brainbase-mcp",
3
- "version": "0.11.0",
3
+ "version": "0.12.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",
@@ -348,5 +348,5 @@
348
348
  "engines": {
349
349
  "node": ">=20"
350
350
  },
351
- "gitHead": "070f39f8014fbbe224bb4615ef89999aeeb8d0a5"
351
+ "gitHead": "084154257b751ba7bf6b303aa889c42ac551c9b9"
352
352
  }
@@ -163,6 +163,7 @@ function textCell(value, fallback) {
163
163
  * @param {string} [options.token] Launch token sent with a correction.
164
164
  * @param {string} [options.tokenHeader] Header name for the token.
165
165
  * @param {boolean} [options.autoLoad=true] Runs the empty search and reads the kinds on mount.
166
+ * @param {string} [options.initialEntityId] Opens this exact entity after the initial Graph read.
166
167
  * @param {() => Date} [options.now] The owner's clock.
167
168
  * @param {boolean} [options.canCorrect=true] When false, no correction control is drawn anywhere
168
169
  * (この記録を直す, 関係を加える, この関係を直す, and この画面で直せること under the kinds of
@@ -204,6 +205,7 @@ export function createGraphRegistryView({
204
205
  token,
205
206
  tokenHeader,
206
207
  autoLoad = true,
208
+ initialEntityId,
207
209
  now = () => new Date(),
208
210
  canCorrect = true,
209
211
  correctionScope,
@@ -617,6 +619,23 @@ export function createGraphRegistryView({
617
619
  const canFix = scope.entity(entity.type) && scope.fields(graphEntityCorrectionFields(entity.type)).length > 0;
618
620
  const relatable = newRelationOptions(entity.type).length > 0;
619
621
  const related = loaded.outgoing.length + loaded.incoming.length > 0;
622
+ const correctionEntries = [
623
+ canFix ? '「この記録を直す」' : null,
624
+ relatable && scope.createEdges ? '「関係を加える」' : null,
625
+ related && scope.edges ? '関係ごとの「この関係を直す」' : null,
626
+ ].filter(Boolean);
627
+ if (correctionEntries.length > 0) {
628
+ const lastEntry = correctionEntries.at(-1);
629
+ const entries = correctionEntries.length === 1
630
+ ? lastEntry
631
+ : `${correctionEntries.slice(0, -1).join('、')}、または${lastEntry}`;
632
+ children.push(workspaceRailBlock(doc, {
633
+ title: '訂正の流れ',
634
+ content: makeElement(doc, 'p', {
635
+ text: `誤りを見つけたときの訂正入口です。${entries}から変更案を入力します。承認待ちの提出ではなく、保存するとその場でGraphに反映され、理由と変更前後が履歴に残ります。保存後に読み直して一致を確かめ、確認できた内容は、次のMCPの search・get_context・resolve_entity で使われます。`,
636
+ }),
637
+ }));
638
+ }
620
639
  const actions = [
621
640
  canFix ? workspaceButton(doc, { text: 'この記録を直す', onClick: () => openCorrection('entity', () => correction.openEntity(entity, { title: 'この記録を直す' })) }) : null,
622
641
  relatable && scope.createEdges
@@ -797,7 +816,14 @@ export function createGraphRegistryView({
797
816
  },
798
817
  };
799
818
  controller.render();
800
- if (autoLoad) void controller.load();
819
+ if (autoLoad) {
820
+ void controller.load().then(() => {
821
+ if (initialEntityId) return controller.openEntity(initialEntityId);
822
+ return undefined;
823
+ });
824
+ } else if (initialEntityId) {
825
+ void controller.openEntity(initialEntityId);
826
+ }
801
827
  return controller;
802
828
  }
803
829