@unson/brainbase-mcp 0.15.1 → 0.15.2
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/dist/canonical-task-service.d.ts +11 -7
- package/dist/canonical-task-service.js +49 -21
- package/dist/cli.js +39 -3
- package/dist/graph-web-http.d.ts +7 -0
- package/dist/graph-web-http.js +14 -0
- package/dist/graph-web.d.ts +36 -6
- package/dist/graph-web.js +45 -4
- package/dist/judgment-value-proof-review.d.ts +24 -0
- package/dist/judgment-value-proof-review.js +120 -9
- package/dist/local-web-host.d.ts +23 -0
- package/dist/local-web-host.js +26 -7
- package/dist/organization-graph-web.d.ts +74 -0
- package/dist/organization-graph-web.js +275 -0
- package/dist/value-proof-review-http.d.ts +3 -0
- package/dist/value-proof-review-http.js +17 -6
- package/dist/world-extension.d.ts +148 -0
- package/dist/world-extension.js +277 -0
- package/package.json +5 -3
- package/ui/graph-projects-view.js +6 -3
- package/ui/graph-registry-view.js +6 -3
- package/ui/graph-view-shared.css +2 -0
- package/ui/graph-view-shared.js +9 -3
- package/ui/local-web-shell.js +82 -11
- package/ui/value-proof-review.js +61 -6
- package/ui/world/world-placement.js +45 -0
- package/ui/world/world-vendor.LICENSE.txt +23 -0
- package/ui/world/world-vendor.js +4220 -0
- package/ui/world/world-view.css +234 -0
- package/ui/world/world-view.js +1033 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type IncomingMessage, type Server, type ServerResponse } from 'node:http';
|
|
2
|
+
import { JudgmentValueProofJournalCache } from './judgment-value-proof-review.js';
|
|
2
3
|
export declare const VALUE_PROOF_REVIEW_HTTP_VERSION: "value-proof-review-http.v1";
|
|
3
4
|
export declare const VALUE_PROOF_REVIEW_TOKEN_HEADER = "x-brainbase-review-token";
|
|
4
5
|
export interface ValueProofReviewHttpOptions {
|
|
@@ -10,6 +11,8 @@ export interface ValueProofReviewHttpOptions {
|
|
|
10
11
|
readonly token: string;
|
|
11
12
|
readonly basePath?: string;
|
|
12
13
|
readonly now?: () => Date;
|
|
14
|
+
/** One cache per journal; a host passes the one it shares with other readers of the same journal. */
|
|
15
|
+
readonly journalCache?: JudgmentValueProofJournalCache;
|
|
13
16
|
}
|
|
14
17
|
export type ValueProofReviewHttpHandler = (request: IncomingMessage, response: ServerResponse) => Promise<boolean>;
|
|
15
18
|
export declare function createValueProofReviewHttpHandler(options: ValueProofReviewHttpOptions): ValueProofReviewHttpHandler;
|
|
@@ -3,7 +3,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
3
3
|
import { createServer } from 'node:http';
|
|
4
4
|
import { join, relative } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { applyJudgmentValueProofFeedback, buildJudgmentValueProofReviewHome, classifyJudgmentValueProof, JudgmentValueProofAnswerConflictError, readJudgmentValueProofAnswers, readJudgmentValueProofFeedback, readJudgmentValueProofJournal, recordJudgmentValueProofAnswer, recordJudgmentValueProofFeedback } from './judgment-value-proof-review.js';
|
|
6
|
+
import { applyJudgmentValueProofFeedback, buildJudgmentValueProofReviewHome, classifyJudgmentValueProof, JudgmentValueProofAnswerConflictError, JudgmentValueProofJournalCache, readJudgmentValueProofAnswers, readJudgmentValueProofFeedback, readJudgmentValueProofJournal, recordJudgmentValueProofAnswer, recordJudgmentValueProofFeedback } from './judgment-value-proof-review.js';
|
|
7
7
|
import { assertSameOrigin, isLoopbackHost, LOCAL_WEB_CONTENT_SECURITY_POLICY, LocalWebHttpError as HttpError, readJsonObjectBody, requestUrl, tokenMatches, writeHttpError as writeError, writeJson } from './local-web-security.js';
|
|
8
8
|
export const VALUE_PROOF_REVIEW_HTTP_VERSION = 'value-proof-review-http.v1';
|
|
9
9
|
export const VALUE_PROOF_REVIEW_TOKEN_HEADER = 'x-brainbase-review-token';
|
|
@@ -22,8 +22,12 @@ function publicHome(home) {
|
|
|
22
22
|
return home;
|
|
23
23
|
const strip = (items) => Object.fromEntries(Object.entries(items).map(([section, list]) => [
|
|
24
24
|
section,
|
|
25
|
-
list.map(({ section: itemSection, proof, feedback_history, answer }) => ({
|
|
26
|
-
section: itemSection,
|
|
25
|
+
list.map(({ section: itemSection, proof, feedback_history, answer, conversation_moved_on_at }) => ({
|
|
26
|
+
section: itemSection,
|
|
27
|
+
proof,
|
|
28
|
+
feedback_history,
|
|
29
|
+
answer: answer ?? null,
|
|
30
|
+
conversation_moved_on_at: conversation_moved_on_at ?? null
|
|
27
31
|
}))
|
|
28
32
|
]));
|
|
29
33
|
return {
|
|
@@ -44,8 +48,11 @@ export function createValueProofReviewHttpHandler(options) {
|
|
|
44
48
|
}
|
|
45
49
|
const basePath = (options.basePath ?? DEFAULT_BASE_PATH).replace(/\/+$/u, '');
|
|
46
50
|
const now = options.now ?? (() => new Date());
|
|
51
|
+
// A large journal has thousands of conversation folders; only the ones that changed are listed again.
|
|
52
|
+
const journalCache = options.journalCache ?? new JudgmentValueProofJournalCache();
|
|
53
|
+
const readJournal = () => readJudgmentValueProofJournal({ root: options.journalRoot, cache: journalCache });
|
|
47
54
|
async function readHome() {
|
|
48
|
-
const journal = await
|
|
55
|
+
const journal = await readJournal();
|
|
49
56
|
if (journal.status !== 'available')
|
|
50
57
|
return buildJudgmentValueProofReviewHome(journal, [], { now: now() });
|
|
51
58
|
const [feedback, answers] = await Promise.all([
|
|
@@ -65,7 +72,7 @@ export function createValueProofReviewHttpHandler(options) {
|
|
|
65
72
|
const body = await authorizedBody(request);
|
|
66
73
|
const intentId = typeof body.intent_id === 'string' ? body.intent_id : '';
|
|
67
74
|
const attemptId = typeof body.decision_attempt_id === 'string' ? body.decision_attempt_id : '';
|
|
68
|
-
const journal = await
|
|
75
|
+
const journal = await readJournal();
|
|
69
76
|
if (journal.status === 'unavailable') {
|
|
70
77
|
throw new HttpError(503, 'judgment_journal_unavailable', journal.reason);
|
|
71
78
|
}
|
|
@@ -78,6 +85,10 @@ export function createValueProofReviewHttpHandler(options) {
|
|
|
78
85
|
if (classifyJudgmentValueProof(proof) !== 'needs_human' || !proof.human_decision) {
|
|
79
86
|
throw new HttpError(422, 'not_answerable', 'This judgment is not waiting for the owner');
|
|
80
87
|
}
|
|
88
|
+
if (entry.conversation_moved_on_at) {
|
|
89
|
+
// The owner answered in that conversation or moved on; answering here could replay a settled question.
|
|
90
|
+
throw new HttpError(422, 'conversation_moved_on', 'The conversation moved on after this question; continue it there');
|
|
91
|
+
}
|
|
81
92
|
try {
|
|
82
93
|
const result = await recordJudgmentValueProofAnswer({
|
|
83
94
|
dataDir: options.dataDir,
|
|
@@ -108,7 +119,7 @@ export function createValueProofReviewHttpHandler(options) {
|
|
|
108
119
|
const body = await authorizedBody(request);
|
|
109
120
|
const intentId = typeof body.intent_id === 'string' ? body.intent_id : '';
|
|
110
121
|
const attemptId = typeof body.decision_attempt_id === 'string' ? body.decision_attempt_id : '';
|
|
111
|
-
const journal = await
|
|
122
|
+
const journal = await readJournal();
|
|
112
123
|
if (journal.status === 'unavailable') {
|
|
113
124
|
throw new HttpError(503, 'judgment_journal_unavailable', journal.reason);
|
|
114
125
|
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { JudgmentValueProofJournalCache } from './judgment-value-proof-review.js';
|
|
2
|
+
import { type LocalWebExtension, type LocalWebModuleContext } from './local-web-host.js';
|
|
3
|
+
import type { GraphFileV2 } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* World view (ledger P17, adopted 2026-10-04): the home's upper layer.
|
|
6
|
+
*
|
|
7
|
+
* The world is a read-only projection. Businesses come from the same Graph
|
|
8
|
+
* the other screens read (the organization Graph under C1, else the local
|
|
9
|
+
* Graph); judgments come from the existing value-proof home on the browser
|
|
10
|
+
* side. Nothing here writes, and a failure to read is reported as a state,
|
|
11
|
+
* never as zero businesses.
|
|
12
|
+
*/
|
|
13
|
+
export declare const WORLD_EXTENSION_ID = "world";
|
|
14
|
+
export declare const WORLD_EXTENSION_VERSION: "world-extension.v1";
|
|
15
|
+
declare const LIFECYCLE_PHASES: readonly ["active", "maintenance", "finished", "concept"];
|
|
16
|
+
/** How a status looks in the world: lit, muted, an empty lot, or a scaffold. */
|
|
17
|
+
export type WorldLifecyclePhase = (typeof LIFECYCLE_PHASES)[number];
|
|
18
|
+
declare const BUILDING_FORMS: readonly ["tower", "hall", "dome", "office"];
|
|
19
|
+
export type WorldBuildingForm = (typeof BUILDING_FORMS)[number];
|
|
20
|
+
/**
|
|
21
|
+
* The words of the owner's Graph, given by the host (`--world-vocabulary`):
|
|
22
|
+
* the project kinds in display order with their label and building, and the
|
|
23
|
+
* statuses with their label and lifecycle phase. None of it is required: a
|
|
24
|
+
* kind the vocabulary does not name is drawn under its own value, and a status
|
|
25
|
+
* it does not name keeps its value and the active look.
|
|
26
|
+
*/
|
|
27
|
+
export interface WorldVocabularyConfig {
|
|
28
|
+
readonly kinds?: Readonly<Record<string, {
|
|
29
|
+
readonly label?: string;
|
|
30
|
+
readonly form?: WorldBuildingForm;
|
|
31
|
+
readonly color?: string;
|
|
32
|
+
}>>;
|
|
33
|
+
readonly statuses?: Readonly<Record<string, {
|
|
34
|
+
readonly label?: string;
|
|
35
|
+
readonly phase?: WorldLifecyclePhase;
|
|
36
|
+
}>>;
|
|
37
|
+
}
|
|
38
|
+
export interface WorldVocabulary {
|
|
39
|
+
readonly kinds: readonly {
|
|
40
|
+
readonly key: string | null;
|
|
41
|
+
readonly label: string;
|
|
42
|
+
readonly form: WorldBuildingForm;
|
|
43
|
+
readonly color: string;
|
|
44
|
+
readonly configured: boolean;
|
|
45
|
+
}[];
|
|
46
|
+
readonly statuses: readonly {
|
|
47
|
+
readonly key: string;
|
|
48
|
+
readonly label: string;
|
|
49
|
+
readonly phase: WorldLifecyclePhase;
|
|
50
|
+
}[];
|
|
51
|
+
}
|
|
52
|
+
export interface WorldEngagement {
|
|
53
|
+
readonly id: string;
|
|
54
|
+
readonly name: string;
|
|
55
|
+
readonly summary: string | null;
|
|
56
|
+
readonly status: string | null;
|
|
57
|
+
}
|
|
58
|
+
export interface WorldBusiness {
|
|
59
|
+
readonly id: string;
|
|
60
|
+
/** The project's code in its Graph (`metadata.code`), else its id; judgments are placed by it. */
|
|
61
|
+
readonly code: string;
|
|
62
|
+
readonly name: string;
|
|
63
|
+
/** `metadata.kind`, or null when the Graph does not classify the project. */
|
|
64
|
+
readonly kind: string | null;
|
|
65
|
+
readonly purpose: string | null;
|
|
66
|
+
readonly status: string | null;
|
|
67
|
+
/** Repository names the Graph registers for this project (`metadata.repositories`). */
|
|
68
|
+
readonly repositories: readonly string[];
|
|
69
|
+
readonly engagements: readonly WorldEngagement[];
|
|
70
|
+
}
|
|
71
|
+
export interface WorldBusinessProjection {
|
|
72
|
+
readonly businesses: readonly WorldBusiness[];
|
|
73
|
+
/** Projects that name a parent the Graph does not hold (kept visible as a count, never dropped silently). */
|
|
74
|
+
readonly unplaced: readonly WorldEngagement[];
|
|
75
|
+
readonly excluded: {
|
|
76
|
+
readonly inactive: number;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export type WorldSource = {
|
|
80
|
+
readonly authority: 'organization_graph';
|
|
81
|
+
readonly server: string;
|
|
82
|
+
} | {
|
|
83
|
+
readonly authority: 'local';
|
|
84
|
+
readonly dataDir: string;
|
|
85
|
+
};
|
|
86
|
+
export type WorldBusinessesResponse = ({
|
|
87
|
+
readonly status: 'ok';
|
|
88
|
+
readonly version: typeof WORLD_EXTENSION_VERSION;
|
|
89
|
+
readonly source: WorldSource;
|
|
90
|
+
readonly as_of: string;
|
|
91
|
+
readonly vocabulary: WorldVocabulary;
|
|
92
|
+
} & WorldBusinessProjection) | {
|
|
93
|
+
readonly status: 'not_initialized' | 'migration_required' | 'unavailable';
|
|
94
|
+
readonly version: typeof WORLD_EXTENSION_VERSION;
|
|
95
|
+
readonly reason: string;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* The projects of a Graph v2 → cities (top-level projects) and their districts
|
|
99
|
+
* (projects whose `metadata.parent_project_id` names another project). A
|
|
100
|
+
* project whose declared parent (`metadata.parent_project_code`) is not in the
|
|
101
|
+
* Graph is counted as unplaced. Projects past `validTo` are counted, not drawn.
|
|
102
|
+
* Nothing here knows any organization's words: kinds and statuses are the
|
|
103
|
+
* Graph's own values.
|
|
104
|
+
*/
|
|
105
|
+
export declare function projectWorldFromGraph(graph: Pick<GraphFileV2, 'entities'>, now?: Date): WorldBusinessProjection;
|
|
106
|
+
/**
|
|
107
|
+
* The vocabulary the world draws with: the host's kinds in their order, then
|
|
108
|
+
* kinds the Graph uses that the host did not name (under their own value),
|
|
109
|
+
* then unclassified projects; statuses are the common lifecycle words with the
|
|
110
|
+
* host's on top.
|
|
111
|
+
*/
|
|
112
|
+
export declare function resolveWorldVocabulary(config: WorldVocabularyConfig | undefined, businesses: readonly WorldBusiness[]): WorldVocabulary;
|
|
113
|
+
/** Reads and checks a vocabulary file; an invalid file is an error, never silently ignored. */
|
|
114
|
+
export declare function readWorldVocabulary(path: string): Promise<WorldVocabularyConfig>;
|
|
115
|
+
/**
|
|
116
|
+
* The businesses from the same Graph the other screens read: the organization
|
|
117
|
+
* Graph when the host reads it (C1), else the local Graph. A Graph that cannot
|
|
118
|
+
* be read is a state with its reason, never zero businesses.
|
|
119
|
+
*/
|
|
120
|
+
export declare function readWorldBusinesses(context: Pick<LocalWebModuleContext, 'dataDir' | 'organizationGraph'>, options?: {
|
|
121
|
+
readonly vocabulary?: WorldVocabularyConfig;
|
|
122
|
+
readonly now?: () => Date;
|
|
123
|
+
}): Promise<WorldBusinessesResponse>;
|
|
124
|
+
export interface WorldJudgmentPlace {
|
|
125
|
+
readonly decision_attempt_id: string;
|
|
126
|
+
/** Repository the judgment Host recorded for the turn (`turn-input.json` project_code); null when unrecorded. */
|
|
127
|
+
readonly workspace: string | null;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Reads, for every saved value proof in the judgment journal, the repository
|
|
131
|
+
* the Host recorded for that turn. The journal is listed through the host's
|
|
132
|
+
* shared listing cache (the same one as the 今日 list), so only folders that
|
|
133
|
+
* changed are listed again; turn inputs are never rewritten by the Host, so
|
|
134
|
+
* each is read once. A journal that cannot be read is `unavailable`, never
|
|
135
|
+
* an empty list.
|
|
136
|
+
*/
|
|
137
|
+
export declare function readJudgmentPlaces(journalRoot: string, options?: {
|
|
138
|
+
readonly cache?: JudgmentValueProofJournalCache;
|
|
139
|
+
readonly workspaces?: Map<string, string | null>;
|
|
140
|
+
}): Promise<{
|
|
141
|
+
readonly status: 'available' | 'unavailable';
|
|
142
|
+
readonly places: readonly WorldJudgmentPlace[];
|
|
143
|
+
readonly reason?: string;
|
|
144
|
+
}>;
|
|
145
|
+
export declare function createWorldExtension(options?: {
|
|
146
|
+
readonly vocabulary?: WorldVocabularyConfig;
|
|
147
|
+
}): LocalWebExtension;
|
|
148
|
+
export {};
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { readJudgmentValueProofJournal } from './judgment-value-proof-review.js';
|
|
4
|
+
import { readLocalGraphState } from './local-web-host.js';
|
|
5
|
+
import { loadPersonalOs } from './ssot.js';
|
|
6
|
+
import { requestUrl, writeJson } from './local-web-security.js';
|
|
7
|
+
/**
|
|
8
|
+
* World view (ledger P17, adopted 2026-10-04): the home's upper layer.
|
|
9
|
+
*
|
|
10
|
+
* The world is a read-only projection. Businesses come from the same Graph
|
|
11
|
+
* the other screens read (the organization Graph under C1, else the local
|
|
12
|
+
* Graph); judgments come from the existing value-proof home on the browser
|
|
13
|
+
* side. Nothing here writes, and a failure to read is reported as a state,
|
|
14
|
+
* never as zero businesses.
|
|
15
|
+
*/
|
|
16
|
+
export const WORLD_EXTENSION_ID = 'world';
|
|
17
|
+
export const WORLD_EXTENSION_VERSION = 'world-extension.v1';
|
|
18
|
+
const LIFECYCLE_PHASES = ['active', 'maintenance', 'finished', 'concept'];
|
|
19
|
+
const BUILDING_FORMS = ['tower', 'hall', 'dome', 'office'];
|
|
20
|
+
/** Lifecycle words common to any Graph; a host vocabulary may relabel or add to them. */
|
|
21
|
+
const DEFAULT_STATUSES = Object.freeze({
|
|
22
|
+
active: { label: '進行中', phase: 'active' },
|
|
23
|
+
maintenance: { label: '保守', phase: 'maintenance' },
|
|
24
|
+
completed: { label: '完了', phase: 'finished' },
|
|
25
|
+
closed: { label: '終了', phase: 'finished' },
|
|
26
|
+
archived: { label: '保管', phase: 'finished' },
|
|
27
|
+
concept: { label: '構想', phase: 'concept' },
|
|
28
|
+
});
|
|
29
|
+
const KIND_PALETTE = ['#1261ad', '#b07a1f', '#35684c', '#6b4fa0', '#a8433a', '#2f7d86', '#7a6a2b', '#5b6470'];
|
|
30
|
+
const UNCLASSIFIED_COLOR = '#69746d';
|
|
31
|
+
const HEX_COLOR = /^#[0-9a-f]{6}$/iu;
|
|
32
|
+
function isRecord(value) {
|
|
33
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
34
|
+
}
|
|
35
|
+
function text(value) {
|
|
36
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
37
|
+
}
|
|
38
|
+
function engagementOf(entity) {
|
|
39
|
+
return {
|
|
40
|
+
id: entity.id,
|
|
41
|
+
name: entity.name,
|
|
42
|
+
summary: text(entity.summary) ?? text(entity.metadata?.goal),
|
|
43
|
+
status: text(entity.metadata?.status),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The projects of a Graph v2 → cities (top-level projects) and their districts
|
|
48
|
+
* (projects whose `metadata.parent_project_id` names another project). A
|
|
49
|
+
* project whose declared parent (`metadata.parent_project_code`) is not in the
|
|
50
|
+
* Graph is counted as unplaced. Projects past `validTo` are counted, not drawn.
|
|
51
|
+
* Nothing here knows any organization's words: kinds and statuses are the
|
|
52
|
+
* Graph's own values.
|
|
53
|
+
*/
|
|
54
|
+
export function projectWorldFromGraph(graph, now = new Date()) {
|
|
55
|
+
const projects = graph.entities.filter((entity) => entity.type === 'project');
|
|
56
|
+
const active = projects.filter((entity) => !entity.validTo || Date.parse(entity.validTo) > now.getTime());
|
|
57
|
+
const byId = new Map(active.map((entity) => [entity.id, entity]));
|
|
58
|
+
// A district hangs on its top-level ancestor, so nested projects stay in one city.
|
|
59
|
+
const topOf = (entity) => {
|
|
60
|
+
let current = entity;
|
|
61
|
+
const seen = new Set();
|
|
62
|
+
for (;;) {
|
|
63
|
+
const parentId = text(current.metadata?.parent_project_id);
|
|
64
|
+
if (!parentId || parentId === current.id)
|
|
65
|
+
return current;
|
|
66
|
+
const parent = byId.get(parentId);
|
|
67
|
+
if (!parent || seen.has(parentId))
|
|
68
|
+
return parent ? current : null;
|
|
69
|
+
seen.add(parentId);
|
|
70
|
+
current = parent;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const cities = new Map();
|
|
74
|
+
const unplaced = [];
|
|
75
|
+
const children = [];
|
|
76
|
+
for (const entity of active) {
|
|
77
|
+
const parentId = text(entity.metadata?.parent_project_id);
|
|
78
|
+
if (parentId && parentId !== entity.id)
|
|
79
|
+
children.push(entity);
|
|
80
|
+
else if (text(entity.metadata?.parent_project_code))
|
|
81
|
+
unplaced.push(engagementOf(entity));
|
|
82
|
+
else
|
|
83
|
+
cities.set(entity.id, { entity, engagements: [] });
|
|
84
|
+
}
|
|
85
|
+
for (const entity of children) {
|
|
86
|
+
const top = topOf(entity);
|
|
87
|
+
const city = top ? cities.get(top.id) : undefined;
|
|
88
|
+
if (city && top.id !== entity.id)
|
|
89
|
+
city.engagements.push(engagementOf(entity));
|
|
90
|
+
else
|
|
91
|
+
unplaced.push(engagementOf(entity));
|
|
92
|
+
}
|
|
93
|
+
const businesses = [...cities.values()].map(({ entity, engagements }) => ({
|
|
94
|
+
id: entity.id,
|
|
95
|
+
code: text(entity.metadata?.code) ?? entity.id,
|
|
96
|
+
name: entity.name,
|
|
97
|
+
kind: text(entity.metadata?.kind),
|
|
98
|
+
purpose: text(entity.metadata?.goal) ?? text(entity.summary),
|
|
99
|
+
status: text(entity.metadata?.status),
|
|
100
|
+
repositories: Array.isArray(entity.metadata?.repositories)
|
|
101
|
+
? entity.metadata.repositories.map(text).filter((name) => name !== null)
|
|
102
|
+
: [],
|
|
103
|
+
engagements: engagements.sort((a, b) => a.name.localeCompare(b.name, 'ja')),
|
|
104
|
+
}));
|
|
105
|
+
return {
|
|
106
|
+
businesses: businesses.sort((a, b) => a.name.localeCompare(b.name, 'ja')),
|
|
107
|
+
unplaced,
|
|
108
|
+
excluded: { inactive: projects.length - active.length },
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The vocabulary the world draws with: the host's kinds in their order, then
|
|
113
|
+
* kinds the Graph uses that the host did not name (under their own value),
|
|
114
|
+
* then unclassified projects; statuses are the common lifecycle words with the
|
|
115
|
+
* host's on top.
|
|
116
|
+
*/
|
|
117
|
+
export function resolveWorldVocabulary(config, businesses) {
|
|
118
|
+
const configured = Object.entries(config?.kinds ?? {});
|
|
119
|
+
const used = new Set(businesses.map((business) => business.kind));
|
|
120
|
+
const kinds = configured.map(([key, entry], index) => ({
|
|
121
|
+
key,
|
|
122
|
+
label: text(entry.label) ?? key,
|
|
123
|
+
form: entry.form && BUILDING_FORMS.includes(entry.form) ? entry.form : 'office',
|
|
124
|
+
color: entry.color && HEX_COLOR.test(entry.color) ? entry.color : KIND_PALETTE[index % KIND_PALETTE.length],
|
|
125
|
+
configured: true,
|
|
126
|
+
}));
|
|
127
|
+
const named = new Set(configured.map(([key]) => key));
|
|
128
|
+
[...used].filter((key) => key !== null && !named.has(key)).sort().forEach((key) => {
|
|
129
|
+
kinds.push({ key, label: key, form: 'office', color: KIND_PALETTE[kinds.length % KIND_PALETTE.length], configured: false });
|
|
130
|
+
});
|
|
131
|
+
if (used.has(null))
|
|
132
|
+
kinds.push({ key: null, label: '分類なし', form: 'office', color: UNCLASSIFIED_COLOR, configured: false });
|
|
133
|
+
const statuses = new Map(Object.entries(DEFAULT_STATUSES));
|
|
134
|
+
for (const [key, entry] of Object.entries(config?.statuses ?? {})) {
|
|
135
|
+
const base = statuses.get(key);
|
|
136
|
+
statuses.set(key, {
|
|
137
|
+
label: text(entry.label) ?? base?.label ?? key,
|
|
138
|
+
phase: entry.phase && LIFECYCLE_PHASES.includes(entry.phase) ? entry.phase : base?.phase ?? 'active',
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return { kinds, statuses: [...statuses].map(([key, entry]) => ({ key, ...entry })) };
|
|
142
|
+
}
|
|
143
|
+
/** Reads and checks a vocabulary file; an invalid file is an error, never silently ignored. */
|
|
144
|
+
export async function readWorldVocabulary(path) {
|
|
145
|
+
const parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
146
|
+
if (!isRecord(parsed))
|
|
147
|
+
throw new Error('world vocabulary must be a JSON object');
|
|
148
|
+
const kinds = {};
|
|
149
|
+
for (const [key, entry] of Object.entries(isRecord(parsed.kinds) ? parsed.kinds : {})) {
|
|
150
|
+
if (!isRecord(entry))
|
|
151
|
+
throw new Error(`world vocabulary kind ${key} must be an object`);
|
|
152
|
+
if (entry.form !== undefined && !BUILDING_FORMS.includes(entry.form))
|
|
153
|
+
throw new Error(`world vocabulary kind ${key} has an unknown form`);
|
|
154
|
+
if (entry.color !== undefined && !(typeof entry.color === 'string' && HEX_COLOR.test(entry.color)))
|
|
155
|
+
throw new Error(`world vocabulary kind ${key} has an invalid color`);
|
|
156
|
+
kinds[key] = { ...(text(entry.label) ? { label: text(entry.label) } : {}), ...(entry.form ? { form: entry.form } : {}), ...(entry.color ? { color: entry.color } : {}) };
|
|
157
|
+
}
|
|
158
|
+
const statuses = {};
|
|
159
|
+
for (const [key, entry] of Object.entries(isRecord(parsed.statuses) ? parsed.statuses : {})) {
|
|
160
|
+
if (!isRecord(entry))
|
|
161
|
+
throw new Error(`world vocabulary status ${key} must be an object`);
|
|
162
|
+
if (entry.phase !== undefined && !LIFECYCLE_PHASES.includes(entry.phase))
|
|
163
|
+
throw new Error(`world vocabulary status ${key} has an unknown phase`);
|
|
164
|
+
statuses[key] = { ...(text(entry.label) ? { label: text(entry.label) } : {}), ...(entry.phase ? { phase: entry.phase } : {}) };
|
|
165
|
+
}
|
|
166
|
+
return { kinds, statuses };
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The businesses from the same Graph the other screens read: the organization
|
|
170
|
+
* Graph when the host reads it (C1), else the local Graph. A Graph that cannot
|
|
171
|
+
* be read is a state with its reason, never zero businesses.
|
|
172
|
+
*/
|
|
173
|
+
export async function readWorldBusinesses(context, options = {}) {
|
|
174
|
+
const now = (options.now ?? (() => new Date()))();
|
|
175
|
+
let graph;
|
|
176
|
+
let source;
|
|
177
|
+
try {
|
|
178
|
+
if (context.organizationGraph) {
|
|
179
|
+
if (!context.organizationGraph.readGraphFile) {
|
|
180
|
+
return { status: 'unavailable', version: WORLD_EXTENSION_VERSION, reason: 'organization_graph_records_unavailable' };
|
|
181
|
+
}
|
|
182
|
+
graph = await context.organizationGraph.readGraphFile();
|
|
183
|
+
source = { authority: 'organization_graph', server: context.organizationGraph.server };
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
const state = await readLocalGraphState(context.dataDir);
|
|
187
|
+
if (state.status === 'not_initialized' || state.status === 'migration_required') {
|
|
188
|
+
return { status: state.status, version: WORLD_EXTENSION_VERSION, reason: state.status };
|
|
189
|
+
}
|
|
190
|
+
if (state.status !== 'ready')
|
|
191
|
+
return { status: 'unavailable', version: WORLD_EXTENSION_VERSION, reason: state.message ?? 'local_graph_unreadable' };
|
|
192
|
+
const os = await loadPersonalOs(context.dataDir);
|
|
193
|
+
if (os.graph.version !== 2)
|
|
194
|
+
return { status: 'migration_required', version: WORLD_EXTENSION_VERSION, reason: 'migration_required' };
|
|
195
|
+
graph = os.graph;
|
|
196
|
+
source = { authority: 'local', dataDir: context.dataDir };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
return { status: 'unavailable', version: WORLD_EXTENSION_VERSION, reason: error instanceof Error ? error.message : 'graph_unreadable' };
|
|
201
|
+
}
|
|
202
|
+
const projection = projectWorldFromGraph(graph, now);
|
|
203
|
+
return {
|
|
204
|
+
status: 'ok',
|
|
205
|
+
version: WORLD_EXTENSION_VERSION,
|
|
206
|
+
source,
|
|
207
|
+
as_of: now.toISOString(),
|
|
208
|
+
vocabulary: resolveWorldVocabulary(options.vocabulary, projection.businesses),
|
|
209
|
+
...projection,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const VALUE_PROOF_SUFFIX = '.value-proof.json';
|
|
213
|
+
/**
|
|
214
|
+
* Reads, for every saved value proof in the judgment journal, the repository
|
|
215
|
+
* the Host recorded for that turn. The journal is listed through the host's
|
|
216
|
+
* shared listing cache (the same one as the 今日 list), so only folders that
|
|
217
|
+
* changed are listed again; turn inputs are never rewritten by the Host, so
|
|
218
|
+
* each is read once. A journal that cannot be read is `unavailable`, never
|
|
219
|
+
* an empty list.
|
|
220
|
+
*/
|
|
221
|
+
export async function readJudgmentPlaces(journalRoot, options = {}) {
|
|
222
|
+
const journal = await readJudgmentValueProofJournal({ root: journalRoot, ...(options.cache ? { cache: options.cache } : {}) });
|
|
223
|
+
if (journal.status !== 'available')
|
|
224
|
+
return { status: 'unavailable', places: [], reason: 'journal_unreadable' };
|
|
225
|
+
const workspaces = options.workspaces ?? new Map();
|
|
226
|
+
const places = await Promise.all(journal.entries.map(async (entry) => {
|
|
227
|
+
const turnInputFile = `${entry.file.slice(0, -VALUE_PROOF_SUFFIX.length)}.turn-input.json`;
|
|
228
|
+
if (!workspaces.has(turnInputFile)) {
|
|
229
|
+
const turnInput = await readJsonFile(turnInputFile);
|
|
230
|
+
workspaces.set(turnInputFile, isRecord(turnInput) ? text(turnInput.project_code) : null);
|
|
231
|
+
}
|
|
232
|
+
return { decision_attempt_id: entry.proof.decision_attempt_id, workspace: workspaces.get(turnInputFile) ?? null };
|
|
233
|
+
}));
|
|
234
|
+
return { status: 'available', places };
|
|
235
|
+
}
|
|
236
|
+
async function readJsonFile(path) {
|
|
237
|
+
try {
|
|
238
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function createWorldModule(context, read) {
|
|
245
|
+
const workspaces = new Map();
|
|
246
|
+
return {
|
|
247
|
+
id: WORLD_EXTENSION_ID,
|
|
248
|
+
uiFiles: [],
|
|
249
|
+
async handle(request, response) {
|
|
250
|
+
if (request.method !== 'GET')
|
|
251
|
+
return false;
|
|
252
|
+
const path = requestUrl(request).pathname;
|
|
253
|
+
if (path === `/api/extensions/${WORLD_EXTENSION_ID}/businesses`) {
|
|
254
|
+
response.setHeader('Cache-Control', 'no-store');
|
|
255
|
+
writeJson(response, 200, await read());
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
if (path === `/api/extensions/${WORLD_EXTENSION_ID}/judgment-places`) {
|
|
259
|
+
response.setHeader('Cache-Control', 'no-store');
|
|
260
|
+
writeJson(response, 200, { version: WORLD_EXTENSION_VERSION, ...(await readJudgmentPlaces(context.journalRoot, { cache: context.journalCache, workspaces })) });
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
return false;
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
export function createWorldExtension(options = {}) {
|
|
268
|
+
return {
|
|
269
|
+
id: WORLD_EXTENSION_ID,
|
|
270
|
+
uiDir: fileURLToPath(new URL('../ui/world/', import.meta.url)),
|
|
271
|
+
uiFiles: ['world-view.js', 'world-view.css', 'world-vendor.js', 'world-placement.js'],
|
|
272
|
+
screenEntry: 'world-view.js',
|
|
273
|
+
// P17: the world sits above the home and opens first.
|
|
274
|
+
navPosition: 'first',
|
|
275
|
+
createModule: (context) => createWorldModule(context, () => readWorldBusinesses(context, { vocabulary: options.vocabulary, now: context.now })),
|
|
276
|
+
};
|
|
277
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unson/brainbase-mcp",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.2",
|
|
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",
|
|
@@ -329,7 +329,8 @@
|
|
|
329
329
|
"release:validate": "node scripts/npm-release.mjs validate",
|
|
330
330
|
"release:publish": "node scripts/npm-release.mjs publish",
|
|
331
331
|
"release:verify": "node scripts/npm-release.mjs verify",
|
|
332
|
-
"build:graph": "node scripts/build-project-graph.mjs"
|
|
332
|
+
"build:graph": "node scripts/build-project-graph.mjs",
|
|
333
|
+
"build:world": "node scripts/build-world-vendor.mjs"
|
|
333
334
|
},
|
|
334
335
|
"dependencies": {
|
|
335
336
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
@@ -344,6 +345,7 @@
|
|
|
344
345
|
"@electric-sql/pglite": "^0.5.8",
|
|
345
346
|
"@types/node": "^22.15.29",
|
|
346
347
|
"esbuild": "0.28.2",
|
|
348
|
+
"three": "0.186.1",
|
|
347
349
|
"tsx": "^4.19.4",
|
|
348
350
|
"typescript": "^5.8.3",
|
|
349
351
|
"vitepress": "^1.6.4",
|
|
@@ -352,5 +354,5 @@
|
|
|
352
354
|
"engines": {
|
|
353
355
|
"node": ">=20"
|
|
354
356
|
},
|
|
355
|
-
"gitHead": "
|
|
357
|
+
"gitHead": "ae251c43b4f42eddbeb18297e67710582a42af1f"
|
|
356
358
|
}
|
|
@@ -188,7 +188,8 @@ function normalizeProjectSummary(value) {
|
|
|
188
188
|
* プロジェクトを直す, `fields` limits its form, `edges: false` withholds 役割を直す and 関わりを終える /
|
|
189
189
|
* 終了日を直す, and `createEdges: false` withholds 関係者を加える. A withheld control is not drawn and the
|
|
190
190
|
* client refuses such a correction before sending it. Without it every correction is allowed.
|
|
191
|
-
* @param {string
|
|
191
|
+
* @param {string | ((record: { id: string, type: string | null, metadata: object } | null) => string | Element | null)} [options.readOnlyNote]
|
|
192
|
+
* With `canCorrect: false`, or where `correctionScope` withholds a
|
|
192
193
|
* control of the selected project, a note shown in the rail where the correction buttons would be.
|
|
193
194
|
* Without it the rail says nothing about corrections.
|
|
194
195
|
* @param {{ label?: string, text: string | Element }} [options.emptyNotice] Replaces the 未登録 notice
|
|
@@ -714,6 +715,8 @@ export function createGraphProjectsView({
|
|
|
714
715
|
const detail = state.detail;
|
|
715
716
|
const loaded = detail?.id === state.selectedId && detail.state === 'ok' ? detail.payload : null;
|
|
716
717
|
const project = loaded?.project ?? listItem(state.selectedId) ?? { id: state.selectedId, name: state.selectedId };
|
|
718
|
+
// The record a host's read-only note may point at.
|
|
719
|
+
const noteSubject = { id: project.id, type: 'project', metadata: loaded?.project?.metadata ?? {} };
|
|
717
720
|
const children = [workspaceRailHead(doc, { kicker: 'プロジェクト', title: project.name, sub: project.id })];
|
|
718
721
|
if (!loaded) {
|
|
719
722
|
const readState = detail?.id === state.selectedId
|
|
@@ -776,9 +779,9 @@ export function createGraphProjectsView({
|
|
|
776
779
|
if (!loaded.participants.some((edge) => edge.id === state.panelAt)) children.push(correction.render(doc));
|
|
777
780
|
// The host's reason for a control it withholds, where that control would be.
|
|
778
781
|
const withheld = !canAdd || projectFields.length === 0 || (!scope.edges && loaded.participants.length > 0);
|
|
779
|
-
if (withheld) children.push(hostReadOnlyNote(doc, readOnlyNote));
|
|
782
|
+
if (withheld) children.push(hostReadOnlyNote(doc, readOnlyNote, noteSubject));
|
|
780
783
|
} else {
|
|
781
|
-
children.push(hostReadOnlyNote(doc, readOnlyNote));
|
|
784
|
+
children.push(hostReadOnlyNote(doc, readOnlyNote, noteSubject));
|
|
782
785
|
}
|
|
783
786
|
children.push(...railExtensions(state.selectedId, loaded));
|
|
784
787
|
children.push(renderOtherRelations(loaded));
|
|
@@ -173,7 +173,8 @@ function textCell(value, fallback) {
|
|
|
173
173
|
* offer この記録を直す, `fields` limits its form, `edges: false` withholds この関係を直す and
|
|
174
174
|
* `createEdges: false` withholds 関係を加える. A withheld control is not drawn and the client refuses
|
|
175
175
|
* such a correction before sending it. Without it every correction is allowed.
|
|
176
|
-
* @param {string
|
|
176
|
+
* @param {string | ((record: { id: string, type: string | null, metadata: object } | null) => string | Element | null)} [options.readOnlyNote]
|
|
177
|
+
* With `canCorrect: false`, or where `correctionScope` withholds a
|
|
177
178
|
* control of the selected record, a note shown in the rail where the correction buttons would be.
|
|
178
179
|
* Without it the rail says nothing about corrections.
|
|
179
180
|
* @param {{ label?: string, text: string | Element }} [options.emptyNotice] Replaces the 未登録 notice
|
|
@@ -601,6 +602,8 @@ export function createGraphRegistryView({
|
|
|
601
602
|
children.push(graphStateNotice(doc, detail, { onRetry: () => controller.loadEntity(detail.id), loadingText: 'この記録を読み込んでいます。' }));
|
|
602
603
|
return children;
|
|
603
604
|
}
|
|
605
|
+
// The record a host's read-only note may point at.
|
|
606
|
+
const noteSubject = { id: entity.id, type: entity.type, metadata: loaded.entity?.metadata ?? {} };
|
|
604
607
|
const summaryRows = [
|
|
605
608
|
['別名', entity.aliases.length > 0 ? entity.aliases.join('、') : { text: 'なし', className: 'is-unrecorded' }],
|
|
606
609
|
['要約', textOrNull(entity.summary)],
|
|
@@ -647,9 +650,9 @@ export function createGraphRegistryView({
|
|
|
647
650
|
const inRelation = [...loaded.outgoing, ...loaded.incoming].some((edge) => edge.id === state.panelAt);
|
|
648
651
|
if (!inRelation) children.push(correction.render(doc));
|
|
649
652
|
// The host's reason for a control it withholds, where that control would be.
|
|
650
|
-
if (!canFix || (relatable && !scope.createEdges) || (related && !scope.edges)) children.push(hostReadOnlyNote(doc, readOnlyNote));
|
|
653
|
+
if (!canFix || (relatable && !scope.createEdges) || (related && !scope.edges)) children.push(hostReadOnlyNote(doc, readOnlyNote, noteSubject));
|
|
651
654
|
} else {
|
|
652
|
-
children.push(hostReadOnlyNote(doc, readOnlyNote));
|
|
655
|
+
children.push(hostReadOnlyNote(doc, readOnlyNote, noteSubject));
|
|
653
656
|
}
|
|
654
657
|
children.push(renderRelations(loaded, 'outgoing'), renderRelations(loaded, 'incoming'));
|
|
655
658
|
children.push(renderCorrectionHistory(doc, loaded.history));
|
package/ui/graph-view-shared.css
CHANGED
package/ui/graph-view-shared.js
CHANGED
|
@@ -459,9 +459,15 @@ function withinCorrectionScope(scope, body, recordType) {
|
|
|
459
459
|
return scope.entity(recordType) && scope.fields(changed).length === changed.length;
|
|
460
460
|
}
|
|
461
461
|
|
|
462
|
-
/**
|
|
463
|
-
|
|
464
|
-
|
|
462
|
+
/**
|
|
463
|
+
* A host's note that corrections are not available here, or null when it passed none. The host may
|
|
464
|
+
* pass a function of the selected record (`{ id, type, metadata }`) returning a string or an element,
|
|
465
|
+
* for a note that points at that record (for example a link to where it is corrected).
|
|
466
|
+
*/
|
|
467
|
+
export function hostReadOnlyNote(doc, readOnlyNote, subject) {
|
|
468
|
+
const value = typeof readOnlyNote === 'function' ? readOnlyNote(subject ?? null) : readOnlyNote;
|
|
469
|
+
if (value && typeof value === 'object' && typeof value.tagName === 'string') return workspaceNotice(doc, { text: value, tone: 'info' });
|
|
470
|
+
const text = textOrNull(value);
|
|
465
471
|
return text ? workspaceNotice(doc, { text, tone: 'info' }) : null;
|
|
466
472
|
}
|
|
467
473
|
|