@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.
@@ -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.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": "ae251c43b4f42eddbeb18297e67710582a42af1f"
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
- 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');
@@ -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
- export function createWorldView({ root, rail, page, document: explicitDocument, fetcher }) {
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(projectId, label) {
838
- return el(doc, 'a', { className: 'bb-world-rail-link', text: label, attrs: { href: `#projects?project=${encodeURIComponent(projectId)}` } });
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(workspaceRailBlock(doc, { title: '詳しく見る', content: projectLink(data.engagement.id, '「プロジェクトと関係者」でこの案件を開く') }));
912
+ blocks.push(detailBlock(data.engagement, '「プロジェクトと関係者」でこの案件を開く'));
880
913
  } else {
881
914
  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}件)`]]) }));
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.id, '開く'));
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(workspaceRailBlock(doc, { title: '詳しく見る', content: projectLink(business.id, '「プロジェクトと関係者」でこの事業を開く') }));
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 [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
- ]);
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
- note('判断', `判断の記録を読めません(${homeResult.ok ? home?.reason ?? home?.status ?? '状態不明' : homeResult.error})。0件ではありません。`, 'warning');
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
- note('判断', `${home.delegation_map.judged}件のうち${placed}件を作業した事業の判断所に、${placement.unplaced.length}件は事業が分からないため広場に。今あなたを待っている判断${waiting}件${waiting ? '(光の柱)' : ''}。`, waiting ? 'attention' : 'info');
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
  });