@unson/brainbase-mcp 0.15.2 → 0.15.3
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-view.js +75 -16
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.3",
|
|
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": "bbcd694ef8e7231d88c1522d95c701421c9c5afa"
|
|
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');
|
package/ui/world/world-view.js
CHANGED
|
@@ -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';
|
|
@@ -785,7 +786,18 @@ function renderFallbackList(doc, root, businesses, rows) {
|
|
|
785
786
|
root.append(list);
|
|
786
787
|
}
|
|
787
788
|
|
|
788
|
-
|
|
789
|
+
/** Why the host has no judgments to give, in the words the screen shows. */
|
|
790
|
+
const JUDGMENT_UNAVAILABLE_TEXT = Object.freeze({
|
|
791
|
+
judgment_journal_not_connected: '判断の記録は各メンバーのMacにあり、組織版には送らないため未接続です',
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* @param {object} options
|
|
796
|
+
* @param {(project: { id: string, code?: string | null }) => string} [options.projectHref] Where a business or
|
|
797
|
+
* engagement opens in the host's プロジェクトと関係者. The local web opens `#projects?project=<id>`; an
|
|
798
|
+
* organization web passes its own route.
|
|
799
|
+
*/
|
|
800
|
+
export function createWorldView({ root, rail, page, document: explicitDocument, fetcher, organizationGraph = null, projectHref = (project) => `#projects?project=${encodeURIComponent(project.id)}` }) {
|
|
789
801
|
const doc = explicitDocument ?? globalThis.document;
|
|
790
802
|
const reducedMotion = Boolean(globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches);
|
|
791
803
|
const wrap = el(doc, 'div', { className: 'bb-world' });
|
|
@@ -820,6 +832,8 @@ export function createWorldView({ root, rail, page, document: explicitDocument,
|
|
|
820
832
|
|
|
821
833
|
let proofs = new Map();
|
|
822
834
|
let placement = { byBusiness: new Map(), unplaced: [] };
|
|
835
|
+
/** Judgments the host kept back because they are not this organization's (organization mode only). */
|
|
836
|
+
let withheld = 0;
|
|
823
837
|
|
|
824
838
|
function judgmentText(item, fallback) {
|
|
825
839
|
const textValue = item?.proof?.interruption?.question_display_text ?? item?.proof?.decision?.summary ?? fallback;
|
|
@@ -834,8 +848,27 @@ export function createWorldView({ root, rail, page, document: explicitDocument,
|
|
|
834
848
|
}
|
|
835
849
|
|
|
836
850
|
/** 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:
|
|
851
|
+
function projectLink(project, label) {
|
|
852
|
+
return el(doc, 'a', { className: 'bb-world-rail-link', text: label, attrs: { href: projectHref(project) } });
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* The same project in the organization web, where it is corrected (Inspector UX-20261004-01). Only when
|
|
857
|
+
* the host reads the organization Graph and was told where its web is.
|
|
858
|
+
*/
|
|
859
|
+
function organizationLink(project) {
|
|
860
|
+
const origin = organizationWebOrigin(organizationGraph);
|
|
861
|
+
if (!origin) return null;
|
|
862
|
+
// A business without its own code carries its id as its code; that is not a code the organization web knows.
|
|
863
|
+
const code = project.code && project.code !== project.id ? project.code : null;
|
|
864
|
+
const target = organizationRecordLink(origin, 'projects', { id: project.id, type: 'project', metadata: { code } });
|
|
865
|
+
return target ? organizationRecordAnchor(doc, target, 'bb-world-rail-link') : null;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/** 詳しく見る: プロジェクトと関係者 here, and the organization web when there is one. */
|
|
869
|
+
function detailBlock(project, label) {
|
|
870
|
+
const content = [projectLink(project, label), organizationLink(project)].filter(Boolean);
|
|
871
|
+
return workspaceRailBlock(doc, { title: '詳しく見る', content });
|
|
839
872
|
}
|
|
840
873
|
|
|
841
874
|
function judgmentList(entries, describe) {
|
|
@@ -876,18 +909,20 @@ export function createWorldView({ root, rail, page, document: explicitDocument,
|
|
|
876
909
|
})];
|
|
877
910
|
if (data.kind === 'engagement') {
|
|
878
911
|
blocks.push(workspaceRailBlock(doc, { title: '登録', content: workspaceDefinition(doc, [['状態', statusText(data.engagement.status)], ['Graph ID', data.engagement.id]]) }));
|
|
879
|
-
blocks.push(
|
|
912
|
+
blocks.push(detailBlock(data.engagement, '「プロジェクトと関係者」でこの案件を開く'));
|
|
880
913
|
} else {
|
|
881
914
|
const open = business.engagements.filter((engagement) => !isFinished(engagement.status)).length;
|
|
882
|
-
|
|
915
|
+
const kind = kindEntry(business.kind);
|
|
916
|
+
const kindText = kind.definition ? `${kind.label}(${kind.definition})` : kind.label;
|
|
917
|
+
blocks.push(workspaceRailBlock(doc, { title: '登録', content: workspaceDefinition(doc, [['分類', kindText], ['状態', statusText(business.status)], ['コード', business.code], ['案件', `${business.engagements.length}件(動いている${open}件)`]]) }));
|
|
883
918
|
const ul = el(doc, 'ul', { className: 'bb-world-rail-list' });
|
|
884
919
|
for (const engagement of business.engagements) {
|
|
885
920
|
const li = el(doc, 'li', { text: `${engagement.name}(${statusText(engagement.status)})` });
|
|
886
|
-
li.append(projectLink(engagement
|
|
921
|
+
li.append(projectLink(engagement, '開く'));
|
|
887
922
|
ul.append(li);
|
|
888
923
|
}
|
|
889
924
|
blocks.push(workspaceRailBlock(doc, { title: '案件(区画)', content: business.engagements.length ? ul : { text: '登録された案件はありません' } }));
|
|
890
|
-
blocks.push(
|
|
925
|
+
blocks.push(detailBlock(business, '「プロジェクトと関係者」でこの事業を開く'));
|
|
891
926
|
}
|
|
892
927
|
blocks.push(cityJudgmentBlock(business));
|
|
893
928
|
showRail(blocks);
|
|
@@ -947,16 +982,36 @@ export function createWorldView({ root, rail, page, document: explicitDocument,
|
|
|
947
982
|
? judgmentList(placement.unplaced, (entry) => `${UNPLACED_REASON_TEXT[entry.reason] ?? entry.reason}${entry.workspace ? `(作業場所 ${entry.workspace})` : ''}`)
|
|
948
983
|
: { text: 'ありません' },
|
|
949
984
|
}));
|
|
985
|
+
if (withheld) {
|
|
986
|
+
blocks.push(workspaceRailBlock(doc, {
|
|
987
|
+
title: `この世界に出していない判断(${withheld}件)`,
|
|
988
|
+
content: { text: 'この組織の事業で作業したと確かめられない判断です。ほかの会社の仕事が混ざるため、内容も作業場所も出しません。「今日」では見られます。' },
|
|
989
|
+
}));
|
|
990
|
+
}
|
|
950
991
|
showRail(blocks);
|
|
951
992
|
}
|
|
952
993
|
}
|
|
953
994
|
|
|
954
995
|
async function load() {
|
|
955
|
-
const
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
996
|
+
const businessesResult = await readJson(fetcher, '/api/extensions/world/businesses');
|
|
997
|
+
// A world drawn from an organization's Graph shows only that organization's judgments: the host keeps
|
|
998
|
+
// the others (another company's work, or unknown) back and sends only how many it kept back.
|
|
999
|
+
const organizationScoped = businessesResult.ok && businessesResult.data?.status === 'ok'
|
|
1000
|
+
&& businessesResult.data.source?.authority === 'organization_graph';
|
|
1001
|
+
let homeResult;
|
|
1002
|
+
let placesResult;
|
|
1003
|
+
if (organizationScoped) {
|
|
1004
|
+
const scoped = await readJson(fetcher, '/api/extensions/world/organization-judgments');
|
|
1005
|
+
const available = scoped.ok && scoped.data?.status === 'available';
|
|
1006
|
+
withheld = available ? scoped.data.withheld : 0;
|
|
1007
|
+
homeResult = available ? { ok: true, data: scoped.data.home } : scoped.ok ? { ok: true, data: { status: 'unavailable', reason: scoped.data?.reason } } : scoped;
|
|
1008
|
+
placesResult = available ? { ok: true, data: { status: 'available', places: scoped.data.places } } : { ok: true, data: { status: 'unavailable', reason: scoped.data?.reason } };
|
|
1009
|
+
} else {
|
|
1010
|
+
[homeResult, placesResult] = await Promise.all([
|
|
1011
|
+
readJson(fetcher, '/api/value-proofs/home'),
|
|
1012
|
+
readJson(fetcher, '/api/extensions/world/judgment-places'),
|
|
1013
|
+
]);
|
|
1014
|
+
}
|
|
960
1015
|
notices.replaceChildren();
|
|
961
1016
|
const note = (label, textValue, tone = 'info') => {
|
|
962
1017
|
const line = el(doc, 'p', { className: `bb-world-hud-line is-${tone}`, attrs: tone === 'info' ? {} : { role: 'alert' } });
|
|
@@ -976,13 +1031,16 @@ export function createWorldView({ root, rail, page, document: explicitDocument,
|
|
|
976
1031
|
const sourceText = businessPayload.source.authority === 'organization_graph' ? `組織のGraph(${businessPayload.source.server})` : 'このMacのGraph';
|
|
977
1032
|
const unclassified = businesses.filter((business) => !business.kind).length;
|
|
978
1033
|
if (unclassified) extra.push(`分類の無い事業${unclassified}件は「分類なし」として描いています`);
|
|
1034
|
+
if (businessPayload.vocabulary?.terms === 'unavailable') extra.push('組織の用語を読めないため、分類の名前は語彙ファイルか値のままです');
|
|
979
1035
|
note('事業', `${sourceText}の事業${businesses.length}件・案件${businesses.reduce((sum, business) => sum + business.engagements.length, 0)}件。${extra.join('。')}${extra.length ? '。' : ''}`);
|
|
980
1036
|
}
|
|
981
1037
|
const home = homeResult.ok ? homeResult.data : null;
|
|
982
1038
|
const readable = home?.status === 'available' && Array.isArray(home.delegation_map?.rows);
|
|
983
1039
|
const rows = readable ? home.delegation_map.rows : [];
|
|
984
1040
|
if (!homeResult.ok || !readable) {
|
|
985
|
-
|
|
1041
|
+
const reason = homeResult.ok ? home?.reason ?? home?.status ?? '状態不明' : homeResult.error;
|
|
1042
|
+
if (JUDGMENT_UNAVAILABLE_TEXT[reason]) note('判断', `${JUDGMENT_UNAVAILABLE_TEXT[reason]}。事業だけを描いています。`);
|
|
1043
|
+
else note('判断', `判断の記録を読めません(${reason})。0件ではありません。`, 'warning');
|
|
986
1044
|
} else {
|
|
987
1045
|
proofs = proofIndex(home);
|
|
988
1046
|
const waiting = [...proofs.values()].filter(isWaiting).length;
|
|
@@ -992,7 +1050,8 @@ export function createWorldView({ root, rail, page, document: explicitDocument,
|
|
|
992
1050
|
const known = places.filter((place) => proofs.has(place.decision_attempt_id)).map((place) => ({ ...place, item: proofs.get(place.decision_attempt_id) }));
|
|
993
1051
|
placement = groupJudgmentPlaces(known, businesses);
|
|
994
1052
|
const placed = known.length - placement.unplaced.length;
|
|
995
|
-
|
|
1053
|
+
const kept = withheld ? `この組織の事業で作業したと確かめられない判断${withheld}件は、この世界には出していません(「今日」で見られます)。` : '';
|
|
1054
|
+
note('判断', `${home.delegation_map.judged}件のうち${placed}件を作業した事業の判断所に、${placement.unplaced.length}件は事業が分からないため広場に。${kept}今あなたを待っている判断${waiting}件${waiting ? '(光の柱)' : ''}。`, waiting ? 'attention' : 'info');
|
|
996
1055
|
} else {
|
|
997
1056
|
note('判断', `${home.delegation_map.judged}件。作業した場所を読めないため、事業には置かず広場の種類別だけで描いています(${placesResult.ok ? placesResult.data?.reason ?? '理由不明' : placesResult.error})。今あなたを待っている判断${waiting}件。`, 'warning');
|
|
998
1057
|
}
|
|
@@ -1028,6 +1087,6 @@ export const screen = Object.freeze({
|
|
|
1028
1087
|
const root = context.document.createElement('div');
|
|
1029
1088
|
root.className = 'bb-shell-page';
|
|
1030
1089
|
container.append(root);
|
|
1031
|
-
return createWorldView({ root, rail: context.rail, page: context.page, document: context.document, fetcher: context.fetcher });
|
|
1090
|
+
return createWorldView({ root, rail: context.rail, page: context.page, document: context.document, fetcher: context.fetcher, organizationGraph: context.organizationGraph ?? null });
|
|
1032
1091
|
},
|
|
1033
1092
|
});
|