@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.
- package/contracts/judgment-value-proof/schema.json +19 -1
- package/dist/judgment-value-proof.d.ts +15 -0
- package/dist/judgment-value-proof.js +17 -0
- package/dist/knowledge-promotion.d.ts +61 -3
- package/dist/knowledge-promotion.js +112 -1
- package/dist/local-web-host.d.ts +4 -4
- package/dist/local-web-host.js +137 -12
- package/package.json +2 -2
- package/ui/graph-registry-view.js +27 -1
- package/ui/local-web-shell.js +80 -8
- package/ui/project-workspace.js +1 -1
- package/ui/value-proof-review.css +54 -0
- package/ui/value-proof-review.js +127 -5
- package/ui/world-model-view.css +61 -0
- package/ui/world-model-view.js +215 -10
|
@@ -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);
|
package/dist/local-web-host.d.ts
CHANGED
|
@@ -100,10 +100,10 @@ export declare function createLocalObjectiveDefinitionBuilder(now: () => Date):
|
|
|
100
100
|
*/
|
|
101
101
|
export declare function createObjectiveFoundationModule(context: LocalWebModuleContext): LocalWebModule;
|
|
102
102
|
/**
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
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
|
/**
|
package/dist/local-web-host.js
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
478
|
-
*
|
|
479
|
-
*
|
|
480
|
-
*
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
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)
|
|
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
|
|