@unson/brainbase-mcp 0.28.0 → 0.30.0

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.
@@ -0,0 +1,140 @@
1
+ declare const SOURCE_CLASS = "owning_repo";
2
+ declare const CONTENT_TYPE = "team_document";
3
+ type Revision = string | number;
4
+ export interface CanonicalDocumentResolution {
5
+ status?: unknown;
6
+ source_class?: unknown;
7
+ content_type?: unknown;
8
+ project_code?: unknown;
9
+ canonical_location?: {
10
+ repository?: string;
11
+ [key: string]: unknown;
12
+ } | null;
13
+ [key: string]: unknown;
14
+ }
15
+ export interface CanonicalDocumentSaveInput {
16
+ project_code?: unknown;
17
+ resolution?: CanonicalDocumentResolution | null;
18
+ content?: unknown;
19
+ path?: unknown;
20
+ repository_path?: unknown;
21
+ base_hash?: unknown;
22
+ expected_hash?: unknown;
23
+ base_revision?: unknown;
24
+ expected_revision?: unknown;
25
+ idempotency_key?: unknown;
26
+ access?: unknown;
27
+ [key: string]: unknown;
28
+ }
29
+ export interface CanonicalDocumentWriteRequest {
30
+ project_code: string;
31
+ path: string;
32
+ content: string;
33
+ content_hash: string;
34
+ base_hash: string | null;
35
+ base_revision: string;
36
+ idempotency_key: string;
37
+ request_fingerprint: string;
38
+ resolution: CanonicalDocumentResolution;
39
+ access: unknown;
40
+ }
41
+ export interface CanonicalDocumentWriteResult {
42
+ path?: string;
43
+ canonical_url?: string | null;
44
+ revision?: Revision;
45
+ idempotency_replayed?: boolean;
46
+ [key: string]: unknown;
47
+ }
48
+ export interface CanonicalDocumentReadResult {
49
+ path?: string;
50
+ content?: unknown;
51
+ content_hash?: unknown;
52
+ revision?: Revision;
53
+ canonical_url?: string | null;
54
+ [key: string]: unknown;
55
+ }
56
+ export interface CanonicalDocumentProvider {
57
+ write(request: CanonicalDocumentWriteRequest): Promise<CanonicalDocumentWriteResult> | CanonicalDocumentWriteResult;
58
+ read(request: {
59
+ project_code: string;
60
+ path: string;
61
+ canonical_url: string | null;
62
+ resolution: CanonicalDocumentResolution;
63
+ access: unknown;
64
+ }): Promise<CanonicalDocumentReadResult> | CanonicalDocumentReadResult;
65
+ }
66
+ export interface CanonicalDocumentSaveResult {
67
+ status: 'saved';
68
+ project_code: string;
69
+ source_class: typeof SOURCE_CLASS;
70
+ content_type: typeof CONTENT_TYPE;
71
+ path: string;
72
+ canonical_url: string | null;
73
+ content_hash: string;
74
+ revision: string;
75
+ readback: {
76
+ path: string;
77
+ content_hash: string;
78
+ revision: string;
79
+ canonical_url: string | null;
80
+ };
81
+ search_indexed: 'unknown';
82
+ idempotency_key: string;
83
+ idempotency_replayed: boolean;
84
+ request_fingerprint: string;
85
+ }
86
+ export interface CanonicalDocumentReceiptStore {
87
+ find?(request: CanonicalDocumentWriteRequest): Promise<{
88
+ request_fingerprint: string;
89
+ result: CanonicalDocumentSaveResult;
90
+ } | null | undefined>;
91
+ put?(record: CanonicalDocumentWriteRequest & {
92
+ result: CanonicalDocumentSaveResult;
93
+ }): Promise<unknown>;
94
+ }
95
+ declare function stableValue(value: unknown): unknown;
96
+ declare function digest(value: string): string;
97
+ export declare class CanonicalDocumentWriterError extends Error {
98
+ code: string;
99
+ status: number;
100
+ details: Record<string, unknown>;
101
+ constructor(code: string, message: string, status?: number, details?: Record<string, unknown>);
102
+ }
103
+ /**
104
+ * Canonical document provider contract.
105
+ *
106
+ * A provider is intentionally supplied by the caller. It must implement:
107
+ * write({ project_code, path, content, base_hash, base_revision,
108
+ * idempotency_key, request_fingerprint, resolution, access })
109
+ * -> { path?, canonical_url?, revision? }
110
+ * read({ project_code, path, canonical_url?, resolution, access })
111
+ * -> { path?, content, content_hash?, revision, canonical_url? }
112
+ *
113
+ * The provider owns the atomic CAS and durable idempotency boundary. This
114
+ * adapter validates the request, forwards both CAS values, and verifies the
115
+ * provider's post-write readback before reporting success.
116
+ */
117
+ export declare class CanonicalDocumentWriterAdapter {
118
+ provider: CanonicalDocumentProvider | null;
119
+ receiptStore: CanonicalDocumentReceiptStore | null;
120
+ completed: Map<string, {
121
+ request_fingerprint: string;
122
+ result: CanonicalDocumentSaveResult;
123
+ }>;
124
+ constructor({ provider, receiptStore }?: {
125
+ provider?: CanonicalDocumentProvider | null;
126
+ receiptStore?: CanonicalDocumentReceiptStore | null;
127
+ });
128
+ save(input?: CanonicalDocumentSaveInput): Promise<CanonicalDocumentSaveResult>;
129
+ normalizeRequest(input: CanonicalDocumentSaveInput): CanonicalDocumentWriteRequest;
130
+ assertProvider(): void;
131
+ assertOwningRepositoryResolution(resolution: CanonicalDocumentResolution | null | undefined, projectCode: string): asserts resolution is CanonicalDocumentResolution;
132
+ readback(request: CanonicalDocumentWriteRequest, writeResult?: CanonicalDocumentWriteResult): Promise<Omit<CanonicalDocumentSaveResult, 'idempotency_key' | 'idempotency_replayed' | 'request_fingerprint'>>;
133
+ normalizeProviderError(error: unknown): CanonicalDocumentWriterError;
134
+ }
135
+ export declare function normalizeRepositoryRelativePath(value: unknown): string;
136
+ export declare const canonicalDocumentWriterInternals: {
137
+ digest: typeof digest;
138
+ stableValue: typeof stableValue;
139
+ };
140
+ export {};
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Saves a team document into its owning repository through a caller-supplied
3
+ * provider. The adapter validates the request, forwards both CAS values and the
4
+ * idempotency key, and reports success only after the provider's readback
5
+ * matches the requested content and write revision.
6
+ *
7
+ * Moved unchanged from brainbase-unson server/services/canonical-document-writer-adapter.js
8
+ * (story-document-and-meeting-source-exits-v1).
9
+ */
10
+ import { createHash } from 'node:crypto';
11
+ import path from 'node:path';
12
+ const SOURCE_CLASS = 'owning_repo';
13
+ const CONTENT_TYPE = 'team_document';
14
+ function stableValue(value) {
15
+ if (Array.isArray(value))
16
+ return value.map(stableValue);
17
+ if (value && typeof value === 'object') {
18
+ const record = value;
19
+ return Object.fromEntries(Object.keys(record).sort().map((key) => [key, stableValue(record[key])]));
20
+ }
21
+ return value;
22
+ }
23
+ function digest(value) {
24
+ return createHash('sha256').update(value, 'utf8').digest('hex');
25
+ }
26
+ function requiredString(value, name) {
27
+ if (typeof value !== 'string' || !value.trim()) {
28
+ throw new CanonicalDocumentWriterError('canonical_document_input_invalid', `${name} is required`, 400, { field: name });
29
+ }
30
+ return value.trim();
31
+ }
32
+ function normalizeRevision(value, name) {
33
+ if (typeof value === 'number' && Number.isInteger(value) && value >= 0)
34
+ return String(value);
35
+ return requiredString(value, name);
36
+ }
37
+ function errorField(error, field) {
38
+ return error && typeof error === 'object' ? error[field] : undefined;
39
+ }
40
+ export class CanonicalDocumentWriterError extends Error {
41
+ code;
42
+ status;
43
+ details;
44
+ constructor(code, message, status = 400, details = {}) {
45
+ super(message);
46
+ this.name = 'CanonicalDocumentWriterError';
47
+ this.code = code;
48
+ this.status = status;
49
+ this.details = details;
50
+ }
51
+ }
52
+ /**
53
+ * Canonical document provider contract.
54
+ *
55
+ * A provider is intentionally supplied by the caller. It must implement:
56
+ * write({ project_code, path, content, base_hash, base_revision,
57
+ * idempotency_key, request_fingerprint, resolution, access })
58
+ * -> { path?, canonical_url?, revision? }
59
+ * read({ project_code, path, canonical_url?, resolution, access })
60
+ * -> { path?, content, content_hash?, revision, canonical_url? }
61
+ *
62
+ * The provider owns the atomic CAS and durable idempotency boundary. This
63
+ * adapter validates the request, forwards both CAS values, and verifies the
64
+ * provider's post-write readback before reporting success.
65
+ */
66
+ export class CanonicalDocumentWriterAdapter {
67
+ provider;
68
+ receiptStore;
69
+ completed;
70
+ constructor({ provider = null, receiptStore = null } = {}) {
71
+ this.provider = provider;
72
+ this.receiptStore = receiptStore;
73
+ this.completed = new Map();
74
+ }
75
+ async save(input = {}) {
76
+ const request = this.normalizeRequest(input);
77
+ this.assertProvider();
78
+ let durable = null;
79
+ if (typeof this.receiptStore?.find === 'function') {
80
+ try {
81
+ durable = await this.receiptStore.find(request);
82
+ }
83
+ catch (error) {
84
+ if (error?.name === 'CanonicalDocumentWriterError')
85
+ throw error;
86
+ throw new CanonicalDocumentWriterError('canonical_document_receipt_unavailable', 'canonical document write receipt could not be read', 503, { cause: errorField(error, 'code') || errorField(error, 'message') || 'unknown' });
87
+ }
88
+ }
89
+ if (durable) {
90
+ if (durable.request_fingerprint !== request.request_fingerprint) {
91
+ throw new CanonicalDocumentWriterError('canonical_document_idempotency_conflict', 'idempotency_key was already used with different document content', 409, { idempotency_key: request.idempotency_key });
92
+ }
93
+ return { ...durable.result, idempotency_replayed: true };
94
+ }
95
+ const previous = this.completed.get(request.idempotency_key);
96
+ if (previous) {
97
+ if (previous.request_fingerprint !== request.request_fingerprint) {
98
+ throw new CanonicalDocumentWriterError('canonical_document_idempotency_conflict', 'idempotency_key was already used with different document content', 409, { idempotency_key: request.idempotency_key });
99
+ }
100
+ return { ...previous.result, idempotency_replayed: true };
101
+ }
102
+ const provider = this.provider;
103
+ let result;
104
+ try {
105
+ const writeResult = await provider.write(request);
106
+ if (!writeResult || typeof writeResult !== 'object' || !writeResult.revision) {
107
+ throw new CanonicalDocumentWriterError('canonical_document_write_invalid', 'canonical document provider did not return the write revision', 503);
108
+ }
109
+ const readback = await this.readback(request, writeResult);
110
+ result = {
111
+ ...readback,
112
+ idempotency_key: request.idempotency_key,
113
+ idempotency_replayed: Boolean(writeResult.idempotency_replayed),
114
+ request_fingerprint: request.request_fingerprint,
115
+ };
116
+ }
117
+ catch (error) {
118
+ throw this.normalizeProviderError(error);
119
+ }
120
+ if (typeof this.receiptStore?.put === 'function') {
121
+ try {
122
+ await this.receiptStore.put({
123
+ ...request,
124
+ result,
125
+ });
126
+ }
127
+ catch (error) {
128
+ throw this.normalizeProviderError(new CanonicalDocumentWriterError('canonical_document_receipt_unavailable', 'canonical document write receipt could not be stored', 503, { cause: errorField(error, 'code') || errorField(error, 'message') || 'unknown' }));
129
+ }
130
+ }
131
+ this.completed.set(request.idempotency_key, {
132
+ request_fingerprint: request.request_fingerprint,
133
+ result,
134
+ });
135
+ return result;
136
+ }
137
+ normalizeRequest(input) {
138
+ const projectCode = requiredString(input.project_code, 'project_code');
139
+ const resolution = input.resolution;
140
+ this.assertOwningRepositoryResolution(resolution, projectCode);
141
+ const content = input.content;
142
+ if (typeof content !== 'string' || !content.trim()) {
143
+ throw new CanonicalDocumentWriterError('canonical_document_input_invalid', 'content is required', 400, { field: 'content' });
144
+ }
145
+ const repositoryPath = normalizeRepositoryRelativePath(input.path ?? input.repository_path);
146
+ const rawBaseHash = input.base_hash ?? input.expected_hash;
147
+ const baseHash = rawBaseHash === undefined || rawBaseHash === null || rawBaseHash === ''
148
+ ? null
149
+ : requiredString(rawBaseHash, 'base_hash');
150
+ const baseRevision = normalizeRevision(input.base_revision ?? input.expected_revision, 'base_revision');
151
+ const idempotencyKey = requiredString(input.idempotency_key, 'idempotency_key');
152
+ const contentHash = digest(content);
153
+ const fingerprint = digest(JSON.stringify(stableValue({
154
+ project_code: projectCode,
155
+ path: repositoryPath,
156
+ content_hash: contentHash,
157
+ base_hash: baseHash,
158
+ base_revision: baseRevision,
159
+ idempotency_key: idempotencyKey,
160
+ source_class: SOURCE_CLASS,
161
+ content_type: CONTENT_TYPE,
162
+ canonical_location: resolution.canonical_location || null,
163
+ })));
164
+ return {
165
+ project_code: projectCode,
166
+ path: repositoryPath,
167
+ content,
168
+ content_hash: contentHash,
169
+ base_hash: baseHash,
170
+ base_revision: baseRevision,
171
+ idempotency_key: idempotencyKey,
172
+ request_fingerprint: fingerprint,
173
+ resolution,
174
+ access: input.access || null,
175
+ };
176
+ }
177
+ assertProvider() {
178
+ if (!this.provider || typeof this.provider.write !== 'function' || typeof this.provider.read !== 'function') {
179
+ throw new CanonicalDocumentWriterError('canonical_document_writer_not_configured', 'canonical document writer is not configured', 503);
180
+ }
181
+ }
182
+ assertOwningRepositoryResolution(resolution, projectCode) {
183
+ if (!resolution
184
+ || resolution.status !== 'resolved'
185
+ || resolution.source_class !== SOURCE_CLASS
186
+ || resolution.content_type !== CONTENT_TYPE
187
+ || resolution.project_code !== projectCode) {
188
+ throw new CanonicalDocumentWriterError('canonical_document_route_invalid', 'team_document must resolve to the owning repository before it can be saved', 422, { required_source_class: SOURCE_CLASS, required_content_type: CONTENT_TYPE });
189
+ }
190
+ const expectedRepository = `project:${projectCode}`;
191
+ const repository = resolution.canonical_location?.repository;
192
+ if (repository && repository !== expectedRepository) {
193
+ throw new CanonicalDocumentWriterError('canonical_document_route_invalid', 'resolution repository does not match project_code', 422, { expected_repository: expectedRepository, repository });
194
+ }
195
+ }
196
+ async readback(request, writeResult = {}) {
197
+ if (!writeResult || typeof writeResult !== 'object') {
198
+ throw new CanonicalDocumentWriterError('canonical_document_write_invalid', 'canonical document provider returned an invalid write result', 503);
199
+ }
200
+ const pathFromWrite = normalizeRepositoryRelativePath(writeResult.path ?? request.path);
201
+ const readback = await this.provider.read({
202
+ project_code: request.project_code,
203
+ path: pathFromWrite,
204
+ canonical_url: writeResult.canonical_url || null,
205
+ resolution: request.resolution,
206
+ access: request.access,
207
+ });
208
+ if (!readback || typeof readback !== 'object' || typeof readback.content !== 'string') {
209
+ throw new CanonicalDocumentWriterError('canonical_document_readback_unavailable', 'canonical document could not be read back after save', 503);
210
+ }
211
+ const readbackPath = normalizeRepositoryRelativePath(readback.path ?? pathFromWrite);
212
+ const readbackHash = digest(readback.content);
213
+ const declaredReadbackHash = typeof readback.content_hash === 'string' ? readback.content_hash.trim() : null;
214
+ if (readbackPath !== request.path
215
+ || (declaredReadbackHash && declaredReadbackHash !== readbackHash)
216
+ || readbackHash !== request.content_hash
217
+ || readback.content !== request.content) {
218
+ throw new CanonicalDocumentWriterError('canonical_document_readback_mismatch', 'canonical document readback does not match the requested content', 502, {
219
+ path: request.path,
220
+ expected_content_hash: request.content_hash,
221
+ actual_content_hash: readbackHash,
222
+ ...(declaredReadbackHash ? { declared_content_hash: declaredReadbackHash } : {}),
223
+ actual_path: readbackPath,
224
+ });
225
+ }
226
+ const writeRevision = normalizeRevision(writeResult.revision, 'writeResult.revision');
227
+ const revision = normalizeRevision(readback.revision, 'readback.revision');
228
+ if (revision !== writeRevision) {
229
+ throw new CanonicalDocumentWriterError('canonical_document_readback_revision_mismatch', 'canonical document readback revision does not match the write commit', 502, { expected_revision: writeRevision, actual_revision: revision });
230
+ }
231
+ return {
232
+ status: 'saved',
233
+ project_code: request.project_code,
234
+ source_class: SOURCE_CLASS,
235
+ content_type: CONTENT_TYPE,
236
+ path: request.path,
237
+ canonical_url: readback.canonical_url || writeResult.canonical_url || null,
238
+ content_hash: request.content_hash,
239
+ revision,
240
+ readback: {
241
+ path: readbackPath,
242
+ content_hash: readbackHash,
243
+ revision,
244
+ canonical_url: readback.canonical_url || writeResult.canonical_url || null,
245
+ },
246
+ search_indexed: 'unknown',
247
+ };
248
+ }
249
+ normalizeProviderError(error) {
250
+ if (error instanceof CanonicalDocumentWriterError)
251
+ return error;
252
+ const rawStatus = errorField(error, 'status');
253
+ const status = Number.isInteger(rawStatus) ? rawStatus : 503;
254
+ const rawCode = errorField(error, 'code');
255
+ const code = typeof rawCode === 'string' && rawCode
256
+ ? rawCode
257
+ : 'canonical_document_writer_failed';
258
+ return new CanonicalDocumentWriterError(code, status === 409 ? 'canonical document write conflicts with the current version' : 'canonical document writer is unavailable', status, status === 409 ? { provider_code: code } : {});
259
+ }
260
+ }
261
+ export function normalizeRepositoryRelativePath(value) {
262
+ const raw = requiredString(value, 'path');
263
+ if (raw.includes('\0') || raw.includes('\\') || raw.startsWith('/') || raw.startsWith('~') || /^[A-Za-z]:[\\/]/.test(raw)) {
264
+ throw new CanonicalDocumentWriterError('canonical_document_path_invalid', 'path must be a repository-relative POSIX path', 400, { field: 'path' });
265
+ }
266
+ const segments = raw.split('/');
267
+ if (segments.some((segment) => !segment || segment === '.' || segment === '..')) {
268
+ throw new CanonicalDocumentWriterError('canonical_document_path_invalid', 'path must not contain empty, dot, or parent segments', 400, { field: 'path' });
269
+ }
270
+ const normalized = path.posix.normalize(raw);
271
+ if (normalized !== raw || normalized === '.' || normalized === '..' || normalized.startsWith('../') || path.posix.isAbsolute(normalized)) {
272
+ throw new CanonicalDocumentWriterError('canonical_document_path_invalid', 'path must be a normalized repository-relative path', 400, { field: 'path' });
273
+ }
274
+ return normalized;
275
+ }
276
+ export const canonicalDocumentWriterInternals = { digest, stableValue };
package/dist/cli.js CHANGED
@@ -1071,7 +1071,7 @@ async function webServe(parsed, io, command) {
1071
1071
  const origin = `http://127.0.0.1:${actualPort}`;
1072
1072
  write(io, [
1073
1073
  `Brainbase: ${origin}/`,
1074
- `- 今日(判断の見返し): ${origin}/#today`,
1074
+ `- 判断の履歴: ${origin}/#value-proofs`,
1075
1075
  `- 目的と現状: ${origin}/#objectives`,
1076
1076
  `- プロジェクトと関係者: ${origin}/#projects`,
1077
1077
  `- 情報と関係: ${origin}/#graph`,
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Contracts shared by the local knowledge capture and preview adapters.
3
+ *
4
+ * The adapter is deliberately provider-agnostic. Production bootstrap does
5
+ * not create one; callers must inject an explicit implementation. This file
6
+ * only normalizes and validates the boundary so an adapter cannot turn
7
+ * missing evidence or an unverified version into a successful result.
8
+ *
9
+ * Moved unchanged from brainbase-unson server/services/knowledge-capture-preview-adapter.js
10
+ * (story-document-and-meeting-source-exits-v1).
11
+ */
12
+ export type KnowledgeVersion = string | {
13
+ state: string;
14
+ value: string | null;
15
+ };
16
+ type Details = Record<string, unknown>;
17
+ type Entry = Record<string, unknown>;
18
+ export declare class KnowledgeAIAdapterContractError extends Error {
19
+ code: string;
20
+ status: number;
21
+ details: Details;
22
+ constructor(message: string, details?: Details);
23
+ }
24
+ export declare function normalizeVersion(value: unknown, field?: string): KnowledgeVersion;
25
+ export declare function versionValue(value: KnowledgeVersion): string | null;
26
+ export declare function normalizeUnknown(value: unknown, field?: string): Array<string | Entry>;
27
+ export declare function normalizeReadback(value: unknown, field?: string): Entry & {
28
+ state: string;
29
+ verified: boolean;
30
+ };
31
+ /**
32
+ * Resolve only explicitly injected adapter methods. There is intentionally
33
+ * no environment or default-provider lookup here.
34
+ */
35
+ export declare function resolveKnowledgeAdapter(adapter: unknown, method: string): ((...args: any[]) => any) | null;
36
+ export declare function normalizeEvidence(value: unknown, { field, allowedReferences, requireExactReference, }?: {
37
+ field?: string;
38
+ allowedReferences?: Set<string> | null;
39
+ requireExactReference?: boolean;
40
+ }): Array<Entry & {
41
+ id: string | null;
42
+ version: KnowledgeVersion;
43
+ source_ref: string | null;
44
+ }>;
45
+ export declare function referenceKey(id: unknown, version: KnowledgeVersion): string;
46
+ export declare function normalizeCitations(value: unknown, { field, allowedReferences, }?: {
47
+ field?: string;
48
+ allowedReferences?: Set<string> | null;
49
+ }): Array<Entry & {
50
+ id: string;
51
+ version: string;
52
+ }>;
53
+ export declare function validateCaptureProposal(value: unknown, { allowedReferences }?: {
54
+ allowedReferences?: Set<string> | null;
55
+ }): {
56
+ proposal: {
57
+ kind: unknown;
58
+ summary: unknown;
59
+ scope: unknown;
60
+ owner_candidate: unknown;
61
+ relations: unknown;
62
+ };
63
+ evidence: (Entry & {
64
+ id: string | null;
65
+ version: KnowledgeVersion;
66
+ source_ref: string | null;
67
+ })[];
68
+ unknown: (string | Entry)[];
69
+ version: KnowledgeVersion;
70
+ readback: Entry & {
71
+ state: string;
72
+ verified: boolean;
73
+ };
74
+ };
75
+ export declare function validatePreviewAnswer(value: unknown, { allowedReferences }: {
76
+ allowedReferences?: Set<string> | null;
77
+ }): {
78
+ answer: string;
79
+ citations: (Entry & {
80
+ id: string;
81
+ version: string;
82
+ })[];
83
+ evidence: (Entry & {
84
+ id: string | null;
85
+ version: KnowledgeVersion;
86
+ source_ref: string | null;
87
+ })[];
88
+ exclusions: {
89
+ [x: string]: unknown;
90
+ }[];
91
+ unknown: (string | Entry)[];
92
+ version: KnowledgeVersion;
93
+ readback: Entry & {
94
+ state: string;
95
+ verified: boolean;
96
+ };
97
+ };
98
+ export {};
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Contracts shared by the local knowledge capture and preview adapters.
3
+ *
4
+ * The adapter is deliberately provider-agnostic. Production bootstrap does
5
+ * not create one; callers must inject an explicit implementation. This file
6
+ * only normalizes and validates the boundary so an adapter cannot turn
7
+ * missing evidence or an unverified version into a successful result.
8
+ *
9
+ * Moved unchanged from brainbase-unson server/services/knowledge-capture-preview-adapter.js
10
+ * (story-document-and-meeting-source-exits-v1).
11
+ */
12
+ export class KnowledgeAIAdapterContractError extends Error {
13
+ code;
14
+ status;
15
+ details;
16
+ constructor(message, details = {}) {
17
+ super(message);
18
+ this.name = 'KnowledgeAIAdapterContractError';
19
+ this.code = 'knowledge_ai_adapter_contract_invalid';
20
+ this.status = 502;
21
+ this.details = details;
22
+ }
23
+ }
24
+ function text(value) {
25
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
26
+ }
27
+ function hasOwn(value, key) {
28
+ return Boolean(value && Object.prototype.hasOwnProperty.call(value, key));
29
+ }
30
+ function fail(message, details = {}) {
31
+ throw new KnowledgeAIAdapterContractError(message, details);
32
+ }
33
+ function isRecord(value) {
34
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
35
+ }
36
+ export function normalizeVersion(value, field = 'version') {
37
+ if (typeof value === 'string' && value.trim())
38
+ return value.trim();
39
+ if (typeof value === 'number' && Number.isFinite(value))
40
+ return String(value);
41
+ if (isRecord(value)) {
42
+ const state = text(value.state) || text(value.status);
43
+ if (!state)
44
+ fail(`${field} must include an explicit state`, { field });
45
+ return {
46
+ state,
47
+ value: typeof value.value === 'string' && value.value.trim()
48
+ ? value.value.trim()
49
+ : typeof value.value === 'number' && Number.isFinite(value.value)
50
+ ? String(value.value)
51
+ : null,
52
+ };
53
+ }
54
+ fail(`${field} is required`, { field });
55
+ }
56
+ export function versionValue(value) {
57
+ return typeof value === 'string' ? value : null;
58
+ }
59
+ function versionKey(value) {
60
+ return typeof value === 'string' ? `known:${value}` : `unknown:${value.state}:${value.value || ''}`;
61
+ }
62
+ export function normalizeUnknown(value, field = 'unknown') {
63
+ if (!Array.isArray(value))
64
+ fail(`${field} must be an array`, { field });
65
+ return value.map((entry, index) => {
66
+ if (typeof entry === 'string' && entry.trim())
67
+ return entry.trim();
68
+ if (isRecord(entry))
69
+ return { ...entry };
70
+ fail(`${field}[${index}] must be text or an object`, { field, index });
71
+ });
72
+ }
73
+ export function normalizeReadback(value, field = 'readback') {
74
+ if (!isRecord(value)) {
75
+ fail(`${field} is required`, { field });
76
+ }
77
+ const state = text(value.state) || text(value.status);
78
+ if (!state)
79
+ fail(`${field}.state is required`, { field });
80
+ if (typeof value.verified !== 'boolean') {
81
+ fail(`${field}.verified must be explicit`, { field });
82
+ }
83
+ return { ...value, state, verified: value.verified };
84
+ }
85
+ /**
86
+ * Resolve only explicitly injected adapter methods. There is intentionally
87
+ * no environment or default-provider lookup here.
88
+ */
89
+ export function resolveKnowledgeAdapter(adapter, method) {
90
+ if (typeof adapter === 'function')
91
+ return adapter;
92
+ if (!adapter || typeof adapter !== 'object')
93
+ return null;
94
+ const candidate = adapter[method];
95
+ if (typeof candidate === 'function')
96
+ return candidate.bind(adapter);
97
+ return null;
98
+ }
99
+ export function normalizeEvidence(value, { field = 'evidence', allowedReferences = null, requireExactReference = false, } = {}) {
100
+ if (!Array.isArray(value))
101
+ fail(`${field} must be an array`, { field });
102
+ return value.map((entry, index) => {
103
+ if (!isRecord(entry)) {
104
+ fail(`${field}[${index}] must be an object`, { field, index });
105
+ }
106
+ const id = text(entry.id) || text(entry.candidate_id) || null;
107
+ const sourceRef = text(entry.source_ref) || text(entry.pointer) || text(entry.source) || null;
108
+ if (requireExactReference && !id) {
109
+ fail(`${field}[${index}].id is required`, { field, index });
110
+ }
111
+ if (!id && !sourceRef) {
112
+ fail(`${field}[${index}] needs an id or source_ref`, { field, index });
113
+ }
114
+ if (!hasOwn(entry, 'version')) {
115
+ fail(`${field}[${index}].version is required`, { field, index });
116
+ }
117
+ const version = normalizeVersion(entry.version, `${field}[${index}].version`);
118
+ if (allowedReferences && id) {
119
+ const key = `${id}\u0000${versionKey(version)}`;
120
+ if (!allowedReferences.has(key)) {
121
+ fail(`${field}[${index}] references an unavailable candidate`, {
122
+ field,
123
+ index,
124
+ id,
125
+ version,
126
+ });
127
+ }
128
+ }
129
+ return {
130
+ ...entry,
131
+ id,
132
+ version,
133
+ source_ref: sourceRef,
134
+ };
135
+ });
136
+ }
137
+ export function referenceKey(id, version) {
138
+ return `${text(id) || ''}\u0000${versionKey(version)}`;
139
+ }
140
+ export function normalizeCitations(value, { field = 'citations', allowedReferences, } = {}) {
141
+ if (!Array.isArray(value))
142
+ fail(`${field} must be an array`, { field });
143
+ return value.map((entry, index) => {
144
+ if (!isRecord(entry)) {
145
+ fail(`${field}[${index}] must be an object`, { field, index });
146
+ }
147
+ const id = text(entry.id) || text(entry.candidate_id);
148
+ if (!id)
149
+ fail(`${field}[${index}].id is required`, { field, index });
150
+ if (!hasOwn(entry, 'version'))
151
+ fail(`${field}[${index}].version is required`, { field, index });
152
+ const version = normalizeVersion(entry.version, `${field}[${index}].version`);
153
+ if (typeof version !== 'string') {
154
+ fail(`${field}[${index}].version must be an exact value`, { field, index, id });
155
+ }
156
+ const key = referenceKey(id, version);
157
+ if (allowedReferences && !allowedReferences.has(key)) {
158
+ fail(`${field}[${index}] references a candidate outside the isolated set`, {
159
+ field,
160
+ index,
161
+ id,
162
+ version,
163
+ });
164
+ }
165
+ return {
166
+ ...entry,
167
+ id,
168
+ version,
169
+ };
170
+ });
171
+ }
172
+ export function validateCaptureProposal(value, { allowedReferences = null } = {}) {
173
+ if (!isRecord(value)) {
174
+ fail('capture adapter must return an object');
175
+ }
176
+ const proposal = isRecord(value.proposal)
177
+ ? value.proposal
178
+ : value;
179
+ for (const field of ['kind', 'summary', 'scope', 'owner_candidate', 'relations']) {
180
+ if (!hasOwn(proposal, field))
181
+ fail(`capture proposal.${field} is required`, { field });
182
+ }
183
+ const version = normalizeVersion(value.version, 'capture proposal.version');
184
+ const unknown = normalizeUnknown(value.unknown, 'capture proposal.unknown');
185
+ const evidence = normalizeEvidence(value.evidence, {
186
+ field: 'capture proposal.evidence',
187
+ allowedReferences,
188
+ });
189
+ const readback = normalizeReadback(value.readback, 'capture proposal.readback');
190
+ return {
191
+ proposal: {
192
+ kind: proposal.kind,
193
+ summary: proposal.summary,
194
+ scope: proposal.scope,
195
+ owner_candidate: proposal.owner_candidate,
196
+ relations: proposal.relations,
197
+ },
198
+ evidence,
199
+ unknown,
200
+ version,
201
+ readback,
202
+ };
203
+ }
204
+ export function validatePreviewAnswer(value, { allowedReferences }) {
205
+ if (!isRecord(value)) {
206
+ fail('preview adapter must return an object');
207
+ }
208
+ const answer = text(value.answer);
209
+ if (!answer)
210
+ fail('preview answer is required', { field: 'answer' });
211
+ const rawCitations = value.citations ?? value.cited_refs ?? value.selected_refs;
212
+ const citations = normalizeCitations(rawCitations, {
213
+ field: 'preview citations',
214
+ allowedReferences,
215
+ });
216
+ const unknown = normalizeUnknown(value.unknown, 'preview unknown');
217
+ const evidence = normalizeEvidence(value.evidence, {
218
+ field: 'preview evidence',
219
+ allowedReferences,
220
+ requireExactReference: true,
221
+ });
222
+ const readback = normalizeReadback(value.readback, 'preview readback');
223
+ const version = normalizeVersion(value.version, 'preview version');
224
+ const exclusions = Array.isArray(value.exclusions)
225
+ ? value.exclusions.map((entry) => ({ ...entry }))
226
+ : [];
227
+ return {
228
+ answer,
229
+ citations,
230
+ evidence,
231
+ exclusions,
232
+ unknown,
233
+ version,
234
+ readback,
235
+ };
236
+ }
@@ -293,7 +293,16 @@ export class NativeMcpRuntime {
293
293
  await this.options.credentials.remove(connection.connectionId);
294
294
  }
295
295
  }
296
- /** The SDK's OAuth provider for one connection. It never starts an interactive authorization. */
296
+ /**
297
+ * Marks this provider as an authorization-code client without a reachable redirect. The SDK
298
+ * treats a provider with no redirect URL as a non-interactive (client-credentials) client and
299
+ * then never uses the refresh token, so an expired access token could not be renewed.
300
+ */
301
+ const NO_INTERACTIVE_REDIRECT = 'urn:brainbase:meeting-source:no-interactive-redirect';
302
+ /**
303
+ * The SDK's OAuth provider for one connection. It renews tokens with the stored refresh token
304
+ * and never starts an interactive authorization: that path ends in `reauth_required`.
305
+ */
297
306
  class ConnectionOAuthProvider {
298
307
  store;
299
308
  connection;
@@ -302,7 +311,7 @@ class ConnectionOAuthProvider {
302
311
  this.connection = connection;
303
312
  }
304
313
  get redirectUrl() {
305
- return undefined;
314
+ return NO_INTERACTIVE_REDIRECT;
306
315
  }
307
316
  get clientMetadata() {
308
317
  return { redirect_uris: [], client_name: 'Brainbase Meeting Source' };
@@ -0,0 +1,89 @@
1
+ /**
2
+ * The meeting source providers Brainbase treats as effective (Tactiq for
3
+ * online meetings, Plaud.ai for offline recordings and calls), with an
4
+ * optional live check of their public integration surfaces on integrations.sh.
5
+ *
6
+ * Moved unchanged from brainbase-unson server/services/meeting-source/meeting-source-integration-catalog.js
7
+ * (story-document-and-meeting-source-exits-v1).
8
+ */
9
+ export interface MeetingSourceCatalogSurface {
10
+ type: string;
11
+ transport: string;
12
+ configured_by: string;
13
+ confidence: string;
14
+ purpose: string;
15
+ }
16
+ export interface MeetingSourceCatalogProvider {
17
+ provider: string;
18
+ domain: string;
19
+ label: string;
20
+ role: string;
21
+ source_role: string;
22
+ catalog_source: string;
23
+ catalog_status: string;
24
+ surfaces: readonly MeetingSourceCatalogSurface[];
25
+ auth: {
26
+ managed_by: string;
27
+ credential_ref_required: boolean;
28
+ local_secret_storage: boolean;
29
+ user_action: string;
30
+ };
31
+ notes: readonly string[];
32
+ }
33
+ export interface MeetingSourceUpstreamDetect {
34
+ ok: boolean;
35
+ status?: number;
36
+ found?: unknown[];
37
+ mcp?: unknown[];
38
+ integrations_json?: unknown;
39
+ auth?: unknown[];
40
+ error?: string;
41
+ }
42
+ export interface MeetingSourceCatalogEntry extends MeetingSourceCatalogProvider {
43
+ effective: true;
44
+ upstream: {
45
+ name: 'integrations.sh';
46
+ domain: string;
47
+ detect_url: string;
48
+ detect_status: 'pending' | 'not_queried';
49
+ };
50
+ upstream_detect: MeetingSourceUpstreamDetect | null;
51
+ }
52
+ export interface MeetingSourceCatalog {
53
+ version: 1;
54
+ generated_at: string;
55
+ upstream: {
56
+ name: 'integrations.sh';
57
+ base_url: string;
58
+ purpose: string;
59
+ };
60
+ providers: MeetingSourceCatalogEntry[];
61
+ }
62
+ type FetchLike = (url: string, init: {
63
+ headers: Record<string, string>;
64
+ }) => Promise<{
65
+ ok: boolean;
66
+ status: number;
67
+ json(): Promise<unknown>;
68
+ }>;
69
+ export interface MeetingSourceIntegrationCatalogOptions {
70
+ baseURL?: string;
71
+ fetchImpl?: FetchLike | null;
72
+ clock?: () => string;
73
+ }
74
+ export declare class MeetingSourceIntegrationCatalogService {
75
+ baseURL: string;
76
+ fetchImpl: FetchLike | null | undefined;
77
+ clock: () => string;
78
+ constructor({ baseURL, fetchImpl, clock, }?: MeetingSourceIntegrationCatalogOptions);
79
+ listCatalog({ includeLiveDetect }?: {
80
+ includeLiveDetect?: boolean;
81
+ }): Promise<MeetingSourceCatalog>;
82
+ getCatalogEntry(provider: unknown, { includeLiveDetect }?: {
83
+ includeLiveDetect?: boolean;
84
+ }): Promise<MeetingSourceCatalogEntry>;
85
+ refreshProvider(provider: unknown): Promise<MeetingSourceCatalogEntry>;
86
+ detectUpstream(domain: string): Promise<MeetingSourceUpstreamDetect>;
87
+ }
88
+ export declare function createMeetingSourceIntegrationCatalogService(options?: MeetingSourceIntegrationCatalogOptions): MeetingSourceIntegrationCatalogService;
89
+ export {};
@@ -0,0 +1,161 @@
1
+ /**
2
+ * The meeting source providers Brainbase treats as effective (Tactiq for
3
+ * online meetings, Plaud.ai for offline recordings and calls), with an
4
+ * optional live check of their public integration surfaces on integrations.sh.
5
+ *
6
+ * Moved unchanged from brainbase-unson server/services/meeting-source/meeting-source-integration-catalog.js
7
+ * (story-document-and-meeting-source-exits-v1).
8
+ */
9
+ const INTEGRATIONS_SH_BASE_URL = 'https://integrations.sh';
10
+ const MEETING_SOURCE_PROVIDER_CATALOG = Object.freeze({
11
+ tactiq: Object.freeze({
12
+ provider: 'tactiq',
13
+ domain: 'tactiq.io',
14
+ label: 'Tactiq',
15
+ role: 'online_primary',
16
+ source_role: 'online meetings',
17
+ catalog_source: 'brainbase_override',
18
+ catalog_status: 'override_required',
19
+ surfaces: Object.freeze([
20
+ Object.freeze({
21
+ type: 'mcp',
22
+ transport: 'brainbase_runtime',
23
+ configured_by: 'brainbase_mcp_runtime',
24
+ confidence: 'manual_override',
25
+ purpose: 'online meeting transcripts and speaker timeline',
26
+ }),
27
+ ]),
28
+ auth: Object.freeze({
29
+ managed_by: 'brainbase_runtime',
30
+ credential_ref_required: true,
31
+ local_secret_storage: false,
32
+ user_action: 'provider login is completed outside Mac Companion and stored by Brainbase runtime',
33
+ }),
34
+ notes: Object.freeze([
35
+ 'integrations.sh may not detect a published MCP surface yet; Brainbase keeps Tactiq as an effective provider override.',
36
+ 'Calendar data is context only. Tactiq is the primary source for online meetings.',
37
+ ]),
38
+ }),
39
+ plaud: Object.freeze({
40
+ provider: 'plaud',
41
+ domain: 'plaud.ai',
42
+ label: 'Plaud.ai',
43
+ role: 'offline_or_call_primary',
44
+ source_role: 'offline recordings, calls, and online meetings without Tactiq',
45
+ catalog_source: 'brainbase_override',
46
+ catalog_status: 'override_required',
47
+ surfaces: Object.freeze([
48
+ Object.freeze({
49
+ type: 'mcp',
50
+ transport: 'brainbase_runtime',
51
+ configured_by: 'brainbase_mcp_runtime',
52
+ confidence: 'manual_override',
53
+ purpose: 'offline recordings, calls, and fallback meeting transcripts',
54
+ }),
55
+ ]),
56
+ auth: Object.freeze({
57
+ managed_by: 'brainbase_runtime',
58
+ credential_ref_required: true,
59
+ local_secret_storage: false,
60
+ user_action: 'provider login is completed outside Mac Companion and stored by Brainbase runtime',
61
+ }),
62
+ notes: Object.freeze([
63
+ 'integrations.sh may not detect a published MCP surface yet; Brainbase keeps Plaud.ai as an effective provider override.',
64
+ 'Calendar data is context only. Plaud.ai is the primary source for offline meetings and calls.',
65
+ ]),
66
+ }),
67
+ });
68
+ function nowIso() {
69
+ return new Date().toISOString();
70
+ }
71
+ function normalizeProvider(provider) {
72
+ return String(provider || '').trim().toLowerCase();
73
+ }
74
+ function providerEntry(provider) {
75
+ const key = normalizeProvider(provider);
76
+ const entry = MEETING_SOURCE_PROVIDER_CATALOG[key];
77
+ if (!entry) {
78
+ const error = new Error(`unsupported meeting source provider: ${provider}`);
79
+ error.statusCode = 404;
80
+ throw error;
81
+ }
82
+ return structuredClone(entry);
83
+ }
84
+ function integrationDetectURL(baseURL, domain) {
85
+ return `${String(baseURL).replace(/\/$/, '')}/api/${encodeURIComponent(domain)}/detect`;
86
+ }
87
+ export class MeetingSourceIntegrationCatalogService {
88
+ baseURL;
89
+ fetchImpl;
90
+ clock;
91
+ constructor({ baseURL = INTEGRATIONS_SH_BASE_URL, fetchImpl = globalThis.fetch, clock = nowIso, } = {}) {
92
+ this.baseURL = baseURL;
93
+ this.fetchImpl = fetchImpl;
94
+ this.clock = clock;
95
+ }
96
+ async listCatalog({ includeLiveDetect = false } = {}) {
97
+ const providers = Object.keys(MEETING_SOURCE_PROVIDER_CATALOG);
98
+ const entries = [];
99
+ for (const provider of providers) {
100
+ entries.push(await this.getCatalogEntry(provider, { includeLiveDetect }));
101
+ }
102
+ return {
103
+ version: 1,
104
+ generated_at: this.clock(),
105
+ upstream: {
106
+ name: 'integrations.sh',
107
+ base_url: this.baseURL,
108
+ purpose: 'detect public integration surfaces; Brainbase override decides effective meeting-source providers',
109
+ },
110
+ providers: entries,
111
+ };
112
+ }
113
+ async getCatalogEntry(provider, { includeLiveDetect = false } = {}) {
114
+ const entry = providerEntry(provider);
115
+ return {
116
+ ...entry,
117
+ effective: true,
118
+ upstream: {
119
+ name: 'integrations.sh',
120
+ domain: entry.domain,
121
+ detect_url: integrationDetectURL(this.baseURL, entry.domain),
122
+ detect_status: includeLiveDetect ? 'pending' : 'not_queried',
123
+ },
124
+ upstream_detect: includeLiveDetect ? await this.detectUpstream(entry.domain) : null,
125
+ };
126
+ }
127
+ async refreshProvider(provider) {
128
+ return this.getCatalogEntry(provider, { includeLiveDetect: true });
129
+ }
130
+ async detectUpstream(domain) {
131
+ if (typeof this.fetchImpl !== 'function') {
132
+ return {
133
+ ok: false,
134
+ error: 'fetch_not_available',
135
+ };
136
+ }
137
+ try {
138
+ const response = await this.fetchImpl(integrationDetectURL(this.baseURL, domain), {
139
+ headers: { accept: 'application/json' },
140
+ });
141
+ const body = await response.json();
142
+ return {
143
+ ok: response.ok,
144
+ status: response.status,
145
+ found: body?.found || [],
146
+ mcp: body?.mcp || [],
147
+ integrations_json: body?.integrationsJson || null,
148
+ auth: body?.auth || [],
149
+ };
150
+ }
151
+ catch (error) {
152
+ return {
153
+ ok: false,
154
+ error: error?.message || 'upstream_detect_failed',
155
+ };
156
+ }
157
+ }
158
+ }
159
+ export function createMeetingSourceIntegrationCatalogService(options = {}) {
160
+ return new MeetingSourceIntegrationCatalogService(options);
161
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unson/brainbase-mcp",
3
- "version": "0.28.0",
3
+ "version": "0.30.0",
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",
@@ -183,6 +183,18 @@
183
183
  "types": "./dist/knowledge-resolution-service.d.ts",
184
184
  "import": "./dist/knowledge-resolution-service.js"
185
185
  },
186
+ "./canonical-document-writer": {
187
+ "types": "./dist/canonical-document-writer.d.ts",
188
+ "import": "./dist/canonical-document-writer.js"
189
+ },
190
+ "./knowledge-capture-preview": {
191
+ "types": "./dist/knowledge-capture-preview.d.ts",
192
+ "import": "./dist/knowledge-capture-preview.js"
193
+ },
194
+ "./meeting-source-integration-catalog": {
195
+ "types": "./dist/meeting-source-integration-catalog.d.ts",
196
+ "import": "./dist/meeting-source-integration-catalog.js"
197
+ },
186
198
  "./knowledge-adapter": {
187
199
  "types": "./dist/knowledge-adapter.d.ts",
188
200
  "import": "./dist/knowledge-adapter.js"
@@ -439,5 +451,5 @@
439
451
  "engines": {
440
452
  "node": ">=20"
441
453
  },
442
- "gitHead": "b8d86d3e394b94cd77bcfe71e8cb6c8703abce4b"
454
+ "gitHead": "6e6cc04041ae0745272e8e61f53fe93cc06d3e40"
443
455
  }
@@ -42,7 +42,7 @@ const UNREADABLE_LABELS = Object.freeze({
42
42
 
43
43
  // Stroke icons from the organization edition's sprite (apps/web/public/index.html).
44
44
  const NAV_ICON_PATHS = Object.freeze({
45
- today: ['M5 20V10m7 10V4m7 16v-7'],
45
+ 'value-proofs': ['M5 20V10m7 10V4m7 16v-7'],
46
46
  objectives: ['M12 4 3 20h18z', 'M12 9v5m0 3h.01'],
47
47
  projects: ['M3 6.5h7l2 2h9v10H3z', 'M3 6.5v-2h7l2 2'],
48
48
  graph: ['M10 13a5 5 0 0 0 7.5.5l2-2a5 5 0 0 0-7-7l-1.2 1.2', 'M14 11a5 5 0 0 0-7.5-.5l-2 2a5 5 0 0 0 7 7l1.2-1.2'],
@@ -53,7 +53,7 @@ const NAV_ICON_PATHS = Object.freeze({
53
53
  const SVG_NS = 'http://www.w3.org/2000/svg';
54
54
 
55
55
  function makeIcon(doc, id) {
56
- const paths = NAV_ICON_PATHS[id] ?? NAV_ICON_PATHS.today;
56
+ const paths = NAV_ICON_PATHS[id] ?? NAV_ICON_PATHS['value-proofs'];
57
57
  const create = (tag) => (typeof doc.createElementNS === 'function' ? doc.createElementNS(SVG_NS, tag) : doc.createElement(tag));
58
58
  const svg = create('svg');
59
59
  svg.setAttribute('class', 'bb-shell-icon');
@@ -90,13 +90,15 @@ function isRecord(value) {
90
90
  export function parseLocalWebTarget(target) {
91
91
  const raw = typeof target === 'string' ? target.replace(/^#/, '') : '';
92
92
  const separator = raw.indexOf('?');
93
- const screenId = separator < 0 ? raw : raw.slice(0, separator);
94
- if (!screenId) return { screenId: null, entityId: null };
93
+ const requested = separator < 0 ? raw : raw.slice(0, separator);
94
+ if (!requested) return { screenId: null, entityId: null };
95
+ // 判断の履歴 was called 今日 (`#today`) until 2026-10-11; old links still open it.
96
+ const screenId = requested === 'today' ? 'value-proofs' : requested;
95
97
  const query = separator < 0 ? '' : raw.slice(separator + 1);
96
98
  const params = new URLSearchParams(query);
97
- // 今日 accepts one judgment to open (`decision` = decision_attempt_id) and
99
+ // 判断の履歴 accepts one judgment to open (`decision` = decision_attempt_id) and
98
100
  // プロジェクトと関係者 one project (`project` = Graph project id); other screens ignore them.
99
- const decisionId = screenId === 'today' ? params.get('decision')?.trim() || null : null;
101
+ const decisionId = screenId === 'value-proofs' ? params.get('decision')?.trim() || null : null;
100
102
  const projectId = screenId === 'projects' ? params.get('project')?.trim() || null : null;
101
103
  return {
102
104
  screenId,
@@ -250,7 +252,7 @@ function mountGraphScreen(screenId, createView) {
250
252
  }
251
253
 
252
254
  export const LOCAL_WEB_SCREENS = Object.freeze([
253
- Object.freeze({ id: 'today', label: '今日', usesGraph: false, rail: true, source: '判断の記録', mount: mountToday }),
255
+ Object.freeze({ id: 'value-proofs', label: '判断の履歴', usesGraph: false, rail: true, source: '判断の記録', mount: mountToday }),
254
256
  Object.freeze({ id: 'minutes', label: '議事録', usesGraph: false, rail: true, source: '正本の議事録', mount: mountMeetingMinutes }),
255
257
  Object.freeze({ id: 'objectives', label: '目的と現状', usesGraph: true, rail: true, source: '手元のGraph', mount: mountObjectives }),
256
258
  Object.freeze({ id: 'projects', label: 'プロジェクトと関係者', usesGraph: true, rail: true, source: '手元のGraph', mount: mountGraphScreen('projects', createGraphProjectsView) }),
@@ -419,7 +421,7 @@ export function createLocalWebShell({
419
421
  if (mounted.has(screen.id)) {
420
422
  const view = mounted.get(screen.id);
421
423
  if (entityId && screen.id === 'graph' && typeof view?.openEntity === 'function') void view.openEntity(entityId);
422
- if (entityId && screen.id === 'today' && typeof view?.openDecision === 'function') view.openDecision(entityId);
424
+ if (entityId && screen.id === 'value-proofs' && typeof view?.openDecision === 'function') view.openDecision(entityId);
423
425
  if (entityId && screen.id === 'projects' && typeof view?.openProject === 'function') void view.openProject(entityId);
424
426
  return;
425
427
  }
@@ -434,7 +436,7 @@ export function createLocalWebShell({
434
436
  const page = Object.freeze({ crumbs: Object.freeze(['あなたのBrainbase', screen.label]), source: screen.source ?? null });
435
437
  const view = screen.mount(slot, Object.freeze({ ...context, rail, page, initialEntityId: entityId })) ?? true;
436
438
  mounted.set(screen.id, view);
437
- if (entityId && screen.id === 'today' && typeof view?.openDecision === 'function') view.openDecision(entityId);
439
+ if (entityId && screen.id === 'value-proofs' && typeof view?.openDecision === 'function') view.openDecision(entityId);
438
440
  if (entityId && screen.id === 'projects' && typeof view?.openProject === 'function') void view.openProject(entityId);
439
441
  updateRail();
440
442
  }
@@ -447,7 +449,7 @@ export function createLocalWebShell({
447
449
  const parsed = parseLocalWebTarget(target);
448
450
  const screen = screens.find((candidate) => candidate.id === parsed.screenId) ?? screens[0];
449
451
  pendingEntityIds.set(screen.id, screen.id === 'graph' ? parsed.entityId
450
- : screen.id === 'today' ? parsed.decisionId ?? null
452
+ : screen.id === 'value-proofs' ? parsed.decisionId ?? null
451
453
  : screen.id === 'projects' ? parsed.projectId ?? null : null);
452
454
  active = screen.id;
453
455
  for (const [screenId, slot] of slots) slot.hidden = screenId !== screen.id;
@@ -1450,6 +1450,9 @@ function addressSelection() {
1450
1450
  * organization web passes its own route.
1451
1451
  * @param {(site: { task_id: string }) => string | null} [options.taskHref] Where a work site's task opens in the
1452
1452
  * host's task screen (to check or correct it). Without one the rail says it cannot be corrected here.
1453
+ * @param {(decisionAttemptId: string | null) => string} [options.judgmentHref] Where a judgment (or, with null,
1454
+ * the list) opens in the host's 判断の履歴. The local web opens `#value-proofs?decision=<id>`; an organization
1455
+ * web passes its own route.
1453
1456
  * @param {'standard' | 'canvas'} [options.presentation] Canvas owns its accessible overlays and ignores the host rail.
1454
1457
  * @param {{ read(): { business: string | null, site: string | null }, write(selection): void }} [options.selection]
1455
1458
  * @param {(business: object) => (BusinessExitsAnswer | Promise<BusinessExitsAnswer>)} [options.businessExits] The tools
@@ -1460,7 +1463,12 @@ function addressSelection() {
1460
1463
  * others are dropped and counted. Without it the world is drawn exactly as before.
1461
1464
  * @typedef {{ status: string, read_at?: string, reason?: string, exits: object[] }} BusinessExitsAnswer
1462
1465
  */
1463
- export function createWorldView({ root, rail, page, presentation = 'standard', document: explicitDocument, fetcher, projectHref = (project) => `#projects?project=${encodeURIComponent(project.id)}`, taskHref = null, selection = addressSelection(), businessExits = null }) {
1466
+ /** 判断の履歴 in the local web (it was 今日, `#today`, until 2026-10-11; the shell still opens old links). */
1467
+ function defaultJudgmentHref(decisionAttemptId) {
1468
+ return decisionAttemptId ? `#value-proofs?decision=${encodeURIComponent(decisionAttemptId)}` : '#value-proofs';
1469
+ }
1470
+
1471
+ export function createWorldView({ root, rail, page, presentation = 'standard', document: explicitDocument, fetcher, projectHref = (project) => `#projects?project=${encodeURIComponent(project.id)}`, taskHref = null, judgmentHref = defaultJudgmentHref, selection = addressSelection(), businessExits = null }) {
1464
1472
  const exitsEnabled = typeof businessExits === 'function';
1465
1473
  const doc = explicitDocument ?? globalThis.document;
1466
1474
  const reducedMotion = Boolean(globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches);
@@ -1634,9 +1642,9 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
1634
1642
  return `${textValue}${suffix}`;
1635
1643
  }
1636
1644
 
1637
- /** Descends to the same judgment in 今日 (the shell opens `#today?decision=`). */
1645
+ /** Descends to the same judgment in the host's 判断の履歴. */
1638
1646
  function decisionLink(decisionAttemptId) {
1639
- const link = el(doc, 'a', { className: 'bb-world-rail-link', text: '「今日」でこの判断を開く', attrs: { href: `#today?decision=${encodeURIComponent(decisionAttemptId)}` } });
1647
+ const link = el(doc, 'a', { className: 'bb-world-rail-link', text: '「判断の履歴」でこの判断を開く', attrs: { href: judgmentHref(decisionAttemptId) } });
1640
1648
  return link;
1641
1649
  }
1642
1650
 
@@ -2242,7 +2250,7 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
2242
2250
  showRail([
2243
2251
  workspaceRailHead(doc, { kicker: `${business.name} の判断所`, title: `判断 ${data.judgments.length}件`, lead: business.purpose ?? undefined }),
2244
2252
  cityJudgmentBlock(business),
2245
- workspaceRailBlock(doc, { title: '詳しく見る', content: workspaceButton(doc, { text: '「今日」で開く', variant: 'primary', onClick: () => { globalThis.location.hash = '#today'; } }) }),
2253
+ workspaceRailBlock(doc, { title: '詳しく見る', content: workspaceButton(doc, { text: '「判断の履歴」で開く', variant: 'primary', onClick: () => { globalThis.location.hash = judgmentHref(null); } }) }),
2246
2254
  ]);
2247
2255
  return;
2248
2256
  }
@@ -2273,7 +2281,7 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
2273
2281
  blocks.push(workspaceRailBlock(doc, { title: '判断(新しい順)', content: items.length ? ul : { text: '判断はありません' } }));
2274
2282
  blocks.push(workspaceRailBlock(doc, {
2275
2283
  title: '詳しく見る',
2276
- content: workspaceButton(doc, { text: '「今日」で開く', variant: 'primary', onClick: () => { globalThis.location.hash = '#today'; } }),
2284
+ content: workspaceButton(doc, { text: '「判断の履歴」で開く', variant: 'primary', onClick: () => { globalThis.location.hash = judgmentHref(null); } }),
2277
2285
  }));
2278
2286
  showRail(blocks);
2279
2287
  return;
@@ -2295,7 +2303,7 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
2295
2303
  if (withheld) {
2296
2304
  blocks.push(workspaceRailBlock(doc, {
2297
2305
  title: `この世界に出していない判断(${withheld}件)`,
2298
- content: { text: 'この組織の事業で作業したと確かめられない判断です。ほかの会社の仕事が混ざるため、内容も作業場所も出しません。「今日」では見られます。' },
2306
+ content: { text: 'この組織の事業で作業したと確かめられない判断です。ほかの会社の仕事が混ざるため、内容も作業場所も出しません。「判断の履歴」では見られます。' },
2299
2307
  }));
2300
2308
  }
2301
2309
  showRail(blocks);
@@ -2368,11 +2376,11 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
2368
2376
  const waiting = [...proofs.values()].filter(isWaiting).length;
2369
2377
  const places = placesResult.ok && placesResult.data?.status === 'available' ? placesResult.data.places : null;
2370
2378
  if (places) {
2371
- // Only judgments the home lists are placed, so the world and 今日 count the same judgments.
2379
+ // Only judgments the home lists are placed, so the world and 判断の履歴 count the same judgments.
2372
2380
  const known = places.filter((place) => proofs.has(place.decision_attempt_id)).map((place) => ({ ...place, item: proofs.get(place.decision_attempt_id) }));
2373
2381
  placement = groupJudgmentPlaces(known, businesses);
2374
2382
  const placed = known.length - placement.unplaced.length;
2375
- const kept = withheld ? `この組織の事業で作業したと確かめられない判断${withheld}件は、この世界には出していません(「今日」で見られます)。` : '';
2383
+ const kept = withheld ? `この組織の事業で作業したと確かめられない判断${withheld}件は、この世界には出していません(「判断の履歴」で見られます)。` : '';
2376
2384
  note('判断', `${home.delegation_map.judged}件のうち${placed}件を作業した事業の判断所に、${placement.unplaced.length}件は事業が分からないため広場に。${kept}今あなたを待っている判断${waiting}件${waiting ? '(光の柱)' : ''}。`, waiting ? 'attention' : 'info', waiting ? `あなたを待っている判断 ${waiting}件` : null);
2377
2385
  } else {
2378
2386
  note('判断', `${home.delegation_map.judged}件。作業した場所を読めないため、事業には置かず広場の種類別だけで描いています(${placesResult.ok ? placesResult.data?.reason ?? '理由不明' : placesResult.error})。今あなたを待っている判断${waiting}件。`, 'warning', '判断の作業場所:読めない');