@unson/brainbase-mcp 0.40.0 → 0.41.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.
@@ -1,8 +1,8 @@
1
1
  import type { CompanyOsObjectives } from './company-os-objectives.js';
2
2
  import { type FoundationStoreContext } from './foundation-store.js';
3
- import type { FoundationRef } from './foundation-catalog.js';
3
+ import { type FoundationRef } from './foundation-catalog.js';
4
4
  import type { FoundationRevision, ObjectiveDefinition } from './ontology-foundation.js';
5
- import type { FoundationCatalogRecord } from './types.js';
5
+ import type { FoundationCatalogRecord, ObjectiveReadback } from './types.js';
6
6
  /**
7
7
  * HTTP boundary shared by the OSS Objective UI and organization adapters.
8
8
  *
@@ -15,6 +15,7 @@ import type { FoundationCatalogRecord } from './types.js';
15
15
  export declare const FOUNDATION_HTTP_CONTRACT_VERSION: "brainbase.foundation-http.v1";
16
16
  export declare const FOUNDATION_HTTP_PREFIX: "/api/foundation";
17
17
  export declare const FOUNDATION_OBJECTIVE_ROUTE_PREFIX: "/api/foundation/objectives";
18
+ export declare const FOUNDATION_OBJECTIVE_READBACK_CONTRACT_VERSION: "brainbase.objective-readback.v1";
18
19
  export declare const FOUNDATION_OBJECTIVE_ROUTES: readonly string[];
19
20
  type MaybePromise<T> = T | Promise<T>;
20
21
  export interface FoundationHttpRouteContext {
@@ -75,6 +76,8 @@ export interface ObjectiveFoundationHttpRouteOptions {
75
76
  /** Constraint relation reads/writes remain owned by the canonical adapter. */
76
77
  listObjectiveConstraintRefs?: (reference: FoundationRevision, context: FoundationStoreContext) => MaybePromise<unknown>;
77
78
  replaceObjectiveConstraintRefs?: (reference: FoundationRef, refs: readonly FoundationRevision[], context: FoundationStoreContext) => MaybePromise<unknown>;
79
+ /** Injectable clock for deterministic readback evidence; production uses the real clock. */
80
+ clock?: () => Date;
78
81
  bodyLimitBytes?: number;
79
82
  }
80
83
  /** Typed, explicitly sanitized error for trusted host adapters. */
@@ -86,6 +89,13 @@ export declare class FoundationHttpError extends Error {
86
89
  currentRevision?: string;
87
90
  });
88
91
  }
