@unson/brainbase-mcp 0.7.0 → 0.8.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.
- package/contracts/foundation/graph-history.sql +634 -0
- package/contracts/judgment-value-proof/schema.json +43 -1
- package/dist/canonical-task-service.js +16 -5
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +107 -0
- package/dist/company-os-objectives.d.ts +11 -0
- package/dist/company-os-objectives.js +84 -47
- package/dist/foundation-authenticated-tools.d.ts +56 -0
- package/dist/foundation-authenticated-tools.js +347 -0
- package/dist/foundation-graph-http.d.ts +43 -0
- package/dist/foundation-graph-http.js +243 -0
- package/dist/foundation-graph-write.d.ts +90 -0
- package/dist/foundation-graph-write.js +447 -0
- package/dist/foundation-public-provider.d.ts +140 -0
- package/dist/foundation-public-provider.js +192 -0
- package/dist/graph-corrections.d.ts +157 -0
- package/dist/graph-corrections.js +620 -0
- package/dist/graph-foundation-reader.d.ts +60 -0
- package/dist/graph-foundation-reader.js +494 -0
- package/dist/graph-retrieval.d.ts +6 -1
- package/dist/graph-revision-reader.d.ts +80 -0
- package/dist/graph-revision-reader.js +246 -0
- package/dist/graph-web-http.d.ts +26 -0
- package/dist/graph-web-http.js +215 -0
- package/dist/graph-web.d.ts +364 -0
- package/dist/graph-web.js +639 -0
- package/dist/judgment-problem-snapshot.d.ts +30 -2
- package/dist/judgment-problem-snapshot.js +302 -6
- package/dist/judgment-value-proof-review.d.ts +166 -0
- package/dist/judgment-value-proof-review.js +437 -0
- package/dist/judgment-value-proof.d.ts +28 -1
- package/dist/judgment-value-proof.js +53 -8
- package/dist/knowledge-continuation.d.ts +127 -0
- package/dist/knowledge-continuation.js +446 -0
- package/dist/knowledge-lookup.d.ts +178 -0
- package/dist/knowledge-lookup.js +758 -0
- package/dist/local-web-fetch-bridge.d.ts +5 -0
- package/dist/local-web-fetch-bridge.js +37 -0
- package/dist/local-web-host.d.ts +109 -0
- package/dist/local-web-host.js +688 -0
- package/dist/local-web-security.d.ts +25 -0
- package/dist/local-web-security.js +95 -0
- package/dist/organization-graph.d.ts +15 -0
- package/dist/organization-graph.js +24 -4
- package/dist/personal-knowledge.d.ts +12 -0
- package/dist/personal-knowledge.js +29 -0
- package/dist/personal-memory-handover.d.ts +108 -0
- package/dist/personal-memory-handover.js +369 -0
- package/dist/philosophy-revision-reader.d.ts +85 -0
- package/dist/philosophy-revision-reader.js +241 -0
- package/dist/portable-graph.d.ts +24 -0
- package/dist/portable-graph.js +77 -0
- package/dist/server.d.ts +7 -2
- package/dist/server.js +9 -6
- package/dist/value-proof-review-http.d.ts +25 -0
- package/dist/value-proof-review-http.js +185 -0
- package/dist/world-model.d.ts +21 -3
- package/dist/world-model.js +94 -12
- package/package.json +94 -3
- package/ui/brainbase-tokens.css +56 -0
- package/ui/graph-own-share.js +367 -0
- package/ui/graph-projects-view.css +23 -0
- package/ui/graph-projects-view.js +730 -0
- package/ui/graph-registry-view.css +101 -0
- package/ui/graph-registry-view.js +804 -0
- package/ui/graph-view-shared.css +522 -0
- package/ui/graph-view-shared.js +1227 -0
- package/ui/judgment-view.css +119 -45
- package/ui/local-web-shell.css +396 -0
- package/ui/local-web-shell.js +378 -0
- package/ui/objective-editor-http-port.js +154 -0
- package/ui/objective-editor.css +180 -74
- package/ui/objective-editor.js +490 -43
- package/ui/outcome-knowledge.css +50 -14
- package/ui/outcome-knowledge.js +15 -1
- package/ui/outcome-mana.css +91 -14
- package/ui/outcome-mana.js +958 -61
- package/ui/value-proof-review.css +504 -0
- package/ui/value-proof-review.js +1104 -0
- package/ui/workspace-kit.css +485 -0
- package/ui/workspace-kit.js +229 -0
- package/ui/world-model-view.css +188 -0
- package/ui/world-model-view.js +486 -0
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
import { cloneFoundationDefinition, digestFoundationDefinition } from './foundation-catalog.js';
|
|
2
|
+
import { canonicalPortableJson } from './portable-graph.js';
|
|
3
|
+
import { FoundationStoreError } from './foundation-store.js';
|
|
4
|
+
import { philosophyRevisionDigest } from './philosophy-revision-reader.js';
|
|
5
|
+
import { validateFoundationDefinition } from './ontology-foundation.js';
|
|
6
|
+
/** Read contract for history that is joined to the current Graph row. */
|
|
7
|
+
export const GRAPH_FOUNDATION_READ_CONTRACT_VERSION = 'graph-foundation-history-read.v1';
|
|
8
|
+
/**
|
|
9
|
+
* The query deliberately joins the historical row to the current Graph row.
|
|
10
|
+
* The host supplies a transaction-bound query function; its RLS policies make
|
|
11
|
+
* an invisible current row disappear before the adapter sees the result.
|
|
12
|
+
*
|
|
13
|
+
* `storage_digest_valid` is calculated by PostgreSQL over `payload::text` so
|
|
14
|
+
* that an adapter never has to reproduce database JSON serialization rules.
|
|
15
|
+
* `current_visible` is selected explicitly to make fake/test adapters and
|
|
16
|
+
* future providers fail closed when they do not prove current-row visibility.
|
|
17
|
+
*/
|
|
18
|
+
export const GRAPH_FOUNDATION_READ_SQL = `
|
|
19
|
+
SELECT
|
|
20
|
+
history.entity_id,
|
|
21
|
+
history.entity_type,
|
|
22
|
+
history.revision,
|
|
23
|
+
history.payload,
|
|
24
|
+
history.project_id,
|
|
25
|
+
history.role_min,
|
|
26
|
+
history.sensitivity,
|
|
27
|
+
history.lifecycle_status,
|
|
28
|
+
history.storage_digest,
|
|
29
|
+
history.captured_at,
|
|
30
|
+
(
|
|
31
|
+
history.storage_digest = 'sha256:' ||
|
|
32
|
+
encode(sha256(convert_to(history.payload::text, 'UTF8')), 'hex')
|
|
33
|
+
) AS storage_digest_valid,
|
|
34
|
+
current_entity.id AS current_entity_id,
|
|
35
|
+
current_entity.entity_type AS current_entity_type,
|
|
36
|
+
current_entity.payload AS current_payload,
|
|
37
|
+
current_entity.project_id AS current_project_id,
|
|
38
|
+
current_entity.role_min AS current_role_min,
|
|
39
|
+
current_entity.sensitivity AS current_sensitivity,
|
|
40
|
+
current_entity.lifecycle_status AS current_lifecycle_status,
|
|
41
|
+
projects.code AS current_project_code,
|
|
42
|
+
true AS current_visible
|
|
43
|
+
FROM public.graph_foundation_revisions AS history
|
|
44
|
+
JOIN public.graph_entities AS current_entity
|
|
45
|
+
ON current_entity.id = history.entity_id
|
|
46
|
+
AND current_entity.entity_type = history.entity_type
|
|
47
|
+
JOIN public.projects AS projects
|
|
48
|
+
ON projects.id = current_entity.project_id
|
|
49
|
+
WHERE history.entity_id = $1
|
|
50
|
+
AND history.entity_type = $2
|
|
51
|
+
AND history.revision = $3
|
|
52
|
+
`;
|
|
53
|
+
const FOUNDATION_TYPES = ['objective', 'variable', 'model', 'constraint'];
|
|
54
|
+
const REVISION_PATTERN = /^[1-9]\d*$/u;
|
|
55
|
+
const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/u;
|
|
56
|
+
const ACTIVE_LIFECYCLE_STATUSES = new Set(['active']);
|
|
57
|
+
const PHILOSOPHY_SCOPE_TYPES = new Set(['project', 'organization']);
|
|
58
|
+
/**
|
|
59
|
+
* Create both public read ports over one trusted, transaction-bound context.
|
|
60
|
+
* No principal or tenant is read from a request payload. The context is
|
|
61
|
+
* closed over by both readers and a caller-supplied context must carry the
|
|
62
|
+
* same principal before any query is issued.
|
|
63
|
+
*/
|
|
64
|
+
export function createGraphFoundationReaders(options) {
|
|
65
|
+
assertReaderOptions(options);
|
|
66
|
+
const context = cloneContext(options.context);
|
|
67
|
+
const query = options.query;
|
|
68
|
+
const store = {
|
|
69
|
+
async read(reference, suppliedContext) {
|
|
70
|
+
assertTrustedPrincipal(context, suppliedContext);
|
|
71
|
+
assertFoundationReference(reference);
|
|
72
|
+
const rows = await executeRead(query, reference.id, reference.type, reference.revision);
|
|
73
|
+
if (rows.length === 0)
|
|
74
|
+
return null;
|
|
75
|
+
if (rows.length !== 1) {
|
|
76
|
+
throw corrupt('Graph foundation history returned multiple rows for one revision');
|
|
77
|
+
}
|
|
78
|
+
const row = rows[0];
|
|
79
|
+
const history = validateHistoryRow(row, reference.id, reference.type, reference.revision);
|
|
80
|
+
const current = validateCurrentRow(row, reference.id, reference.type);
|
|
81
|
+
authorizeCurrentFoundation(current, context);
|
|
82
|
+
// A history row remains readable only through the current Graph row,
|
|
83
|
+
// including its current project and current scope. The old payload is
|
|
84
|
+
// returned unchanged after these current-row checks.
|
|
85
|
+
if (history.projectId !== current.projectId) {
|
|
86
|
+
throw scopeViolation('Historical foundation project differs from current Graph project');
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
definition: cloneFoundationDefinition(history.definition),
|
|
90
|
+
digest: digestFoundationDefinition(history.definition)
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const philosophyReader = {
|
|
95
|
+
async readCanonical(input) {
|
|
96
|
+
return readCanonicalPhilosophyRevision(input, context, query);
|
|
97
|
+
},
|
|
98
|
+
async read(input) {
|
|
99
|
+
return readPhilosophyRevision(input, context, query);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
return { store, philosophyReader };
|
|
103
|
+
}
|
|
104
|
+
async function readCanonicalPhilosophyRevision(input, trustedContext, query) {
|
|
105
|
+
try {
|
|
106
|
+
assertCanonicalPhilosophyReadRequest(input);
|
|
107
|
+
assertTrustedPrincipal(trustedContext, input.context);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
return {
|
|
111
|
+
status: 'unauthorized',
|
|
112
|
+
message: errorMessage(error, 'Philosophy revision read is outside the trusted context')
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (!trustedContext.scope || !input.context.scope) {
|
|
116
|
+
return { status: 'unauthorized', message: 'A trusted scope is required for canonical philosophy reads' };
|
|
117
|
+
}
|
|
118
|
+
let rows;
|
|
119
|
+
try {
|
|
120
|
+
rows = await executeRead(query, input.id, 'philosophy', input.revision);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return { status: 'corrupt', message: 'Philosophy revision history query failed closed' };
|
|
124
|
+
}
|
|
125
|
+
if (rows.length === 0)
|
|
126
|
+
return { status: 'missing', message: 'Philosophy revision was not found' };
|
|
127
|
+
if (rows.length !== 1)
|
|
128
|
+
return { status: 'corrupt', message: 'Philosophy revision history returned multiple rows' };
|
|
129
|
+
try {
|
|
130
|
+
const validated = validatePhilosophyHistoryAccess(rowOrThrow(rows), input.id, input.revision, trustedContext);
|
|
131
|
+
return {
|
|
132
|
+
status: 'resolved',
|
|
133
|
+
record: {
|
|
134
|
+
kind: 'philosophy',
|
|
135
|
+
id: input.id,
|
|
136
|
+
revision: input.revision,
|
|
137
|
+
digest: validated.digest,
|
|
138
|
+
payload: validated.history.payload,
|
|
139
|
+
applicability: validated.history.applicability,
|
|
140
|
+
// This is a read-time projection of the current Graph permission. It
|
|
141
|
+
// does not mutate the canonical philosophy payload or Graph ACL.
|
|
142
|
+
currentAcl: privateCurrentAcl(trustedContext.principal),
|
|
143
|
+
currentScope: cloneJudgmentScope(validated.history.applicability.scope)
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
const message = errorMessage(error, 'Philosophy revision failed integrity validation');
|
|
149
|
+
if (error instanceof FoundationStoreError && error.code === 'authorization_denied') {
|
|
150
|
+
return { status: 'unauthorized', message };
|
|
151
|
+
}
|
|
152
|
+
return { status: 'corrupt', message };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async function readPhilosophyRevision(input, trustedContext, query) {
|
|
156
|
+
try {
|
|
157
|
+
assertPhilosophyReadRequest(input);
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
return { status: 'corrupt', message: errorMessage(error, 'Philosophy revision request is invalid') };
|
|
161
|
+
}
|
|
162
|
+
const reference = input.reference;
|
|
163
|
+
if (input.context.principal !== trustedContext.principal) {
|
|
164
|
+
return { status: 'unauthorized', message: 'The requested principal is outside the trusted read context' };
|
|
165
|
+
}
|
|
166
|
+
if (!trustedContext.scope || !scopeAllowed(reference.scope, trustedContext.scope)) {
|
|
167
|
+
return { status: 'unauthorized', message: 'Philosophy revision is outside the trusted project or organization scope' };
|
|
168
|
+
}
|
|
169
|
+
let rows;
|
|
170
|
+
try {
|
|
171
|
+
rows = await executeRead(query, reference.id, 'philosophy', reference.revision);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return { status: 'corrupt', message: 'Philosophy revision history query failed closed' };
|
|
175
|
+
}
|
|
176
|
+
if (rows.length === 0)
|
|
177
|
+
return { status: 'missing', message: 'Philosophy revision was not found' };
|
|
178
|
+
if (rows.length !== 1)
|
|
179
|
+
return { status: 'corrupt', message: 'Philosophy revision history returned multiple rows' };
|
|
180
|
+
const row = rows[0];
|
|
181
|
+
try {
|
|
182
|
+
const validated = validatePhilosophyHistoryAccess(row, reference.id, reference.revision, trustedContext);
|
|
183
|
+
if (!scopeEqual(validated.history.applicability.scope, reference.scope)) {
|
|
184
|
+
return { status: 'unauthorized', message: 'Philosophy revision applicability scope does not match the request' };
|
|
185
|
+
}
|
|
186
|
+
if (!scopeEqual(validated.current.philosophy.applicability.scope, reference.scope)) {
|
|
187
|
+
return { status: 'unauthorized', message: 'Current philosophy applicability scope does not match the request' };
|
|
188
|
+
}
|
|
189
|
+
if (validated.digest !== reference.digest) {
|
|
190
|
+
return { status: 'corrupt', message: 'Philosophy revision digest does not match the requested reference' };
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
status: 'resolved',
|
|
194
|
+
record: {
|
|
195
|
+
kind: 'philosophy',
|
|
196
|
+
id: reference.id,
|
|
197
|
+
revision: reference.revision,
|
|
198
|
+
digest: validated.digest,
|
|
199
|
+
payload: validated.history.payload,
|
|
200
|
+
applicability: validated.history.applicability,
|
|
201
|
+
// This is a read-time projection of the current Graph permission. It
|
|
202
|
+
// does not mutate the canonical philosophy payload or Graph ACL.
|
|
203
|
+
currentAcl: privateCurrentAcl(trustedContext.principal),
|
|
204
|
+
currentScope: cloneJudgmentScope(reference.scope)
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
const message = errorMessage(error, 'Philosophy revision failed integrity validation');
|
|
210
|
+
if (error instanceof FoundationStoreError && error.code === 'authorization_denied') {
|
|
211
|
+
return { status: 'unauthorized', message };
|
|
212
|
+
}
|
|
213
|
+
return { status: 'corrupt', message };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function validatePhilosophyHistoryAccess(row, id, revision, trustedContext) {
|
|
217
|
+
const history = validatePhilosophyHistoryRow(row, { id, revision });
|
|
218
|
+
const current = validateCurrentRow(row, id, 'philosophy');
|
|
219
|
+
const philosophy = current.philosophy;
|
|
220
|
+
if (!philosophy) {
|
|
221
|
+
throw corrupt('Current Graph philosophy applicability is missing');
|
|
222
|
+
}
|
|
223
|
+
if (history.projectId !== current.projectId) {
|
|
224
|
+
throw new FoundationStoreError('authorization_denied', 'Philosophy history moved outside its current project');
|
|
225
|
+
}
|
|
226
|
+
if (!trustedContext.scope || !scopeAllowed(history.applicability.scope, trustedContext.scope) ||
|
|
227
|
+
!scopeAllowed(philosophy.applicability.scope, trustedContext.scope)) {
|
|
228
|
+
throw new FoundationStoreError('authorization_denied', 'Current philosophy scope is outside the trusted read context');
|
|
229
|
+
}
|
|
230
|
+
if (!scopeEqual(history.applicability.scope, philosophy.applicability.scope)) {
|
|
231
|
+
throw new FoundationStoreError('authorization_denied', 'Historical and current philosophy applicability scopes do not match');
|
|
232
|
+
}
|
|
233
|
+
const digest = philosophyRevisionDigest({
|
|
234
|
+
kind: 'philosophy',
|
|
235
|
+
id,
|
|
236
|
+
revision,
|
|
237
|
+
payload: history.payload,
|
|
238
|
+
applicability: history.applicability
|
|
239
|
+
});
|
|
240
|
+
return { history, current: { ...current, philosophy }, digest };
|
|
241
|
+
}
|
|
242
|
+
async function executeRead(query, id, type, revision) {
|
|
243
|
+
let result;
|
|
244
|
+
try {
|
|
245
|
+
result = await query(GRAPH_FOUNDATION_READ_SQL, [id, type, revision]);
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
throw corrupt('Graph foundation history query failed');
|
|
249
|
+
}
|
|
250
|
+
if (Array.isArray(result))
|
|
251
|
+
return result;
|
|
252
|
+
if (!Array.isArray(result) && 'rows' in result && Array.isArray(result.rows)) {
|
|
253
|
+
return result.rows;
|
|
254
|
+
}
|
|
255
|
+
throw corrupt('Graph foundation history query returned an invalid result');
|
|
256
|
+
}
|
|
257
|
+
function validateHistoryRow(row, id, type, revision) {
|
|
258
|
+
assertRowIdentity(row, id, type, revision);
|
|
259
|
+
assertStorageDigest(row);
|
|
260
|
+
const payload = requireObject(row.payload, 'Foundation history payload');
|
|
261
|
+
const definition = requireObject(payload.foundation, 'Foundation history payload.foundation');
|
|
262
|
+
const result = validateFoundationDefinition(definition, { use: 'draft' });
|
|
263
|
+
if (!result.valid)
|
|
264
|
+
throw corrupt(`Foundation history definition is invalid: ${result.issues.map((issue) => issue.path).join(', ')}`);
|
|
265
|
+
if (definition.id !== id || definition.type !== type || definition.revision !== revision) {
|
|
266
|
+
throw corrupt('Foundation history definition identity does not match the requested revision');
|
|
267
|
+
}
|
|
268
|
+
const projectId = requireString(row.project_id, 'Foundation history project_id');
|
|
269
|
+
return { definition, projectId };
|
|
270
|
+
}
|
|
271
|
+
function validatePhilosophyHistoryRow(row, reference) {
|
|
272
|
+
assertRowIdentity(row, reference.id, 'philosophy', reference.revision);
|
|
273
|
+
assertStorageDigest(row);
|
|
274
|
+
const payload = requireJsonValue(row.payload, 'Philosophy history payload');
|
|
275
|
+
const applicability = parseJudgmentApplicability(payload);
|
|
276
|
+
const projectId = requireString(row.project_id, 'Philosophy history project_id');
|
|
277
|
+
return { payload, applicability, projectId };
|
|
278
|
+
}
|
|
279
|
+
function assertCanonicalPhilosophyReadRequest(input) {
|
|
280
|
+
if (!isPlainRecord(input) || typeof input.id !== 'string' || input.id.trim().length === 0 || input.id.includes('\0') ||
|
|
281
|
+
typeof input.revision !== 'string' || !REVISION_PATTERN.test(input.revision) ||
|
|
282
|
+
!isPlainRecord(input.context) || typeof input.context.principal !== 'string' ||
|
|
283
|
+
input.context.principal.trim().length === 0 || input.context.principal.includes('\0')) {
|
|
284
|
+
throw new TypeError('Canonical philosophy revision request is invalid');
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function rowOrThrow(rows) {
|
|
288
|
+
const row = rows[0];
|
|
289
|
+
if (!row)
|
|
290
|
+
throw corrupt('Philosophy revision history row is missing');
|
|
291
|
+
return row;
|
|
292
|
+
}
|
|
293
|
+
function validateCurrentRow(row, id, type) {
|
|
294
|
+
if (row.current_visible !== true) {
|
|
295
|
+
throw new FoundationStoreError('authorization_denied', 'The current Graph row is not visible in the trusted transaction');
|
|
296
|
+
}
|
|
297
|
+
if (row.current_entity_id !== id || row.current_entity_type !== type) {
|
|
298
|
+
throw corrupt('Current Graph identity does not match the historical row');
|
|
299
|
+
}
|
|
300
|
+
if (!ACTIVE_LIFECYCLE_STATUSES.has(String(row.current_lifecycle_status))) {
|
|
301
|
+
throw new FoundationStoreError('authorization_denied', 'The current Graph row is not active');
|
|
302
|
+
}
|
|
303
|
+
const projectId = requireString(row.current_project_id, 'Current Graph project_id');
|
|
304
|
+
const payload = requireObject(row.current_payload, 'Current Graph payload');
|
|
305
|
+
if (type === 'philosophy') {
|
|
306
|
+
const applicability = parseJudgmentApplicability(payload);
|
|
307
|
+
return { payload, projectId, philosophy: { applicability } };
|
|
308
|
+
}
|
|
309
|
+
const definition = requireObject(payload.foundation, 'Current Graph payload.foundation');
|
|
310
|
+
const result = validateFoundationDefinition(definition, { use: 'draft' });
|
|
311
|
+
if (!result.valid || definition.id !== id || definition.type !== type) {
|
|
312
|
+
throw corrupt('Current Graph foundation definition is invalid');
|
|
313
|
+
}
|
|
314
|
+
return { payload, projectId, foundation: definition };
|
|
315
|
+
}
|
|
316
|
+
function authorizeCurrentFoundation(current, context) {
|
|
317
|
+
if (!current.foundation)
|
|
318
|
+
throw corrupt('Current Graph foundation definition is missing');
|
|
319
|
+
const definition = current.foundation;
|
|
320
|
+
if (!canRead(definition.acl, context.principal)) {
|
|
321
|
+
throw new FoundationStoreError('authorization_denied', 'The current foundation ACL does not permit this principal');
|
|
322
|
+
}
|
|
323
|
+
if (context.scope && !scopeAllowed(definition.scope, context.scope)) {
|
|
324
|
+
throw new FoundationStoreError('scope_violation', 'The current foundation scope is outside the trusted context');
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function assertStorageDigest(row) {
|
|
328
|
+
if (row.storage_digest_valid !== true || typeof row.storage_digest !== 'string' || !DIGEST_PATTERN.test(row.storage_digest)) {
|
|
329
|
+
throw corrupt('Foundation history storage digest is missing or invalid');
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function assertRowIdentity(row, id, type, revision) {
|
|
333
|
+
if (row.entity_id !== id || row.entity_type !== type || row.revision !== revision) {
|
|
334
|
+
throw corrupt('Foundation history identity does not match the requested revision');
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function assertFoundationReference(reference) {
|
|
338
|
+
if (!isPlainRecord(reference) || typeof reference.id !== 'string' || reference.id.length === 0 ||
|
|
339
|
+
!FOUNDATION_TYPES.includes(reference.type) ||
|
|
340
|
+
typeof reference.revision !== 'string' || !REVISION_PATTERN.test(reference.revision)) {
|
|
341
|
+
throw new FoundationStoreError('invalid_input', 'A foundation revision reference requires id, type, and a positive revision');
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function assertPhilosophyReadRequest(input) {
|
|
345
|
+
if (!isPlainRecord(input) || !isPlainRecord(input.reference) || input.reference.kind !== 'philosophy' ||
|
|
346
|
+
typeof input.reference.id !== 'string' || input.reference.id.length === 0 ||
|
|
347
|
+
typeof input.reference.revision !== 'string' || !REVISION_PATTERN.test(input.reference.revision) ||
|
|
348
|
+
typeof input.reference.digest !== 'string' || !DIGEST_PATTERN.test(input.reference.digest) ||
|
|
349
|
+
!isPlainRecord(input.reference.scope) || !PHILOSOPHY_SCOPE_TYPES.has(String(input.reference.scope.type)) ||
|
|
350
|
+
typeof input.reference.scope.id !== 'string' || input.reference.scope.id.length === 0 ||
|
|
351
|
+
!isPlainRecord(input.context) || typeof input.context.principal !== 'string' ||
|
|
352
|
+
!input.context.principal || !isPlainRecord(input.context.scope)) {
|
|
353
|
+
throw new TypeError('Philosophy revision request is invalid');
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function assertReaderOptions(options) {
|
|
357
|
+
if (!options || typeof options !== 'object' || typeof options.query !== 'function') {
|
|
358
|
+
throw new TypeError('A transaction-bound Graph foundation query is required');
|
|
359
|
+
}
|
|
360
|
+
if (!options.context || typeof options.context.principal !== 'string' || options.context.principal.trim().length === 0) {
|
|
361
|
+
throw new TypeError('A trusted Graph foundation principal is required');
|
|
362
|
+
}
|
|
363
|
+
if (options.context.scope !== undefined) {
|
|
364
|
+
const scope = options.context.scope;
|
|
365
|
+
if (!isValidFoundationScope(scope))
|
|
366
|
+
throw new TypeError('A trusted Graph foundation scope is invalid');
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function assertTrustedPrincipal(trusted, supplied) {
|
|
370
|
+
if (!supplied || supplied.principal !== trusted.principal) {
|
|
371
|
+
throw new FoundationStoreError('authorization_denied', 'The supplied principal does not match the trusted read context');
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function cloneContext(context) {
|
|
375
|
+
return {
|
|
376
|
+
principal: context.principal,
|
|
377
|
+
...(context.scope === undefined ? {} : { scope: cloneFoundationScope(context.scope) })
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function privateCurrentAcl(principal) {
|
|
381
|
+
return { ownerId: principal, visibility: 'private', readerIds: [], writerIds: [] };
|
|
382
|
+
}
|
|
383
|
+
function canRead(acl, principal) {
|
|
384
|
+
return acl.visibility === 'public' || acl.ownerId === principal || acl.readerIds.includes(principal) || acl.writerIds.includes(principal);
|
|
385
|
+
}
|
|
386
|
+
function scopeAllowed(scope, trusted) {
|
|
387
|
+
const ids = 'subjectIds' in scope ? scope.subjectIds : [scope.id];
|
|
388
|
+
return ids.some((id) => trusted.subjectIds.includes(id));
|
|
389
|
+
}
|
|
390
|
+
function scopeEqual(left, right) {
|
|
391
|
+
return left.type === right.type && left.id === right.id;
|
|
392
|
+
}
|
|
393
|
+
function cloneFoundationScope(scope) {
|
|
394
|
+
return {
|
|
395
|
+
subjectIds: [...scope.subjectIds],
|
|
396
|
+
validFrom: scope.validFrom,
|
|
397
|
+
...(scope.validUntil === undefined ? {} : { validUntil: scope.validUntil })
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function cloneJudgmentScope(scope) {
|
|
401
|
+
return { type: scope.type, id: scope.id };
|
|
402
|
+
}
|
|
403
|
+
function isValidFoundationScope(value) {
|
|
404
|
+
if (!isPlainRecord(value) || !Array.isArray(value.subjectIds) || value.subjectIds.length === 0 ||
|
|
405
|
+
!value.subjectIds.every((item) => typeof item === 'string' && item.trim().length > 0) ||
|
|
406
|
+
typeof value.validFrom !== 'string' || parseRfc3339(value.validFrom) === undefined)
|
|
407
|
+
return false;
|
|
408
|
+
return value.validUntil === undefined || (typeof value.validUntil === 'string' && parseRfc3339(value.validUntil) !== undefined && parseRfc3339(value.validUntil) > parseRfc3339(value.validFrom));
|
|
409
|
+
}
|
|
410
|
+
function parseJudgmentApplicability(payload) {
|
|
411
|
+
if (!isPlainRecord(payload) || !isPlainRecord(payload.judgmentApplicability)) {
|
|
412
|
+
throw corrupt('Philosophy payload requires judgmentApplicability');
|
|
413
|
+
}
|
|
414
|
+
const value = payload.judgmentApplicability;
|
|
415
|
+
if (!isPlainRecord(value.scope) || !PHILOSOPHY_SCOPE_TYPES.has(String(value.scope.type)) ||
|
|
416
|
+
typeof value.scope.id !== 'string' || value.scope.id.length === 0 ||
|
|
417
|
+
typeof value.validFrom !== 'string' || parseRfc3339(value.validFrom) === undefined ||
|
|
418
|
+
(value.validUntil !== undefined && (typeof value.validUntil !== 'string' || parseRfc3339(value.validUntil) === undefined || parseRfc3339(value.validUntil) < parseRfc3339(value.validFrom)))) {
|
|
419
|
+
throw corrupt('Philosophy judgmentApplicability is invalid');
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
422
|
+
scope: { type: value.scope.type, id: value.scope.id },
|
|
423
|
+
validFrom: value.validFrom,
|
|
424
|
+
...(value.validUntil === undefined ? {} : { validUntil: value.validUntil })
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
function requireObject(value, label) {
|
|
428
|
+
if (!isPlainRecord(value))
|
|
429
|
+
throw corrupt(`${label} must be an object`);
|
|
430
|
+
return value;
|
|
431
|
+
}
|
|
432
|
+
function requireString(value, label) {
|
|
433
|
+
if (typeof value !== 'string' || value.length === 0)
|
|
434
|
+
throw corrupt(`${label} must be a non-empty string`);
|
|
435
|
+
return value;
|
|
436
|
+
}
|
|
437
|
+
function requireJsonValue(value, label) {
|
|
438
|
+
if (value === undefined)
|
|
439
|
+
throw corrupt(`${label} is missing`);
|
|
440
|
+
try {
|
|
441
|
+
canonicalPortableJson(value);
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
throw corrupt(`${label} must be JSON`);
|
|
445
|
+
}
|
|
446
|
+
return value;
|
|
447
|
+
}
|
|
448
|
+
function parseRfc3339(value) {
|
|
449
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))?(Z|[+-]\d{2}:\d{2})$/u.exec(value);
|
|
450
|
+
if (!match)
|
|
451
|
+
return undefined;
|
|
452
|
+
const year = Number(match[1]);
|
|
453
|
+
const month = Number(match[2]);
|
|
454
|
+
const day = Number(match[3]);
|
|
455
|
+
const hour = Number(match[4]);
|
|
456
|
+
const minute = Number(match[5]);
|
|
457
|
+
const second = Number(match[6]);
|
|
458
|
+
const zone = match[8];
|
|
459
|
+
if (month < 1 || month > 12 || day < 1 || day > daysInMonth(year, month) || hour > 23 || minute > 59 || second > 59)
|
|
460
|
+
return undefined;
|
|
461
|
+
const fraction = match[7] ?? '';
|
|
462
|
+
const milliseconds = Number((fraction + '000').slice(0, 3));
|
|
463
|
+
let offsetMinutes = 0;
|
|
464
|
+
if (zone !== 'Z') {
|
|
465
|
+
const sign = zone[0] === '-' ? -1 : 1;
|
|
466
|
+
const offsetHour = Number(zone.slice(1, 3));
|
|
467
|
+
const offsetMinute = Number(zone.slice(4, 6));
|
|
468
|
+
if (offsetHour > 23 || offsetMinute > 59)
|
|
469
|
+
return undefined;
|
|
470
|
+
offsetMinutes = sign * (offsetHour * 60 + offsetMinute);
|
|
471
|
+
}
|
|
472
|
+
const timestamp = Date.UTC(year, month - 1, day, hour, minute, second, milliseconds) - offsetMinutes * 60_000;
|
|
473
|
+
return Number.isFinite(timestamp) ? timestamp : undefined;
|
|
474
|
+
}
|
|
475
|
+
function daysInMonth(year, month) {
|
|
476
|
+
if (month === 2)
|
|
477
|
+
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28;
|
|
478
|
+
return [4, 6, 9, 11].includes(month) ? 30 : 31;
|
|
479
|
+
}
|
|
480
|
+
function isPlainRecord(value) {
|
|
481
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
482
|
+
return false;
|
|
483
|
+
const prototype = Object.getPrototypeOf(value);
|
|
484
|
+
return prototype === Object.prototype || prototype === null;
|
|
485
|
+
}
|
|
486
|
+
function corrupt(message) {
|
|
487
|
+
return new FoundationStoreError('corrupt_catalog', message);
|
|
488
|
+
}
|
|
489
|
+
function scopeViolation(message) {
|
|
490
|
+
return new FoundationStoreError('scope_violation', message);
|
|
491
|
+
}
|
|
492
|
+
function errorMessage(error, fallback) {
|
|
493
|
+
return error instanceof Error && error.message ? error.message : fallback;
|
|
494
|
+
}
|
|
@@ -49,7 +49,12 @@ export interface GraphRetrievalResponse {
|
|
|
49
49
|
schemaVersion: 1 | 2;
|
|
50
50
|
status: 'ok' | 'migration_required';
|
|
51
51
|
migrationRequired: boolean;
|
|
52
|
-
|
|
52
|
+
/**
|
|
53
|
+
* `local_graph`: this Mac's Graph. `owner_private`: the owner's own snapshot
|
|
54
|
+
* kept by an organization (not the organization's facts).
|
|
55
|
+
* `organization_graph`: the organization's shared Graph.
|
|
56
|
+
*/
|
|
57
|
+
authority: 'local_graph' | 'owner_private' | 'organization_graph';
|
|
53
58
|
query: string;
|
|
54
59
|
asOf: string;
|
|
55
60
|
project?: {
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { FoundationAcl } from './ontology-foundation.js';
|
|
2
|
+
import type { JudgmentProblemReference, JudgmentProblemReferenceResolution, JudgmentProblemReferenceResolutionPhase, JudgmentProblemScope, JudgmentProblemSnapshotAccessContext } from './judgment-problem-snapshot.js';
|
|
3
|
+
import type { CanonicalEdge, CanonicalEntity } from './types.js';
|
|
4
|
+
/** Immutable Graph entity/edge reads are versioned independently of Graph file releases. */
|
|
5
|
+
export declare const GRAPH_REVISION_READ_CONTRACT_VERSION: "graph-revision-read.v1";
|
|
6
|
+
export type GraphRevisionKind = 'entity' | 'edge';
|
|
7
|
+
export type GraphRevisionDigest = `sha256:${string}`;
|
|
8
|
+
export type GraphRevisionScope = JudgmentProblemScope;
|
|
9
|
+
export type GraphRevisionPayload = CanonicalEntity | CanonicalEdge;
|
|
10
|
+
export interface GraphRevisionReference {
|
|
11
|
+
readonly kind: GraphRevisionKind;
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly revision: string;
|
|
14
|
+
readonly digest: GraphRevisionDigest;
|
|
15
|
+
/** The tenant/project boundary requested by the judgment. */
|
|
16
|
+
readonly scope: GraphRevisionScope;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The host supplies this port from its current authorization boundary. The
|
|
20
|
+
* OSS package does not create tenant history or infer membership from Graph
|
|
21
|
+
* edges. A caller-provided reference scope is therefore only a requested
|
|
22
|
+
* boundary; the reader must bind it to its trusted tenant/project context.
|
|
23
|
+
*/
|
|
24
|
+
export interface GraphRevisionReadRequest {
|
|
25
|
+
readonly reference: GraphRevisionReference;
|
|
26
|
+
readonly phase: JudgmentProblemReferenceResolutionPhase;
|
|
27
|
+
readonly context: {
|
|
28
|
+
readonly principal: string;
|
|
29
|
+
readonly scope: GraphRevisionScope;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export interface GraphRevisionRecord {
|
|
33
|
+
readonly kind: GraphRevisionKind;
|
|
34
|
+
readonly id: string;
|
|
35
|
+
readonly revision: string;
|
|
36
|
+
/** Digest of the immutable identity and payload, excluding current ACL. */
|
|
37
|
+
readonly digest: GraphRevisionDigest;
|
|
38
|
+
readonly payload: GraphRevisionPayload;
|
|
39
|
+
/** ACL and scope are read-time metadata and must be checked on every read. */
|
|
40
|
+
readonly currentAcl: FoundationAcl;
|
|
41
|
+
readonly currentScope: GraphRevisionScope;
|
|
42
|
+
}
|
|
43
|
+
export type GraphRevisionReadResult = {
|
|
44
|
+
readonly status: 'resolved';
|
|
45
|
+
readonly record: GraphRevisionRecord;
|
|
46
|
+
} | {
|
|
47
|
+
readonly status: 'missing' | 'unauthorized' | 'corrupt';
|
|
48
|
+
readonly message?: string;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* A repository or organization host implements this port. Returning a
|
|
52
|
+
* historical payload is allowed only when current ACL and scope are checked
|
|
53
|
+
* by the host; the adapter below repeats those checks before resolving a
|
|
54
|
+
* JudgmentProblem reference.
|
|
55
|
+
*/
|
|
56
|
+
export interface GraphRevisionReader {
|
|
57
|
+
read(input: GraphRevisionReadRequest): GraphRevisionReadResult | Promise<GraphRevisionReadResult>;
|
|
58
|
+
}
|
|
59
|
+
export interface JudgmentProblemGraphReferenceResolverInput {
|
|
60
|
+
readonly reference: JudgmentProblemReference;
|
|
61
|
+
readonly phase: JudgmentProblemReferenceResolutionPhase;
|
|
62
|
+
readonly context: JudgmentProblemSnapshotAccessContext;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Computes the stable digest used by a GraphRevisionReference. Current ACL
|
|
66
|
+
* is intentionally excluded so an ACL change does not create a false content
|
|
67
|
+
* revision; the adapter still checks the current ACL before every read.
|
|
68
|
+
*/
|
|
69
|
+
export declare function graphRevisionDigest(input: Pick<GraphRevisionRecord, 'kind' | 'id' | 'revision' | 'payload'>): GraphRevisionDigest;
|
|
70
|
+
/** Structural check for callers that need to validate a reference before I/O. */
|
|
71
|
+
export declare function isGraphRevisionReference(value: unknown): value is GraphRevisionReference;
|
|
72
|
+
/**
|
|
73
|
+
* Adapt Graph entity/edge reads to `JudgmentProblemReferenceProvider.resolveOther`.
|
|
74
|
+
* Unknown reference kinds remain unresolved so another provider can be composed
|
|
75
|
+
* explicitly by the caller; this resolver never treats an unhandled kind as
|
|
76
|
+
* resolved.
|
|
77
|
+
*/
|
|
78
|
+
export declare function createJudgmentProblemGraphReferenceResolver(options: {
|
|
79
|
+
readonly reader: GraphRevisionReader;
|
|
80
|
+
}): (input: JudgmentProblemGraphReferenceResolverInput) => Promise<JudgmentProblemReferenceResolution>;
|