@unson/brainbase-mcp 0.6.0 → 0.7.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/dist/canonical-task-contract.d.ts +9 -0
- package/dist/canonical-task-contract.js +39 -0
- package/dist/canonical-task-principal.d.ts +16 -0
- package/dist/canonical-task-principal.js +36 -0
- package/dist/canonical-task-service.d.ts +251 -0
- package/dist/canonical-task-service.js +809 -0
- package/dist/company-os-evaluation.d.ts +193 -0
- package/dist/company-os-evaluation.js +1150 -0
- package/dist/company-os-impact-review.d.ts +248 -0
- package/dist/company-os-impact-review.js +663 -0
- package/dist/company-os-learning-adoption.d.ts +288 -0
- package/dist/company-os-learning-adoption.js +944 -0
- package/dist/company-os-objectives.d.ts +52 -0
- package/dist/company-os-objectives.js +197 -0
- package/dist/company-os-receipt-adapter.d.ts +109 -0
- package/dist/company-os-receipt-adapter.js +636 -0
- package/dist/constraint-exception-store.d.ts +23 -0
- package/dist/constraint-exception-store.js +108 -0
- package/dist/constraint-resolution.d.ts +175 -0
- package/dist/constraint-resolution.js +922 -0
- package/dist/decision-adapter.d.ts +145 -0
- package/dist/decision-adapter.js +586 -0
- package/dist/durable-wait-http.d.ts +51 -0
- package/dist/durable-wait-http.js +497 -0
- package/dist/durable-waits.d.ts +234 -0
- package/dist/durable-waits.js +944 -0
- package/dist/execution-authority-http.d.ts +28 -0
- package/dist/execution-authority-http.js +359 -0
- package/dist/execution-authority.d.ts +201 -0
- package/dist/execution-authority.js +634 -0
- package/dist/foundation-catalog.d.ts +26 -0
- package/dist/foundation-catalog.js +206 -0
- package/dist/foundation-constraint-store.d.ts +50 -0
- package/dist/foundation-constraint-store.js +215 -0
- package/dist/foundation-http.d.ts +90 -0
- package/dist/foundation-http.js +439 -0
- package/dist/foundation-store.d.ts +132 -0
- package/dist/foundation-store.js +654 -0
- package/dist/judgment-dag-composition.d.ts +224 -0
- package/dist/judgment-dag-composition.js +731 -0
- package/dist/judgment-dag.d.ts +2 -0
- package/dist/judgment-dag.js +1 -0
- package/dist/judgment-problem-snapshot.d.ts +185 -0
- package/dist/judgment-problem-snapshot.js +777 -0
- package/dist/judgment-view-http.d.ts +22 -0
- package/dist/judgment-view-http.js +128 -0
- package/dist/judgment-view.d.ts +222 -0
- package/dist/judgment-view.js +627 -0
- package/dist/knowledge-adapter.d.ts +112 -0
- package/dist/knowledge-adapter.js +691 -0
- package/dist/ontology-foundation.d.ts +209 -0
- package/dist/ontology-foundation.js +1000 -0
- package/dist/ontology.d.ts +13 -0
- package/dist/ontology.js +14 -0
- package/dist/organization-connection.d.ts +164 -0
- package/dist/organization-connection.js +428 -0
- package/dist/personal-knowledge.d.ts +120 -0
- package/dist/personal-knowledge.js +750 -0
- package/dist/problem-candidates.d.ts +149 -0
- package/dist/problem-candidates.js +898 -0
- package/dist/problem-selection.d.ts +238 -0
- package/dist/problem-selection.js +995 -0
- package/dist/resource-reservation-http.d.ts +31 -0
- package/dist/resource-reservation-http.js +371 -0
- package/dist/resource-reservations.d.ts +221 -0
- package/dist/resource-reservations.js +577 -0
- package/dist/server.d.ts +97 -0
- package/dist/server.js +180 -1
- package/dist/ssot.d.ts +6 -1
- package/dist/ssot.js +63 -5
- package/dist/streamable-http.d.ts +42 -0
- package/dist/streamable-http.js +288 -0
- package/dist/types.d.ts +20 -0
- package/dist/world-model.d.ts +181 -0
- package/dist/world-model.js +873 -0
- package/package.json +139 -3
- package/ui/judgment-view.css +238 -0
- package/ui/judgment-view.js +508 -0
- package/ui/objective-editor.css +180 -0
- package/ui/objective-editor.js +1139 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { validateFoundationDefinition, validateFoundationRelation } from './ontology-foundation.js';
|
|
3
|
+
const revisionPattern = /^[1-9]\d*$/;
|
|
4
|
+
const digestPattern = /^sha256:[0-9a-f]{64}$/;
|
|
5
|
+
/**
|
|
6
|
+
* The key is an encoded tuple rather than `${type}:${id}` so IDs containing a
|
|
7
|
+
* colon cannot alias another logical resource.
|
|
8
|
+
*/
|
|
9
|
+
export function foundationKey(reference) {
|
|
10
|
+
return JSON.stringify([reference.type, reference.id]);
|
|
11
|
+
}
|
|
12
|
+
export function createEmptyFoundationCatalog() {
|
|
13
|
+
return {
|
|
14
|
+
version: 1,
|
|
15
|
+
records: [],
|
|
16
|
+
latest: {},
|
|
17
|
+
relations: []
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function cloneFoundationCatalog(catalog) {
|
|
21
|
+
return JSON.parse(JSON.stringify(catalog));
|
|
22
|
+
}
|
|
23
|
+
export function cloneFoundationDefinition(definition) {
|
|
24
|
+
return JSON.parse(JSON.stringify(definition));
|
|
25
|
+
}
|
|
26
|
+
export function foundationRef(record) {
|
|
27
|
+
return {
|
|
28
|
+
id: record.definition.id,
|
|
29
|
+
type: record.definition.type,
|
|
30
|
+
revision: record.definition.revision,
|
|
31
|
+
digest: record.digest
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Hashes the complete definition, with object keys sorted recursively. Array
|
|
36
|
+
* order remains significant because criteria, provenance, and authorized uses
|
|
37
|
+
* are ordered contracts in the shared ontology.
|
|
38
|
+
*/
|
|
39
|
+
export function digestFoundationDefinition(definition) {
|
|
40
|
+
const canonical = canonicalJson(definition);
|
|
41
|
+
return `sha256:${createHash('sha256').update(canonical, 'utf8').digest('hex')}`;
|
|
42
|
+
}
|
|
43
|
+
export function nextFoundationRevision(revision) {
|
|
44
|
+
assertRevision(revision, 'revision');
|
|
45
|
+
return (BigInt(revision) + 1n).toString();
|
|
46
|
+
}
|
|
47
|
+
export function assertFoundationRevision(value, label = 'revision') {
|
|
48
|
+
if (!isRecord(value)
|
|
49
|
+
|| typeof value.id !== 'string'
|
|
50
|
+
|| value.id.length === 0
|
|
51
|
+
|| !isFoundationType(value.type)
|
|
52
|
+
|| typeof value.revision !== 'string') {
|
|
53
|
+
throw new Error(`FOUNDATION-INVALID-REFERENCE: ${label} must contain id, type, and revision`);
|
|
54
|
+
}
|
|
55
|
+
assertRevision(value.revision, `${label}.revision`);
|
|
56
|
+
}
|
|
57
|
+
export function assertFoundationCatalog(value) {
|
|
58
|
+
if (value === undefined)
|
|
59
|
+
return;
|
|
60
|
+
if (!isRecord(value) || value.version !== 1 || !Array.isArray(value.records) || !Array.isArray(value.relations) || !isRecord(value.latest)) {
|
|
61
|
+
throw new Error('FOUNDATION-CATALOG-CORRUPT: version, records, latest, and relations are required');
|
|
62
|
+
}
|
|
63
|
+
const recordsByRevision = new Map();
|
|
64
|
+
const latestByLogical = new Map();
|
|
65
|
+
for (const [index, item] of value.records.entries()) {
|
|
66
|
+
if (!isRecord(item) || !isRecord(item.definition) || typeof item.digest !== 'string') {
|
|
67
|
+
throw new Error(`FOUNDATION-CATALOG-CORRUPT: records[${index}] must contain a definition and digest`);
|
|
68
|
+
}
|
|
69
|
+
const definition = item.definition;
|
|
70
|
+
assertDefinitionShape(definition, `records[${index}].definition`);
|
|
71
|
+
const revisionKey = `${foundationKey(definition)}@${definition.revision}`;
|
|
72
|
+
if (recordsByRevision.has(revisionKey)) {
|
|
73
|
+
throw new Error(`FOUNDATION-CATALOG-CORRUPT: duplicate revision ${revisionKey}`);
|
|
74
|
+
}
|
|
75
|
+
if (!digestPattern.test(item.digest) || item.digest !== digestFoundationDefinition(definition)) {
|
|
76
|
+
throw new Error(`FOUNDATION-CATALOG-CORRUPT: digest mismatch for ${revisionKey}`);
|
|
77
|
+
}
|
|
78
|
+
const record = item;
|
|
79
|
+
recordsByRevision.set(revisionKey, record);
|
|
80
|
+
const logicalKey = foundationKey(definition);
|
|
81
|
+
const previous = latestByLogical.get(logicalKey);
|
|
82
|
+
if (!previous || compareRevisions(definition.revision, previous.definition.revision) > 0) {
|
|
83
|
+
latestByLogical.set(logicalKey, record);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const latest = value.latest;
|
|
87
|
+
const latestKeys = Object.keys(latest);
|
|
88
|
+
if (latestKeys.length !== latestByLogical.size) {
|
|
89
|
+
throw new Error('FOUNDATION-CATALOG-CORRUPT: latest pointers do not match records');
|
|
90
|
+
}
|
|
91
|
+
for (const [logicalKey, record] of latestByLogical) {
|
|
92
|
+
const pointer = latest[logicalKey];
|
|
93
|
+
assertFoundationRevision(pointer, `latest[${logicalKey}]`);
|
|
94
|
+
if (foundationKey(pointer) !== logicalKey || pointer.revision !== record.definition.revision) {
|
|
95
|
+
throw new Error(`FOUNDATION-CATALOG-CORRUPT: latest pointer mismatch for ${logicalKey}`);
|
|
96
|
+
}
|
|
97
|
+
if (!recordsByRevision.has(`${logicalKey}@${pointer.revision}`)) {
|
|
98
|
+
throw new Error(`FOUNDATION-CATALOG-CORRUPT: latest pointer has no record for ${logicalKey}@${pointer.revision}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (const [index, relation] of value.relations.entries()) {
|
|
102
|
+
if (!isRecord(relation))
|
|
103
|
+
throw new Error(`FOUNDATION-CATALOG-CORRUPT: relations[${index}] is not an object`);
|
|
104
|
+
const result = safeValidateRelation(relation);
|
|
105
|
+
if (!result.valid) {
|
|
106
|
+
throw new Error(`FOUNDATION-CATALOG-CORRUPT: relations[${index}] ${result.issues.map((issue) => issue.message).join('; ')}`);
|
|
107
|
+
}
|
|
108
|
+
for (const endpoint of [relation.source, relation.target]) {
|
|
109
|
+
if (!isRecord(endpoint) || typeof endpoint.id !== 'string' || !isFoundationType(endpoint.type) || typeof endpoint.revision !== 'string')
|
|
110
|
+
continue;
|
|
111
|
+
assertRevision(endpoint.revision, `relations[${index}] endpoint.revision`);
|
|
112
|
+
const endpointKey = `${foundationKey({ id: endpoint.id, type: endpoint.type })}@${endpoint.revision}`;
|
|
113
|
+
if (!recordsByRevision.has(endpointKey)) {
|
|
114
|
+
throw new Error(`FOUNDATION-CATALOG-CORRUPT: relation endpoint ${endpointKey} has no record`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export function foundationCatalogOrEmpty(value) {
|
|
120
|
+
if (value === undefined)
|
|
121
|
+
return createEmptyFoundationCatalog();
|
|
122
|
+
assertFoundationCatalog(value);
|
|
123
|
+
return cloneFoundationCatalog(value);
|
|
124
|
+
}
|
|
125
|
+
export function findFoundationRecord(catalog, reference) {
|
|
126
|
+
assertFoundationRevision(reference);
|
|
127
|
+
return catalog.records.find((record) => (record.definition.id === reference.id
|
|
128
|
+
&& record.definition.type === reference.type
|
|
129
|
+
&& record.definition.revision === reference.revision));
|
|
130
|
+
}
|
|
131
|
+
export function findLatestFoundationRecord(catalog, type, id) {
|
|
132
|
+
const pointer = catalog.latest[foundationKey({ type, id })];
|
|
133
|
+
return pointer ? findFoundationRecord(catalog, pointer) : undefined;
|
|
134
|
+
}
|
|
135
|
+
function assertDefinitionShape(definition, label) {
|
|
136
|
+
if (!isRecord(definition)
|
|
137
|
+
|| typeof definition.id !== 'string'
|
|
138
|
+
|| definition.id.length === 0
|
|
139
|
+
|| !isFoundationType(definition.type)
|
|
140
|
+
|| typeof definition.revision !== 'string') {
|
|
141
|
+
throw new Error(`FOUNDATION-CATALOG-CORRUPT: ${label} has an invalid identity`);
|
|
142
|
+
}
|
|
143
|
+
assertRevision(definition.revision, `${label}.revision`);
|
|
144
|
+
if (!isRecord(definition.acl)
|
|
145
|
+
|| typeof definition.acl.ownerId !== 'string'
|
|
146
|
+
|| definition.acl.ownerId.length === 0
|
|
147
|
+
|| !Array.isArray(definition.acl.readerIds)
|
|
148
|
+
|| !Array.isArray(definition.acl.writerIds)
|
|
149
|
+
|| !['private', 'project', 'organization', 'public'].includes(definition.acl.visibility)) {
|
|
150
|
+
throw new Error(`FOUNDATION-CATALOG-CORRUPT: ${label}.acl is invalid`);
|
|
151
|
+
}
|
|
152
|
+
const result = safeValidateDefinition(definition);
|
|
153
|
+
if (!result.valid) {
|
|
154
|
+
throw new Error(`FOUNDATION-CATALOG-CORRUPT: ${label} ${result.issues.map((issue) => issue.message).join('; ')}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function safeValidateDefinition(definition) {
|
|
158
|
+
try {
|
|
159
|
+
return validateFoundationDefinition(definition, { use: 'draft' });
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
throw new Error(`invalid definition: ${formatError(error)}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function safeValidateRelation(relation) {
|
|
166
|
+
try {
|
|
167
|
+
return validateFoundationRelation(relation);
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
throw new Error(`invalid relation: ${formatError(error)}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function assertRevision(value, label) {
|
|
174
|
+
if (!revisionPattern.test(value)) {
|
|
175
|
+
throw new Error(`FOUNDATION-INVALID-REVISION: ${label} must be a positive canonical decimal`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function compareRevisions(left, right) {
|
|
179
|
+
const leftValue = BigInt(left);
|
|
180
|
+
const rightValue = BigInt(right);
|
|
181
|
+
return leftValue < rightValue ? -1 : leftValue > rightValue ? 1 : 0;
|
|
182
|
+
}
|
|
183
|
+
function isFoundationType(value) {
|
|
184
|
+
return value === 'objective' || value === 'variable' || value === 'model' || value === 'constraint';
|
|
185
|
+
}
|
|
186
|
+
function isRecord(value) {
|
|
187
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
188
|
+
}
|
|
189
|
+
function canonicalJson(value) {
|
|
190
|
+
const canonical = canonicalize(value);
|
|
191
|
+
const serialized = JSON.stringify(canonical);
|
|
192
|
+
if (serialized === undefined)
|
|
193
|
+
throw new Error('FOUNDATION-DIGEST-INVALID: definition is not JSON serializable');
|
|
194
|
+
return serialized;
|
|
195
|
+
}
|
|
196
|
+
function canonicalize(value) {
|
|
197
|
+
if (Array.isArray(value))
|
|
198
|
+
return value.map((item) => canonicalize(item));
|
|
199
|
+
if (isRecord(value)) {
|
|
200
|
+
return Object.fromEntries(Object.keys(value).sort().flatMap((key) => (value[key] === undefined ? [] : [[key, canonicalize(value[key])]])));
|
|
201
|
+
}
|
|
202
|
+
return value;
|
|
203
|
+
}
|
|
204
|
+
function formatError(error) {
|
|
205
|
+
return error instanceof Error ? error.message : String(error);
|
|
206
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { FoundationRevisionStore } from './foundation-store.js';
|
|
2
|
+
import { type ConstraintAuthorizationContext, type ConstraintExceptionQuery, type ConstraintExceptionRecord, type ConstraintProjection, type ConstraintStore, type ConstraintStoreQuery } from './constraint-resolution.js';
|
|
3
|
+
/**
|
|
4
|
+
* Exception records are deliberately a host supplied boundary. The shared
|
|
5
|
+
* foundation catalog currently stores the ontology-level exception reference
|
|
6
|
+
* (Decision revision and scope), while Story04 also needs approval metadata,
|
|
7
|
+
* expiry, and rationale. A host must therefore provide the canonical
|
|
8
|
+
* evidence/execution adapter for that record until the shared contract grows
|
|
9
|
+
* those fields; this module never creates a private sidecar file.
|
|
10
|
+
*/
|
|
11
|
+
export interface ConstraintExceptionStore {
|
|
12
|
+
append(exception: ConstraintExceptionRecord, context: ConstraintAuthorizationContext): Promise<ConstraintExceptionRecord>;
|
|
13
|
+
list(query: ConstraintExceptionQuery, context: ConstraintAuthorizationContext): Promise<readonly ConstraintExceptionRecord[]>;
|
|
14
|
+
}
|
|
15
|
+
export interface FoundationConstraintStoreOptions {
|
|
16
|
+
foundationStore: FoundationRevisionStore;
|
|
17
|
+
exceptionStore: ConstraintExceptionStore;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Adapts the Objective Story's GraphFoundationRevisionStore to the resolver's
|
|
21
|
+
* ConstraintStore port. Definitions are persisted in the existing PersonalOs
|
|
22
|
+
* aggregate through FoundationRevisionStore; no second Graph or file is
|
|
23
|
+
* created here.
|
|
24
|
+
*/
|
|
25
|
+
export declare class FoundationConstraintStore<TConstraint extends ConstraintProjection = ConstraintProjection> implements ConstraintStore<TConstraint> {
|
|
26
|
+
private readonly foundationStore;
|
|
27
|
+
private readonly exceptionStore;
|
|
28
|
+
constructor(options: FoundationConstraintStoreOptions);
|
|
29
|
+
append(input: {
|
|
30
|
+
record: TConstraint;
|
|
31
|
+
expectedPreviousRevision?: string | null;
|
|
32
|
+
}, context?: ConstraintAuthorizationContext): Promise<TConstraint>;
|
|
33
|
+
get(ref: {
|
|
34
|
+
id: string;
|
|
35
|
+
revision: string;
|
|
36
|
+
}, context?: ConstraintAuthorizationContext): Promise<TConstraint | null>;
|
|
37
|
+
getLatest(id: string, context?: ConstraintAuthorizationContext): Promise<TConstraint | null>;
|
|
38
|
+
list(query: ConstraintStoreQuery, context?: ConstraintAuthorizationContext): Promise<readonly TConstraint[]>;
|
|
39
|
+
appendException(exception: ConstraintExceptionRecord, context?: ConstraintAuthorizationContext): Promise<ConstraintExceptionRecord>;
|
|
40
|
+
listExceptions(query: ConstraintExceptionQuery, context?: ConstraintAuthorizationContext): Promise<readonly ConstraintExceptionRecord[]>;
|
|
41
|
+
/**
|
|
42
|
+
* The injected exception boundary is intentionally persistence-only and may
|
|
43
|
+
* not enforce Foundation ACLs itself. Resolve the referenced Constraint at
|
|
44
|
+
* this adapter boundary before allowing exception evidence to cross it.
|
|
45
|
+
* Current ACL controls access even when an exception points at an older
|
|
46
|
+
* revision; the exact revision is still read to reject dangling references.
|
|
47
|
+
*/
|
|
48
|
+
private authorizeExceptionTarget;
|
|
49
|
+
}
|
|
50
|
+
export declare function createFoundationConstraintStore(options: FoundationConstraintStoreOptions): ConstraintStore;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { FoundationStoreError } from './foundation-store.js';
|
|
2
|
+
import { ConstraintError, validateConstraintExceptionRecord, } from './constraint-resolution.js';
|
|
3
|
+
/**
|
|
4
|
+
* Adapts the Objective Story's GraphFoundationRevisionStore to the resolver's
|
|
5
|
+
* ConstraintStore port. Definitions are persisted in the existing PersonalOs
|
|
6
|
+
* aggregate through FoundationRevisionStore; no second Graph or file is
|
|
7
|
+
* created here.
|
|
8
|
+
*/
|
|
9
|
+
export class FoundationConstraintStore {
|
|
10
|
+
foundationStore;
|
|
11
|
+
exceptionStore;
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.foundationStore = options.foundationStore;
|
|
14
|
+
this.exceptionStore = options.exceptionStore;
|
|
15
|
+
}
|
|
16
|
+
async append(input, context) {
|
|
17
|
+
const storeContext = toFoundationContext(context);
|
|
18
|
+
let reference;
|
|
19
|
+
try {
|
|
20
|
+
if (input.expectedPreviousRevision === null) {
|
|
21
|
+
reference = await this.foundationStore.create(input.record, storeContext);
|
|
22
|
+
}
|
|
23
|
+
else if (input.expectedPreviousRevision !== undefined) {
|
|
24
|
+
reference = await this.foundationStore.update({
|
|
25
|
+
reference: {
|
|
26
|
+
id: input.record.id,
|
|
27
|
+
type: 'constraint',
|
|
28
|
+
revision: input.expectedPreviousRevision,
|
|
29
|
+
},
|
|
30
|
+
next: input.record,
|
|
31
|
+
expectedRevision: input.expectedPreviousRevision,
|
|
32
|
+
}, storeContext);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
throw new ConstraintError('revision_conflict', 'Foundation constraint append requires an explicit previous revision or null for create', 409);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
throw normalizeFoundationError(error, input.record.id);
|
|
40
|
+
}
|
|
41
|
+
if (reference.id !== input.record.id || reference.type !== 'constraint' || reference.revision !== input.record.revision) {
|
|
42
|
+
throw new ConstraintError('constraint_store_corrupt', 'Foundation store returned a different constraint revision than requested', 500, {
|
|
43
|
+
id: input.record.id,
|
|
44
|
+
requestedRevision: input.record.revision,
|
|
45
|
+
storedRevision: reference.revision,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const readback = await this.get({ id: reference.id, revision: reference.revision }, context);
|
|
49
|
+
if (!readback) {
|
|
50
|
+
throw new ConstraintError('constraint_store_corrupt', 'Foundation constraint readback was empty', 500, {
|
|
51
|
+
id: reference.id,
|
|
52
|
+
revision: reference.revision,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return readback;
|
|
56
|
+
}
|
|
57
|
+
async get(ref, context) {
|
|
58
|
+
const storeContext = toFoundationContext(context);
|
|
59
|
+
try {
|
|
60
|
+
const record = await this.foundationStore.read({ id: ref.id, type: 'constraint', revision: ref.revision }, storeContext);
|
|
61
|
+
return record?.definition.type === 'constraint' ? record.definition : null;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
throw normalizeFoundationError(error, ref.id);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async getLatest(id, context) {
|
|
68
|
+
const storeContext = toFoundationContext(context);
|
|
69
|
+
try {
|
|
70
|
+
const record = await this.foundationStore.readLatest('constraint', id, storeContext);
|
|
71
|
+
return record?.definition.type === 'constraint' ? record.definition : null;
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
throw normalizeFoundationError(error, id);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async list(query, context) {
|
|
78
|
+
const storeContext = toFoundationContext(context);
|
|
79
|
+
try {
|
|
80
|
+
if (query.revision !== undefined && query.id !== undefined) {
|
|
81
|
+
const exact = await this.get({ id: query.id, revision: query.revision }, context);
|
|
82
|
+
return exact && (query.ownerId === undefined || exact.acl.ownerId === query.ownerId) ? [exact] : [];
|
|
83
|
+
}
|
|
84
|
+
const records = await this.foundationStore.list('constraint', storeContext);
|
|
85
|
+
return records
|
|
86
|
+
.map((record) => record.definition)
|
|
87
|
+
.filter((definition) => definition.type === 'constraint')
|
|
88
|
+
.filter((definition) => query.id === undefined || definition.id === query.id)
|
|
89
|
+
.filter((definition) => query.ownerId === undefined || definition.acl.ownerId === query.ownerId)
|
|
90
|
+
.map((definition) => definition);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
throw normalizeFoundationError(error);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async appendException(exception, context) {
|
|
97
|
+
const resolvedContext = context ?? missingContext();
|
|
98
|
+
try {
|
|
99
|
+
validateConstraintExceptionRecord(exception);
|
|
100
|
+
await this.authorizeExceptionTarget(exception.constraintRef.id, exception.constraintRef.revision, resolvedContext, 'write');
|
|
101
|
+
return await this.exceptionStore.append(exception, resolvedContext);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
throw normalizeFoundationError(error, exceptionConstraintId(exception));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async listExceptions(query, context) {
|
|
108
|
+
const resolvedContext = context ?? missingContext();
|
|
109
|
+
try {
|
|
110
|
+
if (query.constraintId !== undefined) {
|
|
111
|
+
await this.authorizeExceptionTarget(query.constraintId, query.constraintRevision, resolvedContext, 'read');
|
|
112
|
+
}
|
|
113
|
+
const exceptions = await this.exceptionStore.list(query, resolvedContext);
|
|
114
|
+
for (const exception of exceptions) {
|
|
115
|
+
validateConstraintExceptionRecord(exception);
|
|
116
|
+
await this.authorizeExceptionTarget(exception.constraintRef.id, exception.constraintRef.revision, resolvedContext, 'read');
|
|
117
|
+
}
|
|
118
|
+
return exceptions;
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
throw normalizeFoundationError(error, query.constraintId);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The injected exception boundary is intentionally persistence-only and may
|
|
126
|
+
* not enforce Foundation ACLs itself. Resolve the referenced Constraint at
|
|
127
|
+
* this adapter boundary before allowing exception evidence to cross it.
|
|
128
|
+
* Current ACL controls access even when an exception points at an older
|
|
129
|
+
* revision; the exact revision is still read to reject dangling references.
|
|
130
|
+
*/
|
|
131
|
+
async authorizeExceptionTarget(constraintId, constraintRevision, context, access) {
|
|
132
|
+
const storeContext = toFoundationContext(context);
|
|
133
|
+
const current = await this.foundationStore.readLatest('constraint', constraintId, storeContext);
|
|
134
|
+
if (!current || current.definition.type !== 'constraint') {
|
|
135
|
+
throw new ConstraintError('constraint_not_found', 'exception target constraint was not found', 404, { id: constraintId });
|
|
136
|
+
}
|
|
137
|
+
if (constraintRevision !== undefined) {
|
|
138
|
+
const referenced = await this.foundationStore.read({
|
|
139
|
+
id: constraintId,
|
|
140
|
+
type: 'constraint',
|
|
141
|
+
revision: constraintRevision,
|
|
142
|
+
}, storeContext);
|
|
143
|
+
if (!referenced || referenced.definition.type !== 'constraint') {
|
|
144
|
+
throw new ConstraintError('constraint_not_found', 'exception target constraint revision was not found', 404, { id: constraintId, revision: constraintRevision });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const target = current.definition;
|
|
148
|
+
if (target.acl.ownerId !== context.ownerId) {
|
|
149
|
+
throw new ConstraintError('scope_violation', 'exception target constraint is outside the authorization context', 403, { id: target.id, ownerId: target.acl.ownerId });
|
|
150
|
+
}
|
|
151
|
+
const canRead = target.acl.visibility === 'public'
|
|
152
|
+
|| target.acl.ownerId === context.actorId
|
|
153
|
+
|| target.acl.readerIds.includes(context.actorId)
|
|
154
|
+
|| target.acl.writerIds.includes(context.actorId);
|
|
155
|
+
if (!canRead) {
|
|
156
|
+
throw new ConstraintError('authorization_denied', 'actor cannot read the exception target constraint', 403, { id: target.id });
|
|
157
|
+
}
|
|
158
|
+
if (access === 'write') {
|
|
159
|
+
const canWrite = target.acl.ownerId === context.actorId || target.acl.writerIds.includes(context.actorId);
|
|
160
|
+
if (!canWrite) {
|
|
161
|
+
throw new ConstraintError('authorization_denied', 'actor cannot write exceptions for the target constraint', 403, { id: target.id });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function exceptionConstraintId(exception) {
|
|
167
|
+
if (!exception || typeof exception !== 'object')
|
|
168
|
+
return undefined;
|
|
169
|
+
const reference = exception.constraintRef;
|
|
170
|
+
if (!reference || typeof reference !== 'object')
|
|
171
|
+
return undefined;
|
|
172
|
+
const id = reference.id;
|
|
173
|
+
return typeof id === 'string' ? id : undefined;
|
|
174
|
+
}
|
|
175
|
+
export function createFoundationConstraintStore(options) {
|
|
176
|
+
return new FoundationConstraintStore(options);
|
|
177
|
+
}
|
|
178
|
+
function toFoundationContext(context) {
|
|
179
|
+
// `ownerId` identifies the target scope. It is caller supplied request
|
|
180
|
+
// data and must never be promoted to an authenticated principal. The
|
|
181
|
+
// principal is the actor identity established by the trusted auth boundary.
|
|
182
|
+
if (!context?.actorId)
|
|
183
|
+
return missingContext();
|
|
184
|
+
return { principal: context.actorId };
|
|
185
|
+
}
|
|
186
|
+
function missingContext() {
|
|
187
|
+
throw new ConstraintError('authorization_denied', 'Foundation constraint persistence requires an authenticated actor context', 403);
|
|
188
|
+
}
|
|
189
|
+
function normalizeFoundationError(error, id) {
|
|
190
|
+
if (error instanceof ConstraintError)
|
|
191
|
+
return error;
|
|
192
|
+
if (error instanceof FoundationStoreError) {
|
|
193
|
+
switch (error.code) {
|
|
194
|
+
case 'invalid_input':
|
|
195
|
+
return new ConstraintError('invalid_constraint', error.message, 400, { id });
|
|
196
|
+
case 'not_found':
|
|
197
|
+
return new ConstraintError('constraint_not_found', error.message, 404, { id });
|
|
198
|
+
case 'revision_conflict':
|
|
199
|
+
return new ConstraintError('revision_conflict', error.message, 409, {
|
|
200
|
+
id,
|
|
201
|
+
currentRevision: error.currentRevision,
|
|
202
|
+
});
|
|
203
|
+
case 'authorization_denied':
|
|
204
|
+
return new ConstraintError('authorization_denied', error.message, 403, { id });
|
|
205
|
+
case 'scope_violation':
|
|
206
|
+
return new ConstraintError('scope_violation', error.message, 403, { id });
|
|
207
|
+
case 'corrupt_catalog':
|
|
208
|
+
case 'readback_mismatch':
|
|
209
|
+
return new ConstraintError('constraint_store_corrupt', error.message, 500, { id });
|
|
210
|
+
case 'unsupported_graph':
|
|
211
|
+
return new ConstraintError('constraint_store_unavailable', error.message, 503, { id });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return new ConstraintError('constraint_store_unavailable', 'Foundation constraint store failed', 503, { id });
|
|
215
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { CompanyOsObjectives } from './company-os-objectives.js';
|
|
2
|
+
import { type FoundationStoreContext } from './foundation-store.js';
|
|
3
|
+
import type { FoundationRef } from './foundation-catalog.js';
|
|
4
|
+
import type { FoundationRevision, ObjectiveDefinition } from './ontology-foundation.js';
|
|
5
|
+
import type { FoundationCatalogRecord } from './types.js';
|
|
6
|
+
/**
|
|
7
|
+
* HTTP boundary shared by the OSS Objective UI and organization adapters.
|
|
8
|
+
*
|
|
9
|
+
* This module intentionally exposes a Fetch-style route handler instead of
|
|
10
|
+
* opening a listener. A local host can mount it beside Reservation,
|
|
11
|
+
* Decision, or another Company OS route on one server. Authentication,
|
|
12
|
+
* tenant scope, and CSRF are supplied by that host; none of them are read
|
|
13
|
+
* from a JSON request body.
|
|
14
|
+
*/
|
|
15
|
+
export declare const FOUNDATION_HTTP_CONTRACT_VERSION: "brainbase.foundation-http.v1";
|
|
16
|
+
export declare const FOUNDATION_HTTP_PREFIX: "/api/foundation";
|
|
17
|
+
export declare const FOUNDATION_OBJECTIVE_ROUTE_PREFIX: "/api/foundation/objectives";
|
|
18
|
+
export declare const FOUNDATION_OBJECTIVE_ROUTES: readonly string[];
|
|
19
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
20
|
+
export interface FoundationHttpRouteContext {
|
|
21
|
+
/** Resolved by the host's trusted auth/tenant boundary. */
|
|
22
|
+
readonly context: FoundationStoreContext;
|
|
23
|
+
readonly request: Request;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A route is deliberately a small composable unit. `null` means that the
|
|
27
|
+
* route does not own this request, allowing a reservation/decision route to
|
|
28
|
+
* be mounted beside it without a second server or a route-specific BFF.
|
|
29
|
+
*/
|
|
30
|
+
export interface FoundationHttpRoute {
|
|
31
|
+
readonly name: string;
|
|
32
|
+
readonly methods: readonly string[];
|
|
33
|
+
readonly paths: readonly string[];
|
|
34
|
+
matches(request: Request): boolean;
|
|
35
|
+
handle(request: Request, input: FoundationHttpRouteContext): Promise<Response>;
|
|
36
|
+
}
|
|
37
|
+
export interface FoundationHttpCsrfVerifier {
|
|
38
|
+
verify(request: Request, context: FoundationStoreContext): MaybePromise<void | boolean>;
|
|
39
|
+
}
|
|
40
|
+
export interface FoundationHttpRouterOptions {
|
|
41
|
+
/** Return null to fail closed before any route handler is called. */
|
|
42
|
+
resolveContext(request: Request): MaybePromise<FoundationStoreContext | null>;
|
|
43
|
+
/** Mutation routes return 501 when no verifier is configured. */
|
|
44
|
+
csrf?: FoundationHttpCsrfVerifier;
|
|
45
|
+
routes: readonly FoundationHttpRoute[];
|
|
46
|
+
bodyLimitBytes?: number;
|
|
47
|
+
}
|
|
48
|
+
export interface FoundationHttpRouter {
|
|
49
|
+
readonly contractVersion: typeof FOUNDATION_HTTP_CONTRACT_VERSION;
|
|
50
|
+
readonly routes: readonly FoundationHttpRoute[];
|
|
51
|
+
handle(request: Request): Promise<Response>;
|
|
52
|
+
}
|
|
53
|
+
export interface ObjectiveDefinitionBuilderInput {
|
|
54
|
+
operation: 'create' | 'update';
|
|
55
|
+
/** Sanitized domain fields. Authorization fields have already been rejected. */
|
|
56
|
+
body: Readonly<Record<string, unknown>>;
|
|
57
|
+
context: FoundationStoreContext;
|
|
58
|
+
current?: ObjectiveDefinition;
|
|
59
|
+
expectedRevision?: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Hosts must construct ACL/scope/provenance/storage from trusted context. A
|
|
63
|
+
* request body is only a proposal for domain fields and cannot authorize a
|
|
64
|
+
* tenant, principal, visibility, or permitted use.
|
|
65
|
+
*/
|
|
66
|
+
export type ObjectiveDefinitionBuilder = (input: ObjectiveDefinitionBuilderInput) => MaybePromise<ObjectiveDefinition>;
|
|
67
|
+
export interface ObjectiveFoundationHttpRouteOptions {
|
|
68
|
+
objectives: Pick<CompanyOsObjectives, 'createObjective' | 'readObjective' | 'updateObjective' | 'checkObjectiveReadiness'> & {
|
|
69
|
+
list?(type: 'objective', context: FoundationStoreContext): Promise<FoundationCatalogRecord[]>;
|
|
70
|
+
};
|
|
71
|
+
/** Required for writes; the host supplies trusted ACL/scope fields. */
|
|
72
|
+
buildDefinition?: ObjectiveDefinitionBuilder;
|
|
73
|
+
/** Typed relation reads remain owned by the Story provider. */
|
|
74
|
+
listStoryObjectiveLinks?: (storyId: string, context: FoundationStoreContext) => MaybePromise<unknown>;
|
|
75
|
+
/** Constraint relation reads/writes remain owned by the canonical adapter. */
|
|
76
|
+
listObjectiveConstraintRefs?: (reference: FoundationRevision, context: FoundationStoreContext) => MaybePromise<unknown>;
|
|
77
|
+
replaceObjectiveConstraintRefs?: (reference: FoundationRef, refs: readonly FoundationRevision[], context: FoundationStoreContext) => MaybePromise<unknown>;
|
|
78
|
+
bodyLimitBytes?: number;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Compose OSS Company OS routes under one local server boundary. The
|
|
82
|
+
* resolver is called only for a matching route and the resulting trusted
|
|
83
|
+
* context is passed to that route. Unknown routes remain an ordinary 404 so
|
|
84
|
+
* the host can mount this handler beside other app routes.
|
|
85
|
+
*/
|
|
86
|
+
export declare function createFoundationHttpRouter(options: FoundationHttpRouterOptions): FoundationHttpRouter;
|
|
87
|
+
/** Convenience constructor for a local/organization host with Objective routes only. */
|
|
88
|
+
export declare function createObjectiveFoundationHttpHandler(options: ObjectiveFoundationHttpRouteOptions & Omit<FoundationHttpRouterOptions, 'routes'>): FoundationHttpRouter;
|
|
89
|
+
export declare function createObjectiveFoundationRoute(options: ObjectiveFoundationHttpRouteOptions): FoundationHttpRoute;
|
|
90
|
+
export {};
|