92
+ /**
93
+ * Runtime guard for the Objective detail contract. It is exported so a BFF
94
+ * can fail closed when a canonical host returns an old or malformed payload.
95
+ * The guard only accepts source metadata that agrees with the returned
96
+ * FoundationCatalogRecord; it never creates a source from request data.
97
+ */
98
+ export declare function isObjectiveReadbackPayload(value: unknown): value is ObjectiveReadback;
89
99
  /**
90
100
  * Compose OSS Company OS routes under one local server boundary. The
91
101
  * resolver is called only for a matching route and the resulting trusted
@@ -1,4 +1,5 @@
1
1
  import { FoundationStoreError } from './foundation-store.js';
2
+ import { assertFoundationCatalog, foundationKey } from './foundation-catalog.js';
2
3
  /**
3
4
  * HTTP boundary shared by the OSS Objective UI and organization adapters.
4
5
  *
@@ -11,6 +12,7 @@ import { FoundationStoreError } from './foundation-store.js';
11
12
  export const FOUNDATION_HTTP_CONTRACT_VERSION = 'brainbase.foundation-http.v1';
12
13
  export const FOUNDATION_HTTP_PREFIX = '/api/foundation';
13
14
  export const FOUNDATION_OBJECTIVE_ROUTE_PREFIX = `${FOUNDATION_HTTP_PREFIX}/objectives`;
15
+ export const FOUNDATION_OBJECTIVE_READBACK_CONTRACT_VERSION = 'brainbase.objective-readback.v1';
14
16
  export const FOUNDATION_OBJECTIVE_ROUTES = Object.freeze([
15
17
  'GET /api/foundation/objectives',
16
18
  'GET /api/foundation/objectives/:id',
@@ -23,6 +25,8 @@ export const FOUNDATION_OBJECTIVE_ROUTES = Object.freeze([
23
25
  ]);
24
26
  const DEFAULT_BODY_LIMIT_BYTES = 1024 * 1024;
25
27
  const MUTATING_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
28
+ const OBJECTIVE_READBACK_STATUSES = ['ready', 'error', 'unknown'];
29
+ const FOUNDATION_PROVENANCE_KINDS = new Set(['candidate', 'document', 'observation', 'decision', 'import']);
26
30
  const AUTHORITY_FIELDS = new Set([
27
31
  'acl',
28
32
  'scope',
@@ -78,6 +82,143 @@ export class FoundationHttpError extends Error {
78
82
  this.currentRevision = options.currentRevision;
79
83
  }
80
84
  }
85
+ /**
86
+ * Runtime guard for the Objective detail contract. It is exported so a BFF
87
+ * can fail closed when a canonical host returns an old or malformed payload.
88
+ * The guard only accepts source metadata that agrees with the returned
89
+ * FoundationCatalogRecord; it never creates a source from request data.
90
+ */
91
+ export function isObjectiveReadbackPayload(value) {
92
+ const object = asRecord(value);
93
+ if (!object
94
+ || object.contract_version !== FOUNDATION_OBJECTIVE_READBACK_CONTRACT_VERSION
95
+ || !OBJECTIVE_READBACK_STATUSES.includes(object.status)
96
+ || !isCanonicalIsoTimestamp(object.checked_at)) {
97
+ return false;
98
+ }
99
+ if (object.status === 'ready') {
100
+ if (object.reason !== null)
101
+ return false;
102
+ const record = objectiveReadbackRecord(object.record);
103
+ const objective = objectiveReadbackRecord(object.objective);
104
+ const source = objectiveReadbackSourceValue(object.source);
105
+ if (!record || !objective || !source)
106
+ return false;
107
+ return sameObjectiveIdentity(record, objective)
108
+ && source.source_id === record.definition.id
109
+ && source.source_version === record.definition.revision
110
+ && source.digest === record.digest
111
+ && JSON.stringify(source.provenance) === JSON.stringify(record.definition.provenance);
112
+ }
113
+ return object.source === null
114
+ && object.record === null
115
+ && object.objective === null
116
+ && objectiveReadbackReason(object.reason) !== null;
117
+ }
118
+ function objectiveReadbackSourceFromRecord(record) {
119
+ const normalized = objectiveReadbackRecord(record);
120
+ if (!normalized)
121
+ return null;
122
+ return {
123
+ source_type: 'foundation_revision_store',
124
+ source_id: normalized.definition.id,
125
+ source_version: normalized.definition.revision,
126
+ digest: normalized.digest,
127
+ // Keep the exact canonical provenance array. Do not derive it from the
128
+ // request, a UI label, or a BFF projection.
129
+ provenance: normalized.definition.provenance,
130
+ };
131
+ }
132
+ function objectiveReadbackPayload(status, checkedAt, record, reason) {
133
+ const source = status === 'ready' && record ? objectiveReadbackSourceFromRecord(record) : null;
134
+ if (status === 'ready' && (!source || !record)) {
135
+ throw new Error('objective_readback_source_unavailable');
136
+ }
137
+ return {
138
+ contract_version: FOUNDATION_OBJECTIVE_READBACK_CONTRACT_VERSION,
139
+ status,
140
+ source,
141
+ checked_at: checkedAt,
142
+ record: status === 'ready' ? record : null,
143
+ objective: status === 'ready' ? record : null,
144
+ reason: status === 'ready' ? null : reason,
145
+ };
146
+ }
147
+ function objectiveReadbackReason(value) {
148
+ const object = asRecord(value);
149
+ if (!object || typeof object.code !== 'string' || !object.code.trim()
150
+ || typeof object.message !== 'string' || !object.message.trim())
151
+ return null;
152
+ return { code: object.code, message: object.message };
153
+ }
154
+ function objectiveReadbackRecord(value) {
155
+ const object = asRecord(value);
156
+ const definition = asRecord(object?.definition);
157
+ if (!object || !definition || definition.type !== 'objective'
158
+ || typeof definition.id !== 'string' || !definition.id.trim())
159
+ return null;
160
+ const logicalKey = foundationKey({ type: 'objective', id: definition.id });
161
+ try {
162
+ // Reuse the canonical catalog guard so the readback cannot accept a
163
+ // partial draft shape, an arbitrary digest, or a non-canonical revision.
164
+ // The catalog guard deliberately validates definitions with `use: draft`:
165
+ // displayable draft/adopted Objectives remain readable without turning
166
+ // this transport check into a judgment/evaluation gate.
167
+ assertFoundationCatalog({
168
+ version: 1,
169
+ records: [object],
170
+ latest: {
171
+ [logicalKey]: {
172
+ id: definition.id,
173
+ type: 'objective',
174
+ revision: definition.revision,
175
+ },
176
+ },
177
+ relations: [],
178
+ });
179
+ }
180
+ catch {
181
+ return null;
182
+ }
183
+ return object;
184
+ }
185
+ function objectiveReadbackMatchesRequest(record, requestedId, requestedRevision) {
186
+ return record.definition.id === requestedId
187
+ && (!requestedRevision || record.definition.revision === requestedRevision);
188
+ }
189
+ function objectiveReadbackSourceValue(value) {
190
+ const object = asRecord(value);
191
+ if (!object || object.source_type !== 'foundation_revision_store'
192
+ || typeof object.source_id !== 'string' || !object.source_id.trim()
193
+ || typeof object.source_version !== 'string' || !object.source_version.trim()
194
+ || typeof object.digest !== 'string' || !object.digest.trim()
195
+ || !Array.isArray(object.provenance)
196
+ || !object.provenance.every(isFoundationProvenance))
197
+ return null;
198
+ return object;
199
+ }
200
+ function isFoundationProvenance(value) {
201
+ const object = asRecord(value);
202
+ if (!object)
203
+ return false;
204
+ return typeof object.sourceId === 'string'
205
+ && object.sourceId.trim().length > 0
206
+ && typeof object.sourceKind === 'string'
207
+ && FOUNDATION_PROVENANCE_KINDS.has(object.sourceKind)
208
+ && Array.isArray(object.evidenceIds)
209
+ && object.evidenceIds.every((id) => typeof id === 'string');
210
+ }
211
+ function sameObjectiveIdentity(left, right) {
212
+ return left.definition.id === right.definition.id
213
+ && left.definition.revision === right.definition.revision
214
+ && left.digest === right.digest;
215
+ }
216
+ function isCanonicalIsoTimestamp(value) {
217
+ if (typeof value !== 'string' || !value.trim())
218
+ return false;
219
+ const parsed = new Date(value);
220
+ return !Number.isNaN(parsed.getTime()) && parsed.toISOString() === value;
221
+ }
81
222
  /**
82
223
  * Compose OSS Company OS routes under one local server boundary. The
83
224
  * resolver is called only for a matching route and the resulting trusted
@@ -225,10 +366,34 @@ export function createObjectiveFoundationRoute(options) {
225
366
  return methodNotAllowed(['GET', 'PUT']);
226
367
  }
227
368
  if (method === 'GET') {
228
- const record = await options.objectives.readObjective(id, input.context, revision);
229
- if (!record)
230
- throw new FoundationHttpError(404, 'not_found', `Objective ${id} was not found`);
231
- return jsonResponse(200, { state: 'ready', record, objective: record });
369
+ try {
370
+ const record = await options.objectives.readObjective(id, input.context, revision);
371
+ const checkedAt = objectiveReadbackCheckedAt(options.clock);
372
+ if (!record) {
373
+ return jsonResponse(404, objectiveReadbackPayload('unknown', checkedAt, null, { code: 'not_found', message: `Objective ${id} was not found` }));
374
+ }
375
+ const canonicalRecord = objectiveReadbackRecord(record);
376
+ if (!canonicalRecord) {
377
+ return jsonResponse(502, objectiveReadbackPayload('error', checkedAt, null, { code: 'invalid_canonical_record', message: 'The canonical Objective readback is incomplete' }));
378
+ }
379
+ if (!objectiveReadbackMatchesRequest(canonicalRecord, id, revision)) {
380
+ return jsonResponse(502, objectiveReadbackPayload('error', checkedAt, null, { code: 'readback_identity_mismatch', message: 'The canonical Objective readback does not match the requested id or revision' }));
381
+ }
382
+ // Keep the pre-readback `state` field for existing OSS consumers while
383
+ // the canonical envelope is adopted by newer clients.
384
+ return jsonResponse(200, {
385
+ ...objectiveReadbackPayload('ready', checkedAt, canonicalRecord, null),
386
+ state: 'ready',
387
+ });
388
+ }
389
+ catch (error) {
390
+ const normalized = normalizeHttpError(error);
391
+ const checkedAt = objectiveReadbackCheckedAt(options.clock);
392
+ const status = normalized.status === 404 || normalized.code === 'not_found'
393
+ ? 'unknown'
394
+ : 'error';
395
+ return jsonResponse(normalized.status, objectiveReadbackPayload(status, checkedAt, null, { code: normalized.code, message: normalized.message }));
396
+ }
232
397
  }
233
398
  if (method === 'PUT') {
234
399
  const current = await options.objectives.readObjective(id, input.context, revision);
@@ -252,13 +417,20 @@ export function createObjectiveFoundationRoute(options) {
252
417
  }
253
418
  async function readObjectiveReference(options, id, revision, context) {
254
419
  const record = await options.objectives.readObjective(id, context, revision);
255
- if (!record || record.definition.type !== 'objective')
420
+ if (!record)
256
421
  throw new FoundationHttpError(404, 'not_found', `Objective ${id} was not found`);
422
+ const canonicalRecord = objectiveReadbackRecord(record);
423
+ if (!canonicalRecord) {
424
+ throw new FoundationHttpError(502, 'invalid_canonical_record', 'The canonical Objective readback is incomplete');
425
+ }
426
+ if (!objectiveReadbackMatchesRequest(canonicalRecord, id, revision)) {
427
+ throw new FoundationHttpError(502, 'readback_identity_mismatch', 'The canonical Objective readback does not match the requested id or revision');
428
+ }
257
429
  return {
258
- id: record.definition.id,
430
+ id: canonicalRecord.definition.id,
259
431
  type: 'objective',
260
- revision: record.definition.revision,
261
- digest: record.digest,
432
+ revision: canonicalRecord.definition.revision,
433
+ digest: canonicalRecord.digest,
262
434
  };
263
435
  }
264
436
  async function buildObjectiveDefinition(options, payload, input) {
@@ -337,6 +509,9 @@ function parseConstraintReferences(payload) {
337
509
  function readinessPayload(readiness) {
338
510
  return { state: 'ready', ready: readiness.ready, issues: readiness.issues };
339
511
  }
512
+ function objectiveReadbackCheckedAt(clock) {
513
+ return (clock?.() ?? new Date()).toISOString();
514
+ }
340
515
  function normalizeCollectionPayload(value, key) {
341
516
  if (Array.isArray(value))
342
517
  return { state: value.length ? 'ready' : 'empty', [key]: value, absence_confirmed: true };
@@ -197,6 +197,8 @@ export interface GraphProjectDetail<S extends GraphWebAnySource = GraphWebSource
197
197
  participants: GraphEdgeView[];
198
198
  /** Other relations of the project (governs, owned_by, ...). */
