@unson/brainbase-mcp 0.10.15 → 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/execution-authority-http.d.ts +4 -2
- package/dist/execution-authority-http.js +101 -6
- package/dist/execution-authority.d.ts +89 -8
- package/dist/execution-authority.js +360 -127
- 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/dist/server-support.d.ts +140 -0
- package/dist/server-support.js +158 -0
- package/package.json +6 -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
|
@@ -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'),
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small HTTP-host helpers shared by Brainbase servers: a structured error with
|
|
3
|
+
* client-safe JSON, an async route wrapper, and a JSON logger that masks
|
|
4
|
+
* secrets. They carry no organization, tenant, or deployment concept.
|
|
5
|
+
*/
|
|
6
|
+
type UnknownRecord = Record<string, unknown>;
|
|
7
|
+
export interface ErrorCodeEntry {
|
|
8
|
+
code: string;
|
|
9
|
+
statusCode: number;
|
|
10
|
+
}
|
|
11
|
+
/** Standard error codes. `code` is safe to expose to clients. */
|
|
12
|
+
export declare const ErrorCodes: {
|
|
13
|
+
VALIDATION_ERROR: {
|
|
14
|
+
code: string;
|
|
15
|
+
statusCode: number;
|
|
16
|
+
};
|
|
17
|
+
INVALID_DATE_FORMAT: {
|
|
18
|
+
code: string;
|
|
19
|
+
statusCode: number;
|
|
20
|
+
};
|
|
21
|
+
MISSING_REQUIRED_FIELD: {
|
|
22
|
+
code: string;
|
|
23
|
+
statusCode: number;
|
|
24
|
+
};
|
|
25
|
+
UNSUPPORTED_FORMAT: {
|
|
26
|
+
code: string;
|
|
27
|
+
statusCode: number;
|
|
28
|
+
};
|
|
29
|
+
UNAUTHORIZED: {
|
|
30
|
+
code: string;
|
|
31
|
+
statusCode: number;
|
|
32
|
+
};
|
|
33
|
+
FORBIDDEN: {
|
|
34
|
+
code: string;
|
|
35
|
+
statusCode: number;
|
|
36
|
+
};
|
|
37
|
+
CROSS_TENANT_CANDIDATE: {
|
|
38
|
+
code: string;
|
|
39
|
+
statusCode: number;
|
|
40
|
+
};
|
|
41
|
+
PROJECT_NOT_FOUND: {
|
|
42
|
+
code: string;
|
|
43
|
+
statusCode: number;
|
|
44
|
+
};
|
|
45
|
+
SESSION_NOT_FOUND: {
|
|
46
|
+
code: string;
|
|
47
|
+
statusCode: number;
|
|
48
|
+
};
|
|
49
|
+
TASK_NOT_FOUND: {
|
|
50
|
+
code: string;
|
|
51
|
+
statusCode: number;
|
|
52
|
+
};
|
|
53
|
+
SCHEDULE_NOT_FOUND: {
|
|
54
|
+
code: string;
|
|
55
|
+
statusCode: number;
|
|
56
|
+
};
|
|
57
|
+
WORKTREE_NOT_FOUND: {
|
|
58
|
+
code: string;
|
|
59
|
+
statusCode: number;
|
|
60
|
+
};
|
|
61
|
+
EVENT_NOT_FOUND: {
|
|
62
|
+
code: string;
|
|
63
|
+
statusCode: number;
|
|
64
|
+
};
|
|
65
|
+
CONFLICT: {
|
|
66
|
+
code: string;
|
|
67
|
+
statusCode: number;
|
|
68
|
+
};
|
|
69
|
+
PORT_IN_USE: {
|
|
70
|
+
code: string;
|
|
71
|
+
statusCode: number;
|
|
72
|
+
};
|
|
73
|
+
INTERNAL_ERROR: {
|
|
74
|
+
code: string;
|
|
75
|
+
statusCode: number;
|
|
76
|
+
};
|
|
77
|
+
DATABASE_ERROR: {
|
|
78
|
+
code: string;
|
|
79
|
+
statusCode: number;
|
|
80
|
+
};
|
|
81
|
+
PROJECT_CATALOG_UNAVAILABLE: {
|
|
82
|
+
code: string;
|
|
83
|
+
statusCode: number;
|
|
84
|
+
};
|
|
85
|
+
TIMEOUT: {
|
|
86
|
+
code: string;
|
|
87
|
+
statusCode: number;
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
export interface AppErrorOptions {
|
|
91
|
+
details?: UnknownRecord | null;
|
|
92
|
+
cause?: unknown;
|
|
93
|
+
}
|
|
94
|
+
export interface AppErrorLog {
|
|
95
|
+
code: string;
|
|
96
|
+
message: string;
|
|
97
|
+
statusCode: number;
|
|
98
|
+
timestamp: string;
|
|
99
|
+
details?: UnknownRecord;
|
|
100
|
+
cause?: string;
|
|
101
|
+
}
|
|
102
|
+
/** A structured error: the message and code go to clients, details stay in logs. */
|
|
103
|
+
export declare class AppError extends Error {
|
|
104
|
+
readonly code: string;
|
|
105
|
+
readonly statusCode: number;
|
|
106
|
+
readonly timestamp: string;
|
|
107
|
+
readonly details: UnknownRecord | null;
|
|
108
|
+
cause?: unknown;
|
|
109
|
+
constructor(message: string, errorCode: ErrorCodeEntry, options?: AppErrorOptions);
|
|
110
|
+
/** Client-safe JSON. It never includes details. */
|
|
111
|
+
toJSON(): {
|
|
112
|
+
error: {
|
|
113
|
+
code: string;
|
|
114
|
+
message: string;
|
|
115
|
+
timestamp: string;
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
/** Log object, including details and the cause message. */
|
|
119
|
+
toLog(): AppErrorLog;
|
|
120
|
+
static isAppError(error: unknown): error is AppError;
|
|
121
|
+
static notFound(resource: string, id: string): AppError;
|
|
122
|
+
static validation(message: string, details?: UnknownRecord): AppError;
|
|
123
|
+
static unauthorized(message?: string): AppError;
|
|
124
|
+
static forbidden(message?: string, details?: UnknownRecord): AppError;
|
|
125
|
+
static conflict(message: string, details?: UnknownRecord): AppError;
|
|
126
|
+
static internal(message?: string, cause?: Error): AppError;
|
|
127
|
+
}
|
|
128
|
+
type RouteHandler = (req: any, res: any, next: (error?: unknown) => unknown) => unknown;
|
|
129
|
+
/** Wraps an async route handler so that a rejection reaches the error middleware. */
|
|
130
|
+
export declare function asyncHandler(fn: RouteHandler): (req: any, res: any, next: (error?: unknown) => unknown) => void;
|
|
131
|
+
/** JSON-line logger that masks secrets. DEBUG enables debug lines. */
|
|
132
|
+
export declare const logger: {
|
|
133
|
+
info(msg: string, data?: UnknownRecord): void;
|
|
134
|
+
warn(msg: string, data?: UnknownRecord): void;
|
|
135
|
+
error(msg: string, data?: Error | (UnknownRecord & {
|
|
136
|
+
error?: unknown;
|
|
137
|
+
})): void;
|
|
138
|
+
debug(msg: string, data?: UnknownRecord): void;
|
|
139
|
+
};
|
|
140
|
+
export {};
|