@unson/brainbase-mcp 0.15.2 → 0.15.4
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/local-web-host.d.ts +13 -0
- package/dist/organization-graph-web.d.ts +7 -1
- package/dist/organization-graph-web.js +35 -1
- package/dist/value-proof-review-http.d.ts +3 -1
- package/dist/value-proof-review-http.js +2 -1
- package/dist/world-extension.d.ts +37 -2
- package/dist/world-extension.js +93 -9
- package/package.json +6 -2
- package/ui/graph-projects-view.js +5 -1
- package/ui/graph-registry-view.js +8 -1
- package/ui/graph-view-shared.js +49 -0
- package/ui/local-web-shell.js +25 -34
- package/ui/value-proof-review.js +6 -1
- package/ui/world/world-placement.js +21 -0
- package/ui/world/world-view.js +77 -27
package/dist/local-web-host.d.ts
CHANGED
|
@@ -94,6 +94,19 @@ export interface LocalWebOrganizationGraph {
|
|
|
94
94
|
read(): Promise<InMemoryGraphReader<GraphWebAnySource>>;
|
|
95
95
|
/** The same Graph as plain records (the world reads project metadata from it). */
|
|
96
96
|
readGraphFile?(): Promise<GraphFileV2>;
|
|
97
|
+
/**
|
|
98
|
+
* The organization's words for field values (glossary terms with `payload.vocabulary`), or null when
|
|
99
|
+
* they could not be read. The rest of the Graph is read even when these are not.
|
|
100
|
+
*/
|
|
101
|
+
readVocabularyTerms?(): Promise<readonly GraphVocabularyTerm[] | null>;
|
|
102
|
+
}
|
|
103
|
+
/** One organization term that names a field value, e.g. project.kind = product → プロダクト. */
|
|
104
|
+
export interface GraphVocabularyTerm {
|
|
105
|
+
readonly id: string;
|
|
106
|
+
readonly field: string;
|
|
107
|
+
readonly value: string;
|
|
108
|
+
readonly label: string;
|
|
109
|
+
readonly definition: string | null;
|
|
97
110
|
}
|
|
98
111
|
export interface LocalWebHost {
|
|
99
112
|
readonly server: Server;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LocalWebOrganizationGraph } from './local-web-host.js';
|
|
1
|
+
import type { GraphVocabularyTerm, LocalWebOrganizationGraph } from './local-web-host.js';
|
|
2
2
|
import type { CanonicalEntityKind, GraphFileV2 } from './types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Ledger C1 (adopted 2026-09-24): while the owner validates the Web, the host
|
|
@@ -44,6 +44,12 @@ export interface OrganizationGraphProjection {
|
|
|
44
44
|
readonly danglingRelations: number;
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* The organization's words for field values: active glossary terms that carry
|
|
49
|
+
* `payload.vocabulary = { field, value }`. The name is the term's label, else
|
|
50
|
+
* its term, else the value itself (pure; tested).
|
|
51
|
+
*/
|
|
52
|
+
export declare function projectVocabularyTerms(records: readonly unknown[]): GraphVocabularyTerm[];
|
|
47
53
|
/** Projects organization Graph records into a canonical Graph v2 (pure; tested). */
|
|
48
54
|
export declare function projectOrganizationGraph(records: OrganizationGraphRecords, owner?: {
|
|
49
55
|
id: string;
|
|
@@ -42,6 +42,33 @@ function isCatalogProject(record, payload) {
|
|
|
42
42
|
const scope = text(record.project_code);
|
|
43
43
|
return scope !== null && (text(payload.code) ?? record.id) === scope;
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* The organization's words for field values: active glossary terms that carry
|
|
47
|
+
* `payload.vocabulary = { field, value }`. The name is the term's label, else
|
|
48
|
+
* its term, else the value itself (pure; tested).
|
|
49
|
+
*/
|
|
50
|
+
export function projectVocabularyTerms(records) {
|
|
51
|
+
const terms = [];
|
|
52
|
+
for (const record of records) {
|
|
53
|
+
if (!isRecord(record) || typeof record.id !== 'string' || !isRecord(record.payload))
|
|
54
|
+
continue;
|
|
55
|
+
if (record.lifecycle_status && record.lifecycle_status !== 'active')
|
|
56
|
+
continue;
|
|
57
|
+
const vocabulary = record.payload.vocabulary;
|
|
58
|
+
const field = isRecord(vocabulary) ? text(vocabulary.field) : null;
|
|
59
|
+
const value = isRecord(vocabulary) ? text(vocabulary.value) : null;
|
|
60
|
+
if (!field || !value)
|
|
61
|
+
continue;
|
|
62
|
+
terms.push({
|
|
63
|
+
id: record.id,
|
|
64
|
+
field,
|
|
65
|
+
value,
|
|
66
|
+
label: text(record.payload.label) ?? text(record.payload.term) ?? value,
|
|
67
|
+
definition: text(record.payload.definition)
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return terms.sort((a, b) => a.id.localeCompare(b.id));
|
|
71
|
+
}
|
|
45
72
|
/** A project's code, parent and repositories as the organization Graph states them. */
|
|
46
73
|
function projectPlacement(record, payload, catalogByCode) {
|
|
47
74
|
const scope = text(record.project_code);
|
|
@@ -228,6 +255,10 @@ export async function createOrganizationGraphSource(options = {}) {
|
|
|
228
255
|
const access = await readOrganizationAccess(home);
|
|
229
256
|
if ('reason' in access)
|
|
230
257
|
throw new GraphWebError('unavailable', 'organization_graph_not_connected', `The organization Graph cannot be read (${access.reason})`);
|
|
258
|
+
// The organization's words are read alongside; failing to read them never fails the Graph.
|
|
259
|
+
const termsRead = getRecords(request, access, 'entities?type=glossary_term&limit=500')
|
|
260
|
+
.then((records) => projectVocabularyTerms(records))
|
|
261
|
+
.catch(() => null);
|
|
231
262
|
const [project, person, org, decision, raci, memberOf, assignedTo] = await Promise.all([
|
|
232
263
|
getRecords(request, access, 'entities?type=project&limit=500'),
|
|
233
264
|
getRecords(request, access, 'entities?type=person&limit=500'),
|
|
@@ -249,7 +280,7 @@ export async function createOrganizationGraphSource(options = {}) {
|
|
|
249
280
|
assignedTo: assignedTo
|
|
250
281
|
});
|
|
251
282
|
const source = { dataDir: null, graphFormat: 2, authority: 'organization_graph', server: access.server, readAt: now().toISOString() };
|
|
252
|
-
return { reader: openInMemoryGraph(projection.graph, source), graph: projection.graph };
|
|
283
|
+
return { reader: openInMemoryGraph(projection.graph, source), graph: projection.graph, terms: await termsRead };
|
|
253
284
|
}
|
|
254
285
|
async function current() {
|
|
255
286
|
const at = now().getTime();
|
|
@@ -270,6 +301,9 @@ export async function createOrganizationGraphSource(options = {}) {
|
|
|
270
301
|
},
|
|
271
302
|
async readGraphFile() {
|
|
272
303
|
return (await current()).graph;
|
|
304
|
+
},
|
|
305
|
+
async readVocabularyTerms() {
|
|
306
|
+
return (await current()).terms;
|
|
273
307
|
}
|
|
274
308
|
};
|
|
275
309
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type IncomingMessage, type Server, type ServerResponse } from 'node:http';
|
|
2
|
-
import { JudgmentValueProofJournalCache } from './judgment-value-proof-review.js';
|
|
2
|
+
import { JudgmentValueProofJournalCache, type JudgmentValueProofReviewHome } from './judgment-value-proof-review.js';
|
|
3
3
|
export declare const VALUE_PROOF_REVIEW_HTTP_VERSION: "value-proof-review-http.v1";
|
|
4
4
|
export declare const VALUE_PROOF_REVIEW_TOKEN_HEADER = "x-brainbase-review-token";
|
|
5
5
|
export interface ValueProofReviewHttpOptions {
|
|
@@ -15,6 +15,8 @@ export interface ValueProofReviewHttpOptions {
|
|
|
15
15
|
readonly journalCache?: JudgmentValueProofJournalCache;
|
|
16
16
|
}
|
|
17
17
|
export type ValueProofReviewHttpHandler = (request: IncomingMessage, response: ServerResponse) => Promise<boolean>;
|
|
18
|
+
/** The home as the browser receives it: no local paths beyond the journal root. */
|
|
19
|
+
export declare function publicHome(home: JudgmentValueProofReviewHome): unknown;
|
|
18
20
|
export declare function createValueProofReviewHttpHandler(options: ValueProofReviewHttpOptions): ValueProofReviewHttpHandler;
|
|
19
21
|
export interface ValueProofReviewHostOptions extends Omit<ValueProofReviewHttpOptions, 'token'> {
|
|
20
22
|
readonly token?: string;
|
|
@@ -17,7 +17,8 @@ const UI_FILES = Object.freeze({
|
|
|
17
17
|
'/ui/value-proof-review.js': { file: 'value-proof-review.js', type: 'text/javascript; charset=utf-8' },
|
|
18
18
|
'/ui/value-proof-review.css': { file: 'value-proof-review.css', type: 'text/css; charset=utf-8' }
|
|
19
19
|
});
|
|
20
|
-
|
|
20
|
+
/** The home as the browser receives it: no local paths beyond the journal root. */
|
|
21
|
+
export function publicHome(home) {
|
|
21
22
|
if (home.status === 'unavailable')
|
|
22
23
|
return home;
|
|
23
24
|
const strip = (items) => Object.fromEntries(Object.entries(items).map(([section, list]) => [
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { JudgmentValueProofJournalCache } from './judgment-value-proof-review.js';
|
|
2
|
-
import { type LocalWebExtension, type LocalWebModuleContext } from './local-web-host.js';
|
|
2
|
+
import { type GraphVocabularyTerm, type LocalWebExtension, type LocalWebModuleContext } from './local-web-host.js';
|
|
3
3
|
import type { GraphFileV2 } from './types.js';
|
|
4
4
|
/**
|
|
5
5
|
* World view (ledger P17, adopted 2026-10-04): the home's upper layer.
|
|
@@ -39,10 +39,15 @@ export interface WorldVocabulary {
|
|
|
39
39
|
readonly kinds: readonly {
|
|
40
40
|
readonly key: string | null;
|
|
41
41
|
readonly label: string;
|
|
42
|
+
/** Where the label came from: the organization's term, the host's vocabulary file, or the value itself. */
|
|
43
|
+
readonly label_source: 'graph' | 'config' | 'value';
|
|
44
|
+
readonly definition: string | null;
|
|
42
45
|
readonly form: WorldBuildingForm;
|
|
43
46
|
readonly color: string;
|
|
44
47
|
readonly configured: boolean;
|
|
45
48
|
}[];
|
|
49
|
+
/** Whether the organization's terms were read: `unavailable` when they could not be, `none` without an organization Graph. */
|
|
50
|
+
readonly terms: 'read' | 'unavailable' | 'none';
|
|
46
51
|
readonly statuses: readonly {
|
|
47
52
|
readonly key: string;
|
|
48
53
|
readonly label: string;
|
|
@@ -51,6 +56,8 @@ export interface WorldVocabulary {
|
|
|
51
56
|
}
|
|
52
57
|
export interface WorldEngagement {
|
|
53
58
|
readonly id: string;
|
|
59
|
+
/** The project's own code in its Graph (`metadata.code`), or null when it has none. */
|
|
60
|
+
readonly code: string | null;
|
|
54
61
|
readonly name: string;
|
|
55
62
|
readonly summary: string | null;
|
|
56
63
|
readonly status: string | null;
|
|
@@ -109,7 +116,7 @@ export declare function projectWorldFromGraph(graph: Pick<GraphFileV2, 'entities
|
|
|
109
116
|
* then unclassified projects; statuses are the common lifecycle words with the
|
|
110
117
|
* host's on top.
|
|
111
118
|
*/
|
|
112
|
-
export declare function resolveWorldVocabulary(config: WorldVocabularyConfig | undefined, businesses: readonly WorldBusiness[]): WorldVocabulary;
|
|
119
|
+
export declare function resolveWorldVocabulary(config: WorldVocabularyConfig | undefined, businesses: readonly WorldBusiness[], terms?: readonly GraphVocabularyTerm[] | null | undefined): WorldVocabulary;
|
|
113
120
|
/** Reads and checks a vocabulary file; an invalid file is an error, never silently ignored. */
|
|
114
121
|
export declare function readWorldVocabulary(path: string): Promise<WorldVocabularyConfig>;
|
|
115
122
|
/**
|
|
@@ -121,6 +128,19 @@ export declare function readWorldBusinesses(context: Pick<LocalWebModuleContext,
|
|
|
121
128
|
readonly vocabulary?: WorldVocabularyConfig;
|
|
122
129
|
readonly now?: () => Date;
|
|
123
130
|
}): Promise<WorldBusinessesResponse>;
|
|
131
|
+
/**
|
|
132
|
+
* The world of an organization from the project and glossary-term records its Graph API returns, for an
|
|
133
|
+
* organization web that hosts the same world screen (P16). Pure: the caller reads the records with the
|
|
134
|
+
* member's own grants, so the world shows only what that member may read.
|
|
135
|
+
*/
|
|
136
|
+
export declare function projectOrganizationWorld(records: {
|
|
137
|
+
readonly projects: readonly unknown[];
|
|
138
|
+
readonly glossaryTerms?: readonly unknown[];
|
|
139
|
+
}, options: {
|
|
140
|
+
readonly server: string;
|
|
141
|
+
readonly vocabulary?: WorldVocabularyConfig;
|
|
142
|
+
readonly now?: Date;
|
|
143
|
+
}): WorldBusinessesResponse;
|
|
124
144
|
export interface WorldJudgmentPlace {
|
|
125
145
|
readonly decision_attempt_id: string;
|
|
126
146
|
/** Repository the judgment Host recorded for the turn (`turn-input.json` project_code); null when unrecorded. */
|
|
@@ -142,6 +162,21 @@ export declare function readJudgmentPlaces(journalRoot: string, options?: {
|
|
|
142
162
|
readonly places: readonly WorldJudgmentPlace[];
|
|
143
163
|
readonly reason?: string;
|
|
144
164
|
}>;
|
|
165
|
+
/**
|
|
166
|
+
* The judgments of one organization, for a world drawn from that organization's Graph. A judgment
|
|
167
|
+
* belongs to the organization only when the repository it was worked in is one of the organization's
|
|
168
|
+
* businesses (a registered repository or the business code, the same rule as the world's placement).
|
|
169
|
+
* Every other judgment (another company's work, or one whose repository was not recorded) is withheld:
|
|
170
|
+
* only their count leaves the host, never their questions or repositories.
|
|
171
|
+
*/
|
|
172
|
+
export declare function readOrganizationJudgments(context: Pick<LocalWebModuleContext, 'journalRoot' | 'journalCache' | 'dataDir' | 'now'>, businesses: readonly Pick<WorldBusiness, 'code' | 'repositories'>[], workspaces?: Map<string, string | null>): Promise<{
|
|
173
|
+
readonly status: 'available' | 'unavailable';
|
|
174
|
+
readonly scope: 'organization';
|
|
175
|
+
readonly withheld: number;
|
|
176
|
+
readonly places: readonly WorldJudgmentPlace[];
|
|
177
|
+
readonly home: unknown;
|
|
178
|
+
readonly reason?: string;
|
|
179
|
+
}>;
|
|
145
180
|
export declare function createWorldExtension(options?: {
|
|
146
181
|
readonly vocabulary?: WorldVocabularyConfig;
|
|
147
182
|
}): LocalWebExtension;
|
package/dist/world-extension.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
-
import { readJudgmentValueProofJournal } from './judgment-value-proof-review.js';
|
|
3
|
+
import { buildJudgmentValueProofReviewHome, readJudgmentValueProofAnswers, readJudgmentValueProofFeedback, readJudgmentValueProofJournal, } from './judgment-value-proof-review.js';
|
|
4
|
+
import { publicHome } from './value-proof-review-http.js';
|
|
5
|
+
import { projectOrganizationGraph, projectVocabularyTerms } from './organization-graph-web.js';
|
|
4
6
|
import { readLocalGraphState } from './local-web-host.js';
|
|
5
7
|
import { loadPersonalOs } from './ssot.js';
|
|
6
8
|
import { requestUrl, writeJson } from './local-web-security.js';
|
|
@@ -38,6 +40,7 @@ function text(value) {
|
|
|
38
40
|
function engagementOf(entity) {
|
|
39
41
|
return {
|
|
40
42
|
id: entity.id,
|
|
43
|
+
code: text(entity.metadata?.code),
|
|
41
44
|
name: entity.name,
|
|
42
45
|
summary: text(entity.summary) ?? text(entity.metadata?.goal),
|
|
43
46
|
status: text(entity.metadata?.status),
|
|
@@ -114,22 +117,31 @@ export function projectWorldFromGraph(graph, now = new Date()) {
|
|
|
114
117
|
* then unclassified projects; statuses are the common lifecycle words with the
|
|
115
118
|
* host's on top.
|
|
116
119
|
*/
|
|
117
|
-
export function resolveWorldVocabulary(config, businesses) {
|
|
120
|
+
export function resolveWorldVocabulary(config, businesses, terms = undefined) {
|
|
121
|
+
// The organization names its kinds in the Graph; the host's file only adds a look (form, colour).
|
|
122
|
+
const graphTerms = new Map((terms ?? []).filter((term) => term.field === 'project.kind').map((term) => [term.value, term]));
|
|
123
|
+
const named = (key, configLabel) => {
|
|
124
|
+
const term = graphTerms.get(key);
|
|
125
|
+
if (term)
|
|
126
|
+
return { label: term.label, label_source: 'graph', definition: term.definition };
|
|
127
|
+
const label = text(configLabel);
|
|
128
|
+
return label ? { label, label_source: 'config', definition: null } : { label: key, label_source: 'value', definition: null };
|
|
129
|
+
};
|
|
118
130
|
const configured = Object.entries(config?.kinds ?? {});
|
|
119
131
|
const used = new Set(businesses.map((business) => business.kind));
|
|
120
132
|
const kinds = configured.map(([key, entry], index) => ({
|
|
121
133
|
key,
|
|
122
|
-
|
|
134
|
+
...named(key, entry.label),
|
|
123
135
|
form: entry.form && BUILDING_FORMS.includes(entry.form) ? entry.form : 'office',
|
|
124
136
|
color: entry.color && HEX_COLOR.test(entry.color) ? entry.color : KIND_PALETTE[index % KIND_PALETTE.length],
|
|
125
137
|
configured: true,
|
|
126
138
|
}));
|
|
127
|
-
const
|
|
128
|
-
[...used].filter((key) => key !== null && !
|
|
129
|
-
kinds.push({ key,
|
|
139
|
+
const configuredKeys = new Set(configured.map(([key]) => key));
|
|
140
|
+
[...used].filter((key) => key !== null && !configuredKeys.has(key)).sort().forEach((key) => {
|
|
141
|
+
kinds.push({ key, ...named(key, undefined), form: 'office', color: KIND_PALETTE[kinds.length % KIND_PALETTE.length], configured: false });
|
|
130
142
|
});
|
|
131
143
|
if (used.has(null))
|
|
132
|
-
kinds.push({ key: null, label: '分類なし', form: 'office', color: UNCLASSIFIED_COLOR, configured: false });
|
|
144
|
+
kinds.push({ key: null, label: '分類なし', label_source: 'value', definition: null, form: 'office', color: UNCLASSIFIED_COLOR, configured: false });
|
|
133
145
|
const statuses = new Map(Object.entries(DEFAULT_STATUSES));
|
|
134
146
|
for (const [key, entry] of Object.entries(config?.statuses ?? {})) {
|
|
135
147
|
const base = statuses.get(key);
|
|
@@ -138,7 +150,7 @@ export function resolveWorldVocabulary(config, businesses) {
|
|
|
138
150
|
phase: entry.phase && LIFECYCLE_PHASES.includes(entry.phase) ? entry.phase : base?.phase ?? 'active',
|
|
139
151
|
});
|
|
140
152
|
}
|
|
141
|
-
return { kinds, statuses: [...statuses].map(([key, entry]) => ({ key, ...entry })) };
|
|
153
|
+
return { kinds, statuses: [...statuses].map(([key, entry]) => ({ key, ...entry })), terms: terms === undefined ? 'none' : terms === null ? 'unavailable' : 'read' };
|
|
142
154
|
}
|
|
143
155
|
/** Reads and checks a vocabulary file; an invalid file is an error, never silently ignored. */
|
|
144
156
|
export async function readWorldVocabulary(path) {
|
|
@@ -174,6 +186,7 @@ export async function readWorldBusinesses(context, options = {}) {
|
|
|
174
186
|
const now = (options.now ?? (() => new Date()))();
|
|
175
187
|
let graph;
|
|
176
188
|
let source;
|
|
189
|
+
let terms;
|
|
177
190
|
try {
|
|
178
191
|
if (context.organizationGraph) {
|
|
179
192
|
if (!context.organizationGraph.readGraphFile) {
|
|
@@ -181,6 +194,7 @@ export async function readWorldBusinesses(context, options = {}) {
|
|
|
181
194
|
}
|
|
182
195
|
graph = await context.organizationGraph.readGraphFile();
|
|
183
196
|
source = { authority: 'organization_graph', server: context.organizationGraph.server };
|
|
197
|
+
terms = context.organizationGraph.readVocabularyTerms ? await context.organizationGraph.readVocabularyTerms().catch(() => null) : null;
|
|
184
198
|
}
|
|
185
199
|
else {
|
|
186
200
|
const state = await readLocalGraphState(context.dataDir);
|
|
@@ -205,7 +219,29 @@ export async function readWorldBusinesses(context, options = {}) {
|
|
|
205
219
|
version: WORLD_EXTENSION_VERSION,
|
|
206
220
|
source,
|
|
207
221
|
as_of: now.toISOString(),
|
|
208
|
-
vocabulary: resolveWorldVocabulary(options.vocabulary, projection.businesses),
|
|
222
|
+
vocabulary: resolveWorldVocabulary(options.vocabulary, projection.businesses, terms),
|
|
223
|
+
...projection,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* The world of an organization from the project and glossary-term records its Graph API returns, for an
|
|
228
|
+
* organization web that hosts the same world screen (P16). Pure: the caller reads the records with the
|
|
229
|
+
* member's own grants, so the world shows only what that member may read.
|
|
230
|
+
*/
|
|
231
|
+
export function projectOrganizationWorld(records, options) {
|
|
232
|
+
const now = options.now ?? new Date();
|
|
233
|
+
const { graph } = projectOrganizationGraph({
|
|
234
|
+
entities: { person: [], org: [], project: records.projects, decision: [], raci_assignment: [] },
|
|
235
|
+
memberOf: [],
|
|
236
|
+
assignedTo: [],
|
|
237
|
+
});
|
|
238
|
+
const projection = projectWorldFromGraph(graph, now);
|
|
239
|
+
return {
|
|
240
|
+
status: 'ok',
|
|
241
|
+
version: WORLD_EXTENSION_VERSION,
|
|
242
|
+
source: { authority: 'organization_graph', server: options.server },
|
|
243
|
+
as_of: now.toISOString(),
|
|
244
|
+
vocabulary: resolveWorldVocabulary(options.vocabulary, projection.businesses, projectVocabularyTerms(records.glossaryTerms ?? [])),
|
|
209
245
|
...projection,
|
|
210
246
|
};
|
|
211
247
|
}
|
|
@@ -233,6 +269,44 @@ export async function readJudgmentPlaces(journalRoot, options = {}) {
|
|
|
233
269
|
}));
|
|
234
270
|
return { status: 'available', places };
|
|
235
271
|
}
|
|
272
|
+
/**
|
|
273
|
+
* The judgments of one organization, for a world drawn from that organization's Graph. A judgment
|
|
274
|
+
* belongs to the organization only when the repository it was worked in is one of the organization's
|
|
275
|
+
* businesses (a registered repository or the business code, the same rule as the world's placement).
|
|
276
|
+
* Every other judgment (another company's work, or one whose repository was not recorded) is withheld:
|
|
277
|
+
* only their count leaves the host, never their questions or repositories.
|
|
278
|
+
*/
|
|
279
|
+
export async function readOrganizationJudgments(context, businesses, workspaces = new Map()) {
|
|
280
|
+
const journal = await readJudgmentValueProofJournal({ root: context.journalRoot, cache: context.journalCache });
|
|
281
|
+
if (journal.status !== 'available')
|
|
282
|
+
return { status: 'unavailable', scope: 'organization', withheld: 0, places: [], home: null, reason: 'journal_unreadable' };
|
|
283
|
+
const belongs = (workspace) => workspace !== null
|
|
284
|
+
&& businesses.some((business) => business.repositories.includes(workspace) || business.code === workspace);
|
|
285
|
+
const kept = [];
|
|
286
|
+
const places = [];
|
|
287
|
+
for (const entry of journal.entries) {
|
|
288
|
+
const workspace = await entryWorkspace(entry.file, workspaces);
|
|
289
|
+
if (!belongs(workspace))
|
|
290
|
+
continue;
|
|
291
|
+
kept.push(entry);
|
|
292
|
+
places.push({ decision_attempt_id: entry.proof.decision_attempt_id, workspace });
|
|
293
|
+
}
|
|
294
|
+
const [feedback, answers] = await Promise.all([
|
|
295
|
+
readJudgmentValueProofFeedback({ dataDir: context.dataDir }),
|
|
296
|
+
readJudgmentValueProofAnswers({ dataDir: context.dataDir }),
|
|
297
|
+
]);
|
|
298
|
+
// Rejected files are left out too: their names would say nothing about the organization they belong to.
|
|
299
|
+
const home = buildJudgmentValueProofReviewHome({ ...journal, entries: kept, rejected: [] }, feedback, { now: context.now(), answers });
|
|
300
|
+
return { status: 'available', scope: 'organization', withheld: journal.entries.length - kept.length, places, home: publicHome(home) };
|
|
301
|
+
}
|
|
302
|
+
async function entryWorkspace(valueProofFile, workspaces) {
|
|
303
|
+
const turnInputFile = `${valueProofFile.slice(0, -VALUE_PROOF_SUFFIX.length)}.turn-input.json`;
|
|
304
|
+
if (!workspaces.has(turnInputFile)) {
|
|
305
|
+
const turnInput = await readJsonFile(turnInputFile);
|
|
306
|
+
workspaces.set(turnInputFile, isRecord(turnInput) ? text(turnInput.project_code) : null);
|
|
307
|
+
}
|
|
308
|
+
return workspaces.get(turnInputFile) ?? null;
|
|
309
|
+
}
|
|
236
310
|
async function readJsonFile(path) {
|
|
237
311
|
try {
|
|
238
312
|
return JSON.parse(await readFile(path, 'utf8'));
|
|
@@ -255,6 +329,16 @@ function createWorldModule(context, read) {
|
|
|
255
329
|
writeJson(response, 200, await read());
|
|
256
330
|
return true;
|
|
257
331
|
}
|
|
332
|
+
if (path === `/api/extensions/${WORLD_EXTENSION_ID}/organization-judgments`) {
|
|
333
|
+
response.setHeader('Cache-Control', 'no-store');
|
|
334
|
+
const world = await read();
|
|
335
|
+
if (world.status !== 'ok' || world.source.authority !== 'organization_graph') {
|
|
336
|
+
writeJson(response, 200, { version: WORLD_EXTENSION_VERSION, status: 'unavailable', scope: 'organization', withheld: 0, places: [], home: null, reason: 'not_reading_organization_graph' });
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
writeJson(response, 200, { version: WORLD_EXTENSION_VERSION, ...(await readOrganizationJudgments(context, world.businesses, workspaces)) });
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
258
342
|
if (path === `/api/extensions/${WORLD_EXTENSION_ID}/judgment-places`) {
|
|
259
343
|
response.setHeader('Cache-Control', 'no-store');
|
|
260
344
|
writeJson(response, 200, { version: WORLD_EXTENSION_VERSION, ...(await readJudgmentPlaces(context.journalRoot, { cache: context.journalCache, workspaces })) });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unson/brainbase-mcp",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.4",
|
|
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",
|
|
@@ -147,6 +147,10 @@
|
|
|
147
147
|
"types": "./dist/world-model.d.ts",
|
|
148
148
|
"import": "./dist/world-model.js"
|
|
149
149
|
},
|
|
150
|
+
"./world": {
|
|
151
|
+
"types": "./dist/world-extension.d.ts",
|
|
152
|
+
"import": "./dist/world-extension.js"
|
|
153
|
+
},
|
|
150
154
|
"./problem-selection": {
|
|
151
155
|
"types": "./dist/problem-selection.d.ts",
|
|
152
156
|
"import": "./dist/problem-selection.js"
|
|
@@ -354,5 +358,5 @@
|
|
|
354
358
|
"engines": {
|
|
355
359
|
"node": ">=20"
|
|
356
360
|
},
|
|
357
|
-
"gitHead": "
|
|
361
|
+
"gitHead": "1a548f70b900633c8cdcfe3c468de0a082b156fe"
|
|
358
362
|
}
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
graphPageContext,
|
|
35
35
|
graphStateNotice,
|
|
36
36
|
hostReadOnlyNote,
|
|
37
|
+
hostSourceNotice,
|
|
37
38
|
isEdgeView,
|
|
38
39
|
isEntityView,
|
|
39
40
|
isRecord,
|
|
@@ -507,7 +508,10 @@ export function createGraphProjectsView({
|
|
|
507
508
|
const legacyChrome = [pageHeader];
|
|
508
509
|
const children = [pageHeader];
|
|
509
510
|
const dir = state.list.state === 'ok' ? textOrNull(state.list.payload.source?.dataDir) : null;
|
|
510
|
-
|
|
511
|
+
// The record a host's 出典 notice may point at: the selected project once its detail is read.
|
|
512
|
+
const sourceDetail = state.detail?.id === state.selectedId && state.detail.state === 'ok' ? state.detail.payload.project : null;
|
|
513
|
+
const sourceSubject = sourceDetail ? { id: sourceDetail.id, type: 'project', metadata: sourceDetail.metadata ?? {} } : null;
|
|
514
|
+
const sourceBlock = workspaceHostNotice(doc, hostSourceNotice(sourceNotice, sourceSubject)) ?? workspaceNotice(doc, {
|
|
511
515
|
label: '出典',
|
|
512
516
|
text: `このMacのGraph${dir ? `(${dir})` : ''}から読み、ここで直した内容もそこへ保存します。関係者は、プロジェクトに参加している人や責任を持つ人の記録で、ログインや共有の設定ではありません。`,
|
|
513
517
|
});
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
graphPageContext,
|
|
36
36
|
graphStateNotice,
|
|
37
37
|
hostReadOnlyNote,
|
|
38
|
+
hostSourceNotice,
|
|
38
39
|
isEdgeView,
|
|
39
40
|
isEntityView,
|
|
40
41
|
isRecord,
|
|
@@ -444,6 +445,12 @@ export function createGraphRegistryView({
|
|
|
444
445
|
return section;
|
|
445
446
|
}
|
|
446
447
|
|
|
448
|
+
/** The record a host's 出典 notice may point at: the selected record once it is read. */
|
|
449
|
+
function sourceSubject() {
|
|
450
|
+
const entity = state.detail?.state === 'ok' ? state.detail.payload.entity : null;
|
|
451
|
+
return entity ? { id: entity.id, type: entity.type, metadata: entity.metadata ?? {} } : null;
|
|
452
|
+
}
|
|
453
|
+
|
|
447
454
|
function renderWorkspace() {
|
|
448
455
|
const dir = state.search.state === 'ok' ? textOrNull(state.search.payload.source?.dataDir) : null;
|
|
449
456
|
return [
|
|
@@ -453,7 +460,7 @@ export function createGraphRegistryView({
|
|
|
453
460
|
lead: writable ? 'Graphに何がどう登録されているかを確かめ、誤りを直します。' : 'Graphに何がどう登録されているかを確かめます。',
|
|
454
461
|
source: context.source,
|
|
455
462
|
}),
|
|
456
|
-
workspaceHostNotice(doc, sourceNotice) ?? workspaceNotice(doc, {
|
|
463
|
+
workspaceHostNotice(doc, hostSourceNotice(sourceNotice, sourceSubject())) ?? workspaceNotice(doc, {
|
|
457
464
|
label: '出典',
|
|
458
465
|
text: `このMacのGraph${dir ? `(${dir})` : ''}だけを表示します。ここで直した内容は、同じGraphを読むMCPの search・get_context・resolve_entity で次から使われます。`,
|
|
459
466
|
}),
|
package/ui/graph-view-shared.js
CHANGED
|
@@ -459,6 +459,55 @@ function withinCorrectionScope(scope, body, recordType) {
|
|
|
459
459
|
return scope.entity(recordType) && scope.fields(changed).length === changed.length;
|
|
460
460
|
}
|
|
461
461
|
|
|
462
|
+
/**
|
|
463
|
+
* A host's 出典 notice with its text resolved for the selected record: the host may pass `text` as a
|
|
464
|
+
* function of that record (`{ id, type, metadata }`, or null before one is read), for a notice that points
|
|
465
|
+
* at it. Returns the notice for `workspaceHostNotice`, or the value it was given.
|
|
466
|
+
*/
|
|
467
|
+
export function hostSourceNotice(sourceNotice, subject) {
|
|
468
|
+
if (!sourceNotice || typeof sourceNotice.text !== 'function') return sourceNotice;
|
|
469
|
+
return { ...sourceNotice, text: sourceNotice.text(subject ?? null) };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// ---------------------------------------------------------------------------
|
|
473
|
+
// The organization's web (Inspector UX-20261004-01)
|
|
474
|
+
|
|
475
|
+
const ORGANIZATION_RECORD_ID = /^[A-Za-z0-9_.:-]{1,200}$/u;
|
|
476
|
+
|
|
477
|
+
/** The organization web's origin the host was told about, or null. */
|
|
478
|
+
export function organizationWebOrigin(organizationGraph) {
|
|
479
|
+
if (organizationGraph?.status !== 'connected') return null;
|
|
480
|
+
const origin = organizationGraph.web_url;
|
|
481
|
+
return typeof origin === 'string' && /^https:\/\/[^/?#@\s]+$/u.test(origin) ? origin : null;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Where a record opens in the organization web, `{ href, label }`, or null when it has no usable id. The
|
|
486
|
+
* organization web opens 情報と関係 by record id and プロジェクトと関係者 by project code, and keeps that record
|
|
487
|
+
* across sign-in. A project opens by its own code only: a sub-project's scope code is its parent's and
|
|
488
|
+
* would open the parent, so without its own code the record opens by id.
|
|
489
|
+
*/
|
|
490
|
+
export function organizationRecordLink(origin, screenId, record) {
|
|
491
|
+
const id = typeof record?.id === 'string' && ORGANIZATION_RECORD_ID.test(record.id) ? record.id : null;
|
|
492
|
+
const own = typeof record?.metadata?.code === 'string' ? record.metadata.code.trim() : '';
|
|
493
|
+
const code = own && ORGANIZATION_RECORD_ID.test(own) ? own : null;
|
|
494
|
+
if (screenId === 'projects' && record?.type === 'project' && code) {
|
|
495
|
+
return { href: `${origin}/?screen=projects&project=${encodeURIComponent(code)}`, label: '「プロジェクトと関係者」' };
|
|
496
|
+
}
|
|
497
|
+
return id ? { href: `${origin}/?screen=graph&entity_id=${encodeURIComponent(id)}`, label: '「情報と関係」' } : null;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export const ORGANIZATION_RECORD_LINK_TEXT = '組織版でこの記録を開く';
|
|
501
|
+
|
|
502
|
+
/** A link that opens `target` from organizationRecordLink in a new tab. */
|
|
503
|
+
export function organizationRecordAnchor(doc, target, className) {
|
|
504
|
+
return makeElement(doc, 'a', {
|
|
505
|
+
...(className ? { className } : {}),
|
|
506
|
+
text: ORGANIZATION_RECORD_LINK_TEXT,
|
|
507
|
+
attrs: { href: target.href, target: '_blank', rel: 'noopener noreferrer' },
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
462
511
|
/**
|
|
463
512
|
* A host's note that corrections are not available here, or null when it passed none. The host may
|
|
464
513
|
* pass a function of the selected record (`{ id, type, metadata }`) returning a string or an element,
|
package/ui/local-web-shell.js
CHANGED
|
@@ -21,7 +21,7 @@ import { createObjectiveEditorController } from './objective-editor.js';
|
|
|
21
21
|
import { createObjectiveEditorHttpPort } from './objective-editor-http-port.js';
|
|
22
22
|
import { createValueProofReviewUI } from './value-proof-review.js';
|
|
23
23
|
import { createWorldModelView } from './world-model-view.js';
|
|
24
|
-
import { createGraphClient } from './graph-view-shared.js';
|
|
24
|
+
import { createGraphClient, organizationRecordAnchor, organizationRecordLink, organizationWebOrigin } from './graph-view-shared.js';
|
|
25
25
|
|
|
26
26
|
export const LOCAL_WEB_SHELL_CONTRACT_VERSION = 'brainbase.local-web-shell.v1';
|
|
27
27
|
|
|
@@ -205,20 +205,8 @@ function mountObjectives(container, context) {
|
|
|
205
205
|
|
|
206
206
|
const ORGANIZATION_READ_ONLY_NOTE = '組織のGraphを読み取り専用で表示しています。登録の訂正は組織版で行ってください。';
|
|
207
207
|
// What the owner needs before leaving for the organization web (Inspector UX-20261004-01): a link to this
|
|
208
|
-
// very record there, who may correct it, and what happens after saving.
|
|
209
|
-
|
|
210
|
-
const ORGANIZATION_RECORD_ID = /^[A-Za-z0-9_.:-]{1,200}$/u;
|
|
211
|
-
export function organizationRecordLink(origin, screenId, record) {
|
|
212
|
-
const id = typeof record?.id === 'string' && ORGANIZATION_RECORD_ID.test(record.id) ? record.id : null;
|
|
213
|
-
// The organization web picks a project by its own code. Only that code is used: a sub-project's scope
|
|
214
|
-
// code is its parent's and would open the parent, so without its own code the record opens by id.
|
|
215
|
-
const own = typeof record?.metadata?.code === 'string' ? record.metadata.code.trim() : '';
|
|
216
|
-
const code = own && ORGANIZATION_RECORD_ID.test(own) ? own : null;
|
|
217
|
-
if (screenId === 'projects' && record?.type === 'project' && code) {
|
|
218
|
-
return { href: `${origin}/?screen=projects&project=${encodeURIComponent(code)}`, label: '「プロジェクトと関係者」' };
|
|
219
|
-
}
|
|
220
|
-
return id ? { href: `${origin}/?screen=graph&entity_id=${encodeURIComponent(id)}`, label: '「情報と関係」' } : null;
|
|
221
|
-
}
|
|
208
|
+
// very record there, who may correct it, and what happens after saving.
|
|
209
|
+
export { organizationRecordLink };
|
|
222
210
|
const organizationReadOnlyNoteLinked = (doc, origin, screenId) => (record) => {
|
|
223
211
|
const target = organizationRecordLink(origin, screenId, record);
|
|
224
212
|
if (!target) return ORGANIZATION_READ_ONLY_NOTE;
|
|
@@ -226,27 +214,32 @@ const organizationReadOnlyNoteLinked = (doc, origin, screenId) => (record) => {
|
|
|
226
214
|
note.append(
|
|
227
215
|
makeElement(doc, 'span', { text: [
|
|
228
216
|
'組織のGraphを読み取り専用で表示しています。ここでは直せません。訂正の手順:',
|
|
229
|
-
`①
|
|
230
|
-
'② 直せるのは、組織版での役割がgmかceo
|
|
217
|
+
`① 下のリンク(上の「出典」にも同じリンクがあります)から、組織版の${target.label}でこの記録を開きます。ログインしていなければSlackでログインすると、そのままこの記録に戻ります。`,
|
|
218
|
+
'② 直せるのは、組織版での役割がgmかceoで、このプロジェクトが属する事業(事業そのものなら、その事業)を許可されている人です。直せるのはプロジェクトの記録(名前・別名・要約・期間・目的・状態)だけで、人物の記録と、関係者の追加・終了は組織版でもまだ直せません。',
|
|
231
219
|
'③ 保存すると、理由といっしょに組織のGraphへ書き込まれ、組織版がその場で読み戻して表示します。この画面には、1分ほどたってから再読み込みすると反映されます。',
|
|
232
220
|
'',
|
|
233
221
|
].join('\n') }),
|
|
234
|
-
|
|
222
|
+
organizationRecordAnchor(doc, target),
|
|
235
223
|
);
|
|
236
224
|
return note;
|
|
237
225
|
};
|
|
238
226
|
|
|
239
|
-
/**
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
227
|
+
/**
|
|
228
|
+
* The 出典 notice of a screen reading the organization Graph. Once a record is selected its link opens
|
|
229
|
+
* that record in the organization web; before that, the screen there.
|
|
230
|
+
*/
|
|
231
|
+
const organizationSourceText = (doc, origin, screenId, sourceText) => (record) => {
|
|
232
|
+
const target = record ? organizationRecordLink(origin, screenId, record) : null;
|
|
233
|
+
const link = target
|
|
234
|
+
? organizationRecordAnchor(doc, target)
|
|
235
|
+
: makeElement(doc, 'a', {
|
|
236
|
+
text: '組織版で開いて訂正する',
|
|
237
|
+
attrs: { href: `${origin}/?screen=${encodeURIComponent(screenId)}`, target: '_blank', rel: 'noopener noreferrer' },
|
|
238
|
+
});
|
|
239
|
+
const source = makeElement(doc, 'span');
|
|
240
|
+
source.append(makeElement(doc, 'span', { text: `${sourceText} ` }), link);
|
|
241
|
+
return source;
|
|
242
|
+
};
|
|
250
243
|
|
|
251
244
|
function mountGraphScreen(screenId, createView) {
|
|
252
245
|
return (container, context) => {
|
|
@@ -254,15 +247,13 @@ function mountGraphScreen(screenId, createView) {
|
|
|
254
247
|
makePage(container, context).append(viewRoot);
|
|
255
248
|
// Ledger C1: when the host reads the organization Graph, nothing here can be corrected.
|
|
256
249
|
const readOnly = context.organizationGraph?.status === 'connected';
|
|
257
|
-
const
|
|
250
|
+
const origin = readOnly ? organizationWebOrigin(context.organizationGraph) : null;
|
|
258
251
|
const sourceText = `組織のGraph(${context.organizationGraph?.server ?? '接続先不明'})を読み取り専用で表示しています。このMacのGraphではありません。`;
|
|
259
|
-
const source = link ? makeElement(context.document, 'span') : null;
|
|
260
|
-
source?.append(makeElement(context.document, 'span', { text: `${sourceText} ` }), link);
|
|
261
252
|
return createView({
|
|
262
253
|
...(readOnly ? {
|
|
263
254
|
canCorrect: false,
|
|
264
|
-
readOnlyNote:
|
|
265
|
-
sourceNotice: { label: '出典', text:
|
|
255
|
+
readOnlyNote: origin ? organizationReadOnlyNoteLinked(context.document, origin, screenId) : ORGANIZATION_READ_ONLY_NOTE,
|
|
256
|
+
sourceNotice: { label: '出典', text: origin ? organizationSourceText(context.document, origin, screenId, sourceText) : sourceText },
|
|
266
257
|
} : {}),
|
|
267
258
|
root: viewRoot,
|
|
268
259
|
rail: context.rail,
|
package/ui/value-proof-review.js
CHANGED
|
@@ -916,6 +916,11 @@ function renderJudgmentDetail(doc, state, item, callbacks) {
|
|
|
916
916
|
const { proof } = item;
|
|
917
917
|
const detail = [];
|
|
918
918
|
const row = rowOfItem(state, itemKey(proof));
|
|
919
|
+
const proofKind = kindLabel(proof);
|
|
920
|
+
// Keep a judgment-kind detail aligned with the row that groups it.
|
|
921
|
+
const displayedKind = row?.source === 'judgment_kind'
|
|
922
|
+
? rowName(row)
|
|
923
|
+
: proofKind ? proofKind : row ? rowName(row) : null;
|
|
919
924
|
if (row) {
|
|
920
925
|
const back = workspaceButton(doc, { text: `← ${rowName(row)}`, variant: 'quiet', onClick: () => callbacks.onShowRow?.() });
|
|
921
926
|
back.className = `${back.className} bb-vpr-back`;
|
|
@@ -924,7 +929,7 @@ function renderJudgmentDetail(doc, state, item, callbacks) {
|
|
|
924
929
|
const head = workspaceRailHead(doc, {
|
|
925
930
|
kicker: itemStateLabel(item),
|
|
926
931
|
title: itemTitle(proof),
|
|
927
|
-
sub:
|
|
932
|
+
sub: displayedKind ? `判断の種類: ${displayedKind}` : null,
|
|
928
933
|
});
|
|
929
934
|
const heading = Array.from(head.children ?? []).find((child) => child.tagName === 'H2');
|
|
930
935
|
heading?.setAttribute('tabindex', '-1');
|
|
@@ -43,3 +43,24 @@ export function groupJudgmentPlaces(places, businesses) {
|
|
|
43
43
|
}
|
|
44
44
|
return { byBusiness, unplaced };
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The lots around a city's landmark for `count` districts: a square grid of `step` spacing whose cells
|
|
49
|
+
* outside the landmark's lot hold at least `count` districts (a 2x2 grid lies wholly inside it).
|
|
50
|
+
*/
|
|
51
|
+
export function districtLots(count, step = 2.5) {
|
|
52
|
+
let cols = Math.max(2, Math.ceil(Math.sqrt(count + 1)));
|
|
53
|
+
for (;;) {
|
|
54
|
+
const cells = [];
|
|
55
|
+
for (let row = 0; row < cols; row += 1) {
|
|
56
|
+
for (let col = 0; col < cols; col += 1) {
|
|
57
|
+
const cx = -((cols - 1) * step) / 2 + col * step;
|
|
58
|
+
const cz = -((cols - 1) * step) / 2 + row * step;
|
|
59
|
+
if (Math.abs(cx) < 2.2 && Math.abs(cz) < 2.2) continue; // the landmark's lot
|
|
60
|
+
cells.push([cx, cz]);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (cells.length >= count) return { cols, cells };
|
|
64
|
+
cols += 1;
|
|
65
|
+
}
|
|
66
|
+
}
|
package/ui/world/world-view.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { THREE, MapControls } from './world-vendor.js';
|
|
18
|
-
import { groupJudgmentPlaces, UNPLACED_REASON_TEXT } from './world-placement.js';
|
|
18
|
+
import { districtLots, groupJudgmentPlaces, UNPLACED_REASON_TEXT } from './world-placement.js';
|
|
19
19
|
import {
|
|
20
20
|
makeWorkspaceElement as el,
|
|
21
21
|
workspacePageHeader,
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
workspaceDetailEmpty,
|
|
26
26
|
workspaceButton,
|
|
27
27
|
} from '../../workspace-kit.js';
|
|
28
|
+
import { organizationRecordAnchor, organizationRecordLink, organizationWebOrigin } from '../../graph-view-shared.js';
|
|
28
29
|
|
|
29
30
|
export const WORLD_VIEW_CONTRACT_VERSION = 'brainbase.world-view.v0';
|
|
30
31
|
|
|
@@ -52,7 +53,7 @@ const BUSINESS_STATE_TEXT = Object.freeze({
|
|
|
52
53
|
*/
|
|
53
54
|
let worldVocabulary = { kinds: [], statuses: [] };
|
|
54
55
|
const kindEntry = (key) => worldVocabulary.kinds.find((entry) => entry.key === (key ?? null))
|
|
55
|
-
?? { key: key ?? null, label: key ?? '分類なし', form: 'office', color: '#69746d' };
|
|
56
|
+
?? { key: key ?? null, label: key ?? '分類なし', definition: null, form: 'office', color: '#69746d' };
|
|
56
57
|
const kindColor = (key) => Number.parseInt(kindEntry(key).color.slice(1), 16);
|
|
57
58
|
const statusEntry = (status) => worldVocabulary.statuses.find((entry) => entry.key === status) ?? null;
|
|
58
59
|
const statusPhase = (status) => statusEntry(status)?.phase ?? 'active';
|
|
@@ -504,17 +505,8 @@ function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear }
|
|
|
504
505
|
cityLabel.node.style.borderColor = `${entry.color}80`;
|
|
505
506
|
cityLabel.business = business;
|
|
506
507
|
const n = business.engagements.length;
|
|
507
|
-
const cols = Math.max(2, Math.ceil(Math.sqrt(n + 1)));
|
|
508
508
|
const step = 2.5;
|
|
509
|
-
const cells =
|
|
510
|
-
for (let row = 0; row < cols; row += 1) {
|
|
511
|
-
for (let col = 0; col < cols; col += 1) {
|
|
512
|
-
const cx = -((cols - 1) * step) / 2 + col * step;
|
|
513
|
-
const cz = -((cols - 1) * step) / 2 + row * step;
|
|
514
|
-
if (Math.abs(cx) < 2.2 && Math.abs(cz) < 2.2) continue; // the landmark's lot
|
|
515
|
-
cells.push([cx, cz]);
|
|
516
|
-
}
|
|
517
|
-
}
|
|
509
|
+
const { cols, cells } = districtLots(n, step);
|
|
518
510
|
cells.sort((a, b) => Math.hypot(...a) - Math.hypot(...b));
|
|
519
511
|
business.engagements.forEach((engagement, index) => {
|
|
520
512
|
const [cx, cz] = cells[index % cells.length];
|
|
@@ -785,7 +777,18 @@ function renderFallbackList(doc, root, businesses, rows) {
|
|
|
785
777
|
root.append(list);
|
|
786
778
|
}
|
|
787
779
|
|
|
788
|
-
|
|
780
|
+
/** Why the host has no judgments to give, in the words the screen shows. */
|
|
781
|
+
const JUDGMENT_UNAVAILABLE_TEXT = Object.freeze({
|
|
782
|
+
judgment_journal_not_connected: '判断の記録は各メンバーのMacにあり、組織版には送らないため未接続です',
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* @param {object} options
|
|
787
|
+
* @param {(project: { id: string, code?: string | null }) => string} [options.projectHref] Where a business or
|
|
788
|
+
* engagement opens in the host's プロジェクトと関係者. The local web opens `#projects?project=<id>`; an
|
|
789
|
+
* organization web passes its own route.
|
|
790
|
+
*/
|
|
791
|
+
export function createWorldView({ root, rail, page, document: explicitDocument, fetcher, organizationGraph = null, projectHref = (project) => `#projects?project=${encodeURIComponent(project.id)}` }) {
|
|
789
792
|
const doc = explicitDocument ?? globalThis.document;
|
|
790
793
|
const reducedMotion = Boolean(globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches);
|
|
791
794
|
const wrap = el(doc, 'div', { className: 'bb-world' });
|
|
@@ -820,6 +823,8 @@ export function createWorldView({ root, rail, page, document: explicitDocument,
|
|
|
820
823
|
|
|
821
824
|
let proofs = new Map();
|
|
822
825
|
let placement = { byBusiness: new Map(), unplaced: [] };
|
|
826
|
+
/** Judgments the host kept back because they are not this organization's (organization mode only). */
|
|
827
|
+
let withheld = 0;
|
|
823
828
|
|
|
824
829
|
function judgmentText(item, fallback) {
|
|
825
830
|
const textValue = item?.proof?.interruption?.question_display_text ?? item?.proof?.decision?.summary ?? fallback;
|
|
@@ -834,8 +839,27 @@ export function createWorldView({ root, rail, page, document: explicitDocument,
|
|
|
834
839
|
}
|
|
835
840
|
|
|
836
841
|
/** Descends to the same project in プロジェクトと関係者 (the shell opens `#projects?project=`). */
|
|
837
|
-
function projectLink(
|
|
838
|
-
return el(doc, 'a', { className: 'bb-world-rail-link', text: label, attrs: { href:
|
|
842
|
+
function projectLink(project, label) {
|
|
843
|
+
return el(doc, 'a', { className: 'bb-world-rail-link', text: label, attrs: { href: projectHref(project) } });
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* The same project in the organization web, where it is corrected (Inspector UX-20261004-01). Only when
|
|
848
|
+
* the host reads the organization Graph and was told where its web is.
|
|
849
|
+
*/
|
|
850
|
+
function organizationLink(project) {
|
|
851
|
+
const origin = organizationWebOrigin(organizationGraph);
|
|
852
|
+
if (!origin) return null;
|
|
853
|
+
// A business without its own code carries its id as its code; that is not a code the organization web knows.
|
|
854
|
+
const code = project.code && project.code !== project.id ? project.code : null;
|
|
855
|
+
const target = organizationRecordLink(origin, 'projects', { id: project.id, type: 'project', metadata: { code } });
|
|
856
|
+
return target ? organizationRecordAnchor(doc, target, 'bb-world-rail-link') : null;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/** 詳しく見る: プロジェクトと関係者 here, and the organization web when there is one. */
|
|
860
|
+
function detailBlock(project, label) {
|
|
861
|
+
const content = [projectLink(project, label), organizationLink(project)].filter(Boolean);
|
|
862
|
+
return workspaceRailBlock(doc, { title: '詳しく見る', content });
|
|
839
863
|
}
|
|
840
864
|
|
|
841
865
|
function judgmentList(entries, describe) {
|
|
@@ -876,18 +900,20 @@ export function createWorldView({ root, rail, page, document: explicitDocument,
|
|
|
876
900
|
})];
|
|
877
901
|
if (data.kind === 'engagement') {
|
|
878
902
|
blocks.push(workspaceRailBlock(doc, { title: '登録', content: workspaceDefinition(doc, [['状態', statusText(data.engagement.status)], ['Graph ID', data.engagement.id]]) }));
|
|
879
|
-
blocks.push(
|
|
903
|
+
blocks.push(detailBlock(data.engagement, '「プロジェクトと関係者」でこの案件を開く'));
|
|
880
904
|
} else {
|
|
881
905
|
const open = business.engagements.filter((engagement) => !isFinished(engagement.status)).length;
|
|
882
|
-
|
|
906
|
+
const kind = kindEntry(business.kind);
|
|
907
|
+
const kindText = kind.definition ? `${kind.label}(${kind.definition})` : kind.label;
|
|
908
|
+
blocks.push(workspaceRailBlock(doc, { title: '登録', content: workspaceDefinition(doc, [['分類', kindText], ['状態', statusText(business.status)], ['コード', business.code], ['案件', `${business.engagements.length}件(動いている${open}件)`]]) }));
|
|
883
909
|
const ul = el(doc, 'ul', { className: 'bb-world-rail-list' });
|
|
884
910
|
for (const engagement of business.engagements) {
|
|
885
911
|
const li = el(doc, 'li', { text: `${engagement.name}(${statusText(engagement.status)})` });
|
|
886
|
-
li.append(projectLink(engagement
|
|
912
|
+
li.append(projectLink(engagement, '開く'));
|
|
887
913
|
ul.append(li);
|
|
888
914
|
}
|
|
889
915
|
blocks.push(workspaceRailBlock(doc, { title: '案件(区画)', content: business.engagements.length ? ul : { text: '登録された案件はありません' } }));
|
|
890
|
-
blocks.push(
|
|
916
|
+
blocks.push(detailBlock(business, '「プロジェクトと関係者」でこの事業を開く'));
|
|
891
917
|
}
|
|
892
918
|
blocks.push(cityJudgmentBlock(business));
|
|
893
919
|
showRail(blocks);
|
|
@@ -947,16 +973,36 @@ export function createWorldView({ root, rail, page, document: explicitDocument,
|
|
|
947
973
|
? judgmentList(placement.unplaced, (entry) => `${UNPLACED_REASON_TEXT[entry.reason] ?? entry.reason}${entry.workspace ? `(作業場所 ${entry.workspace})` : ''}`)
|
|
948
974
|
: { text: 'ありません' },
|
|
949
975
|
}));
|
|
976
|
+
if (withheld) {
|
|
977
|
+
blocks.push(workspaceRailBlock(doc, {
|
|
978
|
+
title: `この世界に出していない判断(${withheld}件)`,
|
|
979
|
+
content: { text: 'この組織の事業で作業したと確かめられない判断です。ほかの会社の仕事が混ざるため、内容も作業場所も出しません。「今日」では見られます。' },
|
|
980
|
+
}));
|
|
981
|
+
}
|
|
950
982
|
showRail(blocks);
|
|
951
983
|
}
|
|
952
984
|
}
|
|
953
985
|
|
|
954
986
|
async function load() {
|
|
955
|
-
const
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
987
|
+
const businessesResult = await readJson(fetcher, '/api/extensions/world/businesses');
|
|
988
|
+
// A world drawn from an organization's Graph shows only that organization's judgments: the host keeps
|
|
989
|
+
// the others (another company's work, or unknown) back and sends only how many it kept back.
|
|
990
|
+
const organizationScoped = businessesResult.ok && businessesResult.data?.status === 'ok'
|
|
991
|
+
&& businessesResult.data.source?.authority === 'organization_graph';
|
|
992
|
+
let homeResult;
|
|
993
|
+
let placesResult;
|
|
994
|
+
if (organizationScoped) {
|
|
995
|
+
const scoped = await readJson(fetcher, '/api/extensions/world/organization-judgments');
|
|
996
|
+
const available = scoped.ok && scoped.data?.status === 'available';
|
|
997
|
+
withheld = available ? scoped.data.withheld : 0;
|
|
998
|
+
homeResult = available ? { ok: true, data: scoped.data.home } : scoped.ok ? { ok: true, data: { status: 'unavailable', reason: scoped.data?.reason } } : scoped;
|
|
999
|
+
placesResult = available ? { ok: true, data: { status: 'available', places: scoped.data.places } } : { ok: true, data: { status: 'unavailable', reason: scoped.data?.reason } };
|
|
1000
|
+
} else {
|
|
1001
|
+
[homeResult, placesResult] = await Promise.all([
|
|
1002
|
+
readJson(fetcher, '/api/value-proofs/home'),
|
|
1003
|
+
readJson(fetcher, '/api/extensions/world/judgment-places'),
|
|
1004
|
+
]);
|
|
1005
|
+
}
|
|
960
1006
|
notices.replaceChildren();
|
|
961
1007
|
const note = (label, textValue, tone = 'info') => {
|
|
962
1008
|
const line = el(doc, 'p', { className: `bb-world-hud-line is-${tone}`, attrs: tone === 'info' ? {} : { role: 'alert' } });
|
|
@@ -976,13 +1022,16 @@ export function createWorldView({ root, rail, page, document: explicitDocument,
|
|
|
976
1022
|
const sourceText = businessPayload.source.authority === 'organization_graph' ? `組織のGraph(${businessPayload.source.server})` : 'このMacのGraph';
|
|
977
1023
|
const unclassified = businesses.filter((business) => !business.kind).length;
|
|
978
1024
|
if (unclassified) extra.push(`分類の無い事業${unclassified}件は「分類なし」として描いています`);
|
|
1025
|
+
if (businessPayload.vocabulary?.terms === 'unavailable') extra.push('組織の用語を読めないため、分類の名前は語彙ファイルか値のままです');
|
|
979
1026
|
note('事業', `${sourceText}の事業${businesses.length}件・案件${businesses.reduce((sum, business) => sum + business.engagements.length, 0)}件。${extra.join('。')}${extra.length ? '。' : ''}`);
|
|
980
1027
|
}
|
|
981
1028
|
const home = homeResult.ok ? homeResult.data : null;
|
|
982
1029
|
const readable = home?.status === 'available' && Array.isArray(home.delegation_map?.rows);
|
|
983
1030
|
const rows = readable ? home.delegation_map.rows : [];
|
|
984
1031
|
if (!homeResult.ok || !readable) {
|
|
985
|
-
|
|
1032
|
+
const reason = homeResult.ok ? home?.reason ?? home?.status ?? '状態不明' : homeResult.error;
|
|
1033
|
+
if (JUDGMENT_UNAVAILABLE_TEXT[reason]) note('判断', `${JUDGMENT_UNAVAILABLE_TEXT[reason]}。事業だけを描いています。`);
|
|
1034
|
+
else note('判断', `判断の記録を読めません(${reason})。0件ではありません。`, 'warning');
|
|
986
1035
|
} else {
|
|
987
1036
|
proofs = proofIndex(home);
|
|
988
1037
|
const waiting = [...proofs.values()].filter(isWaiting).length;
|
|
@@ -992,7 +1041,8 @@ export function createWorldView({ root, rail, page, document: explicitDocument,
|
|
|
992
1041
|
const known = places.filter((place) => proofs.has(place.decision_attempt_id)).map((place) => ({ ...place, item: proofs.get(place.decision_attempt_id) }));
|
|
993
1042
|
placement = groupJudgmentPlaces(known, businesses);
|
|
994
1043
|
const placed = known.length - placement.unplaced.length;
|
|
995
|
-
|
|
1044
|
+
const kept = withheld ? `この組織の事業で作業したと確かめられない判断${withheld}件は、この世界には出していません(「今日」で見られます)。` : '';
|
|
1045
|
+
note('判断', `${home.delegation_map.judged}件のうち${placed}件を作業した事業の判断所に、${placement.unplaced.length}件は事業が分からないため広場に。${kept}今あなたを待っている判断${waiting}件${waiting ? '(光の柱)' : ''}。`, waiting ? 'attention' : 'info');
|
|
996
1046
|
} else {
|
|
997
1047
|
note('判断', `${home.delegation_map.judged}件。作業した場所を読めないため、事業には置かず広場の種類別だけで描いています(${placesResult.ok ? placesResult.data?.reason ?? '理由不明' : placesResult.error})。今あなたを待っている判断${waiting}件。`, 'warning');
|
|
998
1048
|
}
|
|
@@ -1028,6 +1078,6 @@ export const screen = Object.freeze({
|
|
|
1028
1078
|
const root = context.document.createElement('div');
|
|
1029
1079
|
root.className = 'bb-shell-page';
|
|
1030
1080
|
container.append(root);
|
|
1031
|
-
return createWorldView({ root, rail: context.rail, page: context.page, document: context.document, fetcher: context.fetcher });
|
|
1081
|
+
return createWorldView({ root, rail: context.rail, page: context.page, document: context.document, fetcher: context.fetcher, organizationGraph: context.organizationGraph ?? null });
|
|
1032
1082
|
},
|
|
1033
1083
|
});
|