@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.
@@ -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
- function publicHome(home) {
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;
@@ -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
- label: text(entry.label) ?? key,
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 named = new Set(configured.map(([key]) => key));
128
- [...used].filter((key) => key !== null && !named.has(key)).sort().forEach((key) => {
129
- kinds.push({ key, label: key, form: 'office', color: KIND_PALETTE[kinds.length % KIND_PALETTE.length], configured: false });
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.2",
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": "ae251c43b4f42eddbeb18297e67710582a42af1f"
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
- const sourceBlock = workspaceHostNotice(doc, sourceNotice) ?? workspaceNotice(doc, {
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
  }),
@@ -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,
@@ -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. The organization web opens
209
- // 情報と関係 by record id and プロジェクトと関係者 by project code, and keeps that record across sign-in.
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
- `① 下のリンクから、組織版の${target.label}でこの記録を開きます。ログインしていなければSlackでログインすると、そのままこの記録に戻ります。`,
230
- '② 直せるのは、組織版での役割がgmかceoで、このプロジェクトとその上の事業の両方を許可されている人です。直せるのはプロジェクトの記録(名前・別名・要約・期間・目的・状態)だけで、人物の記録と、関係者の追加・終了は組織版でもまだ直せません。',
217
+ `① 下のリンク(上の「出典」にも同じリンクがあります)から、組織版の${target.label}でこの記録を開きます。ログインしていなければSlackでログインすると、そのままこの記録に戻ります。`,
218
+ '② 直せるのは、組織版での役割がgmかceoで、このプロジェクトが属する事業(事業そのものなら、その事業)を許可されている人です。直せるのはプロジェクトの記録(名前・別名・要約・期間・目的・状態)だけで、人物の記録と、関係者の追加・終了は組織版でもまだ直せません。',
231
219
  '③ 保存すると、理由といっしょに組織のGraphへ書き込まれ、組織版がその場で読み戻して表示します。この画面には、1分ほどたってから再読み込みすると反映されます。',
232
220
  '',
233
221
  ].join('\n') }),
234
- makeElement(doc, 'a', { text: '組織版でこの記録を開く', attrs: { href: target.href, target: '_blank', rel: 'noopener noreferrer' } }),
222
+ organizationRecordAnchor(doc, target),
235
223
  );
236
224
  return note;
237
225
  };
238
226
 
239
- /** The organization's web for this screen, or null when the host was not told where it is. */
240
- function organizationWebLink(doc, organizationGraph, screenId) {
241
- const origin = typeof organizationGraph?.web_url === 'string' && /^https:\/\/[^/?#@\s]+$/u.test(organizationGraph.web_url)
242
- ? organizationGraph.web_url
243
- : null;
244
- if (!origin) return null;
245
- return makeElement(doc, 'a', {
246
- text: '組織版で開いて訂正する',
247
- attrs: { href: `${origin}/?screen=${encodeURIComponent(screenId)}`, target: '_blank', rel: 'noopener noreferrer' },
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 link = readOnly ? organizationWebLink(context.document, context.organizationGraph, screenId) : null;
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: link ? organizationReadOnlyNoteLinked(context.document, context.organizationGraph.web_url, screenId) : ORGANIZATION_READ_ONLY_NOTE,
265
- sourceNotice: { label: '出典', text: source ?? sourceText },
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,
@@ -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: kindLabel(proof) ? `判断の種類: ${kindLabel(proof)}` : row ? rowName(row) : null,
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
+ }
@@ -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
- export function createWorldView({ root, rail, page, document: explicitDocument, fetcher }) {
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(projectId, label) {
838
- return el(doc, 'a', { className: 'bb-world-rail-link', text: label, attrs: { href: `#projects?project=${encodeURIComponent(projectId)}` } });
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(workspaceRailBlock(doc, { title: '詳しく見る', content: projectLink(data.engagement.id, '「プロジェクトと関係者」でこの案件を開く') }));
903
+ blocks.push(detailBlock(data.engagement, '「プロジェクトと関係者」でこの案件を開く'));
880
904
  } else {
881
905
  const open = business.engagements.filter((engagement) => !isFinished(engagement.status)).length;
882
- blocks.push(workspaceRailBlock(doc, { title: '登録', content: workspaceDefinition(doc, [['状態', statusText(business.status)], ['コード', business.code], ['案件', `${business.engagements.length}件(動いている${open}件)`]]) }));
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.id, '開く'));
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(workspaceRailBlock(doc, { title: '詳しく見る', content: projectLink(business.id, '「プロジェクトと関係者」でこの事業を開く') }));
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 [businessesResult, homeResult, placesResult] = await Promise.all([
956
- readJson(fetcher, '/api/extensions/world/businesses'),
957
- readJson(fetcher, '/api/value-proofs/home'),
958
- readJson(fetcher, '/api/extensions/world/judgment-places'),
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
- note('判断', `判断の記録を読めません(${homeResult.ok ? home?.reason ?? home?.status ?? '状態不明' : homeResult.error})。0件ではありません。`, 'warning');
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
- note('判断', `${home.delegation_map.judged}件のうち${placed}件を作業した事業の判断所に、${placement.unplaced.length}件は事業が分からないため広場に。今あなたを待っている判断${waiting}件${waiting ? '(光の柱)' : ''}。`, waiting ? 'attention' : 'info');
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
  });