199
199
  relations: GraphEdgeView[];
200
+ /** True only after the same Graph v2 was fully read and strictly validated with no participant edge. */
201
+ participantsAbsenceConfirmed: boolean;
200
202
  history: GraphCorrectionRecord[];
201
203
  issues: GraphReadIssue[];
202
204
  }
package/dist/graph-web.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { access, readdir, readFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
- import { isActiveAt } from './canonical-graph.js';
3
+ import { isActiveAt, validateCanonicalGraph } from './canonical-graph.js';
4
4
  import { assertFoundationCatalog, findFoundationRecord } from './foundation-catalog.js';
5
5
  import { GRAPH_CORRECTION_EDGE_FIELDS, GRAPH_CORRECTION_ENTITY_FIELDS, GRAPH_CORRECTION_NEW_EDGE_RELATIONS, GRAPH_CORRECTION_NEW_ENTITY_TYPES, GRAPH_CORRECTION_PROJECT_FIELDS, GRAPH_CORRECTIONS_FILE, graphRecordDigest, isProjectIconDataUrl, readGraphCorrectionHistory } from './graph-corrections.js';
6
6
  import { ONTOLOGY_VERSION, portableOntology } from './ontology.js';
@@ -213,6 +213,14 @@ export async function readGraphProject(dataDir, projectId, options = {}) {
213
213
  return projectDetailOf(loaded.graph, loaded.source, projectId, asOf, await loadReferences(dataDir));
214
214
  }
215
215
  function projectDetailOf(graph, source, projectId, asOf, references) {
216
+ try {
217
+ // loadPersonalOs intentionally preserves duplicate entity IDs for audit reads;
218
+ // participant absence must use a strict, same-snapshot Graph validation.
219
+ validateCanonicalGraph(graph);
220
+ }
221
+ catch (error) {
222
+ throw new GraphWebError('unavailable', 'graph_unavailable', errorMessage(error));
223
+ }
216
224
  const project = graph.entities.find((entity) => entity.id === projectId && entity.type === 'project');
217
225
  if (!project)
218
226
  throw new GraphWebError('not_found', 'project_not_found', `No project ${projectId}`);
@@ -232,6 +240,7 @@ function projectDetailOf(graph, source, projectId, asOf, references) {
232
240
  },
233
241
  participants,
234
242
  relations,
243
+ participantsAbsenceConfirmed: participants.length === 0,
235
244
  history: historyFor(references.history, touched),
236
245
  issues: references.issues
237
246
  };
@@ -447,7 +447,9 @@ function validJournalEvent(value, sessionRef, key, eventKey) {
447
447
  || !sha256Text(text(value.input_digest))
448
448
  || !sha256Text(text(value.response_digest))
449
449
  || !sha256Text(text(value.event_fingerprint))
450
- || !(text(value.display_line) || value.event_kind === 'turn_resolution' && value.display_line === null))
450
+ || !(value.display_line === undefined
451
+ || value.display_line === null
452
+ || text(value.display_line)))
451
453
  return false;
452
454
  return Boolean(eventKey && /^[a-f0-9]{64}$/u.test(eventKey)
453
455
  && sha256(text(value.tool_use_id)) === eventKey);
@@ -586,7 +586,7 @@ export function createObjectiveFoundationModule(context) {
586
586
  });
587
587
  return {
588
588
  id: 'foundation-objectives',
589
- uiFiles: ['objective-editor.js', 'objective-editor.css', 'objective-editor-http-port.js'],
589
+ uiFiles: ['objective-editor.js', 'objective-editor.css', 'objective-editor-http-port.js', 'objective-readback-contract.js'],
590
590
  async handle(request, response) {
591
591
  const path = requestUrl(request).pathname;
592
592
  if (path !== FOUNDATION_HTTP_PREFIX && !path.startsWith(`${FOUNDATION_HTTP_PREFIX}/`))
@@ -47,8 +47,8 @@ export interface PortableGraphOwner {
47
47
  * `applicability`, constraint exception scopes).
48
48
  *
49
49
  * The result is a read projection, not a bundle to store or send: the input
50
- * is not changed, relation IDs stay those of the stored bundle (they name the
51
- * relation there) and foundation digests are not recomputed. A bundle that
50
+ * is not changed, endpoint-dependent edge IDs are re-derived for the bound
51
+ * projection, and foundation digests are not recomputed. A bundle that
52
52
  * already has another record with the owner's person ID is refused rather
53
53
  * than merged.
54
54
  */
@@ -1,5 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { validateCanonicalGraph } from './canonical-graph.js';
2
+ import { canonicalEdgeId, validateCanonicalGraph } from './canonical-graph.js';
3
3
  import { retrieveGraph } from './graph-retrieval.js';
4
4
  /** The public, read-only interchange format for a canonical Graph v2 snapshot. */
5
5
  export const PORTABLE_GRAPH_SCHEMA_VERSION = 1;
@@ -114,8 +114,8 @@ export const PORTABLE_GRAPH_OWNER_PLACEHOLDER = 'self';
114
114
  * `applicability`, constraint exception scopes).
115
115
  *
116
116
  * The result is a read projection, not a bundle to store or send: the input
117
- * is not changed, relation IDs stay those of the stored bundle (they name the
118
- * relation there) and foundation digests are not recomputed. A bundle that
117
+ * is not changed, endpoint-dependent edge IDs are re-derived for the bound
118
+ * projection, and foundation digests are not recomputed. A bundle that
119
119
  * already has another record with the owner's person ID is refused rather
120
120
  * than merged.
121
121
  */
@@ -160,6 +160,7 @@ export function bindPortableGraphOwner(bundle, owner) {
160
160
  for (const edge of graph.edges) {
161
161
  edge.fromId = swap(edge.fromId);
162
162
  edge.toId = swap(edge.toId);
163
+ edge.id = canonicalEdgeId(edge);
163
164
  }
164
165
  for (const record of graph.foundation?.records ?? []) {
165
166
  const definition = record.definition;
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { FoundationDefinition, FoundationRelationReference, FoundationRevision } from './ontology-foundation.js';
1
+ import type { FoundationDefinition, FoundationRelationReference, FoundationRevision, FoundationProvenance } from './ontology-foundation.js';
2
2
  export type EntityKind = 'person' | 'org' | 'project' | 'relationship' | 'decision';
3
3
  export type CanonicalEntityKind = 'person' | 'org' | 'project' | 'decision';
4
4
  export type CoreRelation = 'member_of' | 'participates_in' | 'accountable_for' | 'owned_by' | 'related_to' | 'governs' | 'supersedes';
@@ -56,6 +56,37 @@ export interface FoundationCatalogRecord {
56
56
  definition: FoundationDefinition;
57
57
  digest: string;
58
58
  }
59
+ /**
60
+ * A detail read is useful only when the host can show which canonical record
61
+ * was actually read. This envelope deliberately keeps the source separate
62
+ * from the record so a UI cannot mistake a client supplied proposal for a
63
+ * canonical provenance.
64
+ */
65
+ export type ObjectiveReadbackStatus = 'ready' | 'error' | 'unknown';
66
+ export interface ObjectiveReadbackReason {
67
+ code: string;
68
+ message: string;
69
+ }
70
+ export interface ObjectiveReadbackSource {
71
+ source_type: 'foundation_revision_store';
72
+ source_id: string;
73
+ source_version: string;
74
+ digest: string;
75
+ provenance: readonly FoundationProvenance[];
76
+ }
77
+ export interface ObjectiveReadback {
78
+ contract_version: 'brainbase.objective-readback.v1';
79
+ status: ObjectiveReadbackStatus;
80
+ /** Null means that no canonical record was established by this read. */
81
+ source: ObjectiveReadbackSource | null;
82
+ /** The time at which the canonical read completed (or failed). */
83
+ checked_at: string;
84
+ record: FoundationCatalogRecord | null;
85
+ /** Compatibility alias retained for existing Objective UI consumers. */
86
+ objective: FoundationCatalogRecord | null;
87
+ /** Ready has no reason; error/unknown must explain the unresolved result. */
88
+ reason: ObjectiveReadbackReason | null;
89
+ }
59
90
  export interface FoundationCatalog {
60
91
  version: 1;
61
92
  records: FoundationCatalogRecord[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unson/brainbase-mcp",
3
- "version": "0.40.0",
3
+ "version": "0.41.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",
@@ -481,5 +481,5 @@
481
481
  "engines": {
482
482
  "node": ">=20"
483
483
  },
484
- "gitHead": "05c940b491a191339b9bc59ffb123579220714a3"
484
+ "gitHead": "c460d39f08bd68e8630fcdd000a934017f41203c"
485
485
  }
@@ -148,6 +148,17 @@ export function normalizeFoundationCatalog(value, expectedScopeId) {
148
148
  if (scopeId !== expectedScopeId) {
149
149
  throw error('scope_mismatch', '選択した対象範囲と応答の対象範囲が一致しません。');
150
150
  }
151
+ const readStatus = nonEmpty(firstValue(value, 'read_status', 'readStatus'));
152
+ const catalogStatus = nonEmpty(value.status);
153
+ const knownReadableStatuses = new Set(['complete', 'ready', 'resolved', 'empty']);
154
+ const unreadableStatus = (readStatus && !knownReadableStatuses.has(readStatus))
155
+ || (catalogStatus && !knownReadableStatuses.has(catalogStatus));
156
+ const collections = [value.objectives, value.variables, value.models, value.philosophies];
157
+ const hasRecords = collections.some((records) => Array.isArray(records) && records.length > 0);
158
+ const absenceConfirmed = value.absence_confirmed === true;
159
+ if (unreadableStatus || (!hasRecords && ('absence_confirmed' in value || readStatus || catalogStatus) && !absenceConfirmed)) {
160
+ throw error('unavailable', '目的・哲学・世界モデルの取得状態を確認できません。');
161
+ }
151
162
  validateFoundationRecords(value.objectives, 'objective', '目的');
152
163
  validateFoundationRecords(value.variables, 'variable', '変数');
153
164
  validateFoundationRecords(value.models, 'model', '世界モデル');
@@ -159,6 +170,10 @@ export function normalizeFoundationCatalog(value, expectedScopeId) {
159
170
  variables: value.variables,
160
171
  models: value.models,
161
172
  philosophies: value.philosophies,
173
+ ...(readStatus ? { read_status: readStatus } : {}),
174
+ ...(catalogStatus ? { status: catalogStatus } : {}),
175
+ ...(typeof value.absence_confirmed === 'boolean' ? { absence_confirmed: value.absence_confirmed } : {}),
176
+ ...(value.source !== undefined ? { source: value.source } : {}),
162
177
  };
163
178
  }
164
179
 
@@ -818,7 +833,8 @@ export function createFoundationOverview({ root, port, scopeId, document: explic
818
833
  if (destroyed || requestGeneration !== generation) return state;
819
834
  const code = cause?.code === 'scope_mismatch' ? 'scope_mismatch'
820
835
  : cause?.code === 'invalid_schema' ? 'invalid'
821
- : cause?.code === 'authorization_denied' || cause?.code === 'forbidden' ? 'denied' : 'error';
836
+ : cause?.code === 'authorization_denied' || cause?.code === 'forbidden' ? 'denied'
837
+ : cause?.code === 'unavailable' ? 'unavailable' : 'error';
822
838
  state.status = code;
823
839
  state.error = {
824
840
  code,
@@ -80,7 +80,8 @@ export function graphOwnShareSource(option, { fetcher } = {}) {
80
80
  export function ownShareHandoverText(handover) {
81
81
  if (!isRecord(handover) || !textOrNull(handover.graphId)) return null;
82
82
  const where = textOrNull(handover.projectCode);
83
- return `束 ${handover.graphId}(${where ? `置き場所 ${where}、` : ''}${formatDateTime(handover.createdAt)} に送信)`;
83
+ const digest = textOrNull(handover.digest) ?? '未確認';
84
+ return `束 ${handover.graphId}(${where ? `置き場所 ${where}、` : ''}digest ${digest}、${formatDateTime(handover.createdAt)} に送信)`;
84
85
  }
85
86
 
86
87
  /**
@@ -216,7 +217,8 @@ export function ownEntityDetail(doc, detail, { onClose, onRetry, onOpen } = {})
216
217
  ...(entity.type === 'project' ? [['目的', textOrNull(entity.goal)], ['状態', textOrNull(entity.status)]] : []),
217
218
  ]),
218
219
  }));
219
- wrap.append(decisionRecordBlock(doc, entity, loaded.decisionRecord));
220
+ const decisionBlock = decisionRecordBlock(doc, entity, loaded.decisionRecord);
221
+ if (decisionBlock) wrap.append(decisionBlock);
220
222
  for (const [key, title, empty] of [['outgoing', '出る関係', 'この記録から出る関係はありません。'], ['incoming', '入る関係', 'この記録へ入る関係はありません。']]) {
221
223
  const edges = Array.isArray(loaded[key]) ? loaded[key] : [];
222
224
  wrap.append(workspaceRailBlock(doc, {