@feltdb/core 0.11.3 → 0.11.5
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/README.md +8 -6
- package/dist/architecture-invariant.d.ts +407 -0
- package/dist/architecture-invariant.d.ts.map +1 -0
- package/dist/architecture-invariant.js +708 -0
- package/dist/collection.d.ts +11 -1
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +37 -6
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +1 -1
- package/dist/db.d.ts +9 -5
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +24 -6
- package/dist/embedded-query.d.ts +43 -0
- package/dist/embedded-query.d.ts.map +1 -0
- package/dist/embedded-query.js +327 -0
- package/dist/feltdb-vision-invariants.d.ts +46 -0
- package/dist/feltdb-vision-invariants.d.ts.map +1 -0
- package/dist/feltdb-vision-invariants.js +213 -0
- package/dist/feltdb.d.ts +12 -1
- package/dist/feltdb.d.ts.map +1 -1
- package/dist/file-db.d.ts +8 -0
- package/dist/file-db.d.ts.map +1 -1
- package/dist/file-db.js +24 -0
- package/dist/http-db.d.ts +14 -0
- package/dist/http-db.d.ts.map +1 -1
- package/dist/http-db.js +1 -0
- package/dist/index-core.d.ts +3 -0
- package/dist/index-core.d.ts.map +1 -1
- package/dist/index-core.js +2 -0
- package/dist/index-types.d.ts +5 -1
- package/dist/index-types.d.ts.map +1 -1
- package/dist/indexeddb-db.d.ts +18 -0
- package/dist/indexeddb-db.d.ts.map +1 -1
- package/dist/indexeddb-db.js +28 -0
- package/dist/memory-db.d.ts +8 -0
- package/dist/memory-db.d.ts.map +1 -1
- package/dist/memory-db.js +19 -0
- package/dist/studio-app/assets/index-Cg6EWmLb.js +30 -0
- package/dist/studio-app/index.html +1 -1
- package/package.json +1 -1
- package/dist/studio-app/assets/index-CSLtbbtP.js +0 -30
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Architecture invariants.
|
|
3
|
+
*
|
|
4
|
+
* application / human
|
|
5
|
+
* │ declares
|
|
6
|
+
* ▼
|
|
7
|
+
* FeltDB ──── invariant (identity, declaration, version, lifecycle, provenance)
|
|
8
|
+
* │ │
|
|
9
|
+
* │ snapshot │ referenced by exact version
|
|
10
|
+
* ▼ ▼
|
|
11
|
+
* evaluator (jev, or any other) ────────▶ evaluation (PASS | DRIFT | UNKNOWN)
|
|
12
|
+
* │
|
|
13
|
+
* ▼
|
|
14
|
+
* FeltDB
|
|
15
|
+
*
|
|
16
|
+
* An architecture invariant is a durable declaration that some condition is
|
|
17
|
+
* expected to remain true of an application: across code, contracts,
|
|
18
|
+
* configuration, endpoints, secrets, authority boundaries, or any other
|
|
19
|
+
* architectural surface.
|
|
20
|
+
*
|
|
21
|
+
* FeltDB owns the declaration. It stores invariant identity, the versioned
|
|
22
|
+
* declaration, lifecycle, provenance, and the durable history of what was
|
|
23
|
+
* evaluated. It does not decide whether an invariant is currently satisfied,
|
|
24
|
+
* and it contains no evaluator.
|
|
25
|
+
*
|
|
26
|
+
* An evaluator owns the judgment. It reads a declaration snapshot, collects
|
|
27
|
+
* evidence, and records an advisory evaluation against the exact invariant
|
|
28
|
+
* version it read. `evaluations.record()` can only write evaluations: the
|
|
29
|
+
* store it is given rejects a write to any other collection, so an evaluator
|
|
30
|
+
* cannot redefine, reword, or retire the invariant it was asked about, and a
|
|
31
|
+
* DRIFT result never changes the declaration it drifted from.
|
|
32
|
+
*
|
|
33
|
+
* An invariant references the authorities it constrains; it never restates
|
|
34
|
+
* them. `.flow` remains authoritative for its contract and configuration
|
|
35
|
+
* model, AppPort for AppPort contracts, AuthBoundry for authorization
|
|
36
|
+
* decisions, and FeltDB for durable application state. A reference names the
|
|
37
|
+
* authority and states the relationship that must remain true about it.
|
|
38
|
+
*/
|
|
39
|
+
import { canonicalApplicationBytes } from './canonical-application.js';
|
|
40
|
+
import { FeltDBErrorCode, FeltDBServiceError } from './error-codes.js';
|
|
41
|
+
/** Durable collection holding one head record per declared invariant. */
|
|
42
|
+
export const FELTDB_ARCHITECTURE_INVARIANT_COLLECTION = '_feltdb.ArchitectureInvariant';
|
|
43
|
+
/** Durable collection holding the immutable declaration of every version. */
|
|
44
|
+
export const FELTDB_ARCHITECTURE_INVARIANT_VERSION_COLLECTION = '_feltdb.ArchitectureInvariantVersion';
|
|
45
|
+
/** Durable collection holding immutable advisory evaluations. */
|
|
46
|
+
export const FELTDB_ARCHITECTURE_EVALUATION_COLLECTION = '_feltdb.ArchitectureInvariantEvaluation';
|
|
47
|
+
export const ARCHITECTURE_INVARIANT_CONTRACT_VERSION = 1;
|
|
48
|
+
/** Largest page a bounded invariant or evaluation query may request. */
|
|
49
|
+
export const ARCHITECTURE_INVARIANT_MAX_LIMIT = 1_000;
|
|
50
|
+
/** Page size used when a caller does not request one. */
|
|
51
|
+
export const ARCHITECTURE_INVARIANT_DEFAULT_LIMIT = 100;
|
|
52
|
+
export const ARCHITECTURE_INVARIANT_SEVERITIES = ['critical', 'high', 'medium', 'low', 'info'];
|
|
53
|
+
export const ARCHITECTURE_INVARIANT_STATUSES = ['active', 'inactive'];
|
|
54
|
+
/**
|
|
55
|
+
* How an invariant can be established.
|
|
56
|
+
*
|
|
57
|
+
* Not every invariant is mechanically checkable. Some are decided by a test or
|
|
58
|
+
* a conformance check, some need evidence interpreted by an advisory
|
|
59
|
+
* evaluator, and some can only be escalated to a person. All three are
|
|
60
|
+
* first-class: `human-review` is a complete answer to "how is this verified",
|
|
61
|
+
* not an admission that the model failed to describe it.
|
|
62
|
+
*/
|
|
63
|
+
export const ARCHITECTURE_VERIFICATION_METHODS = [
|
|
64
|
+
'automated-test', 'contract-conformance', 'source-analysis',
|
|
65
|
+
'runtime-observation', 'benchmark', 'jev-advisory', 'human-review',
|
|
66
|
+
];
|
|
67
|
+
/**
|
|
68
|
+
* Advisory results an evaluator may record.
|
|
69
|
+
*
|
|
70
|
+
* `UNKNOWN` exists so that missing evidence stays missing evidence. An
|
|
71
|
+
* evaluator that cannot establish a condition records `UNKNOWN`; it does not
|
|
72
|
+
* record `DRIFT`, and FeltDB never promotes one to the other.
|
|
73
|
+
*/
|
|
74
|
+
export const ARCHITECTURE_EVALUATION_RESULTS = ['PASS', 'DRIFT', 'UNKNOWN'];
|
|
75
|
+
export const ARCHITECTURE_INVARIANT_CAPABILITIES = {
|
|
76
|
+
declare: 'architecture:invariant.declare',
|
|
77
|
+
read: 'architecture:invariant.read',
|
|
78
|
+
evaluate: 'architecture:evaluation.record',
|
|
79
|
+
readEvaluation: 'architecture:evaluation.read',
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Declaration fields an evaluation may never carry.
|
|
83
|
+
*
|
|
84
|
+
* An evaluator states what it observed. The moment its output could also carry
|
|
85
|
+
* a `severity` or a `description`, recording an evaluation would be a way to
|
|
86
|
+
* redefine the invariant, and the authority boundary would be advisory in name
|
|
87
|
+
* only. These are rejected rather than ignored, so a caller that believes it
|
|
88
|
+
* is changing the declaration finds out.
|
|
89
|
+
*/
|
|
90
|
+
const EVALUATION_FORBIDDEN_FIELDS = [
|
|
91
|
+
'name', 'description', 'category', 'severity', 'scope', 'status',
|
|
92
|
+
'verification', 'references', 'version', 'declaration_digest', 'provenance',
|
|
93
|
+
];
|
|
94
|
+
const EVIDENCE_REFERENCE = /^[a-z][a-z0-9+.-]*:.+$/;
|
|
95
|
+
function invalid(message) {
|
|
96
|
+
return new FeltDBServiceError(message, FeltDBErrorCode.VALIDATION_ERROR);
|
|
97
|
+
}
|
|
98
|
+
function requireText(value, label, max = 4_000) {
|
|
99
|
+
if (typeof value !== 'string' || !value.trim())
|
|
100
|
+
throw invalid(`${label} must be a non-empty string`);
|
|
101
|
+
const text = value.trim();
|
|
102
|
+
if (text.length > max)
|
|
103
|
+
throw invalid(`${label} must be at most ${max} characters`);
|
|
104
|
+
return text;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Identities become durable record keys, so a separator inside one would let
|
|
108
|
+
* two different declarations resolve to the same row.
|
|
109
|
+
*/
|
|
110
|
+
function requireIdentity(value, label) {
|
|
111
|
+
const text = requireText(value, label, 200);
|
|
112
|
+
if (/[:/\s]/.test(text))
|
|
113
|
+
throw invalid(`${label} must not contain ":", "/", or whitespace`);
|
|
114
|
+
return text;
|
|
115
|
+
}
|
|
116
|
+
function requireMember(value, allowed, label) {
|
|
117
|
+
if (typeof value !== 'string' || !allowed.includes(value)) {
|
|
118
|
+
throw invalid(`${label} must be one of ${allowed.join(', ')}`);
|
|
119
|
+
}
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
122
|
+
function normalizeEvidence(value, label) {
|
|
123
|
+
if (value === undefined)
|
|
124
|
+
return [];
|
|
125
|
+
if (!Array.isArray(value))
|
|
126
|
+
throw invalid(`${label} must be an array of evidence references`);
|
|
127
|
+
const seen = new Set();
|
|
128
|
+
for (const entry of value) {
|
|
129
|
+
const reference = requireText(entry, `${label} entry`, 1_000);
|
|
130
|
+
if (!EVIDENCE_REFERENCE.test(reference)) {
|
|
131
|
+
throw invalid(`${label} entry "${reference}" must be a "scheme:reference" evidence reference`);
|
|
132
|
+
}
|
|
133
|
+
seen.add(reference);
|
|
134
|
+
}
|
|
135
|
+
return [...seen];
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* An invariant must say how it can be established, even when the answer is
|
|
139
|
+
* that only a person can decide it. An invariant nobody can verify by any
|
|
140
|
+
* means is a sentence, not an invariant.
|
|
141
|
+
*/
|
|
142
|
+
function normalizeVerification(value, label) {
|
|
143
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
144
|
+
throw invalid(`${label} must declare at least one way this invariant can be established`);
|
|
145
|
+
}
|
|
146
|
+
return value.map((entry, position) => {
|
|
147
|
+
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
|
|
148
|
+
throw invalid(`${label}[${position}] must be an object`);
|
|
149
|
+
}
|
|
150
|
+
const input = entry;
|
|
151
|
+
const reference = input.reference === undefined ? undefined : requireText(input.reference, `${label}[${position}].reference`, 1_000);
|
|
152
|
+
if (reference !== undefined && !EVIDENCE_REFERENCE.test(reference)) {
|
|
153
|
+
throw invalid(`${label}[${position}].reference must be a "scheme:reference" evidence reference`);
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
method: requireMember(input.method, ARCHITECTURE_VERIFICATION_METHODS, `${label}[${position}].method`),
|
|
157
|
+
...(reference === undefined ? {} : { reference }),
|
|
158
|
+
...(input.evaluator === undefined ? {} : { evaluator: requireText(input.evaluator, `${label}[${position}].evaluator`, 200) }),
|
|
159
|
+
...(input.predicate === undefined ? {} : { predicate: requireText(input.predicate, `${label}[${position}].predicate`) }),
|
|
160
|
+
...(input.notes === undefined ? {} : { notes: requireText(input.notes, `${label}[${position}].notes`) }),
|
|
161
|
+
...(input.parameters === undefined ? {} : (() => {
|
|
162
|
+
if (typeof input.parameters !== 'object' || input.parameters === null || Array.isArray(input.parameters)) {
|
|
163
|
+
throw invalid(`${label}[${position}].parameters must be an object`);
|
|
164
|
+
}
|
|
165
|
+
return { parameters: input.parameters };
|
|
166
|
+
})()),
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
function normalizeReferences(value, label) {
|
|
171
|
+
if (value === undefined)
|
|
172
|
+
return [];
|
|
173
|
+
if (!Array.isArray(value))
|
|
174
|
+
throw invalid(`${label} must be an array`);
|
|
175
|
+
return value.map((entry, position) => {
|
|
176
|
+
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
|
|
177
|
+
throw invalid(`${label}[${position}] must be an object`);
|
|
178
|
+
}
|
|
179
|
+
const reference = entry;
|
|
180
|
+
return {
|
|
181
|
+
authority: requireText(reference.authority, `${label}[${position}].authority`, 200),
|
|
182
|
+
reference: requireText(reference.reference, `${label}[${position}].reference`, 1_000),
|
|
183
|
+
relationship: requireText(reference.relationship, `${label}[${position}].relationship`),
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
function normalizeDeclaration(input) {
|
|
188
|
+
return {
|
|
189
|
+
name: requireText(input.name, 'name', 300),
|
|
190
|
+
description: requireText(input.description, 'description'),
|
|
191
|
+
category: requireIdentity(input.category, 'category'),
|
|
192
|
+
severity: requireMember(input.severity, ARCHITECTURE_INVARIANT_SEVERITIES, 'severity'),
|
|
193
|
+
scope: requireIdentity(input.scope, 'scope'),
|
|
194
|
+
verification: normalizeVerification(input.verification, 'verification'),
|
|
195
|
+
references: normalizeReferences(input.references, 'references'),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function declarationOf(value) {
|
|
199
|
+
return {
|
|
200
|
+
name: value.name,
|
|
201
|
+
description: value.description,
|
|
202
|
+
category: value.category,
|
|
203
|
+
severity: value.severity,
|
|
204
|
+
scope: value.scope,
|
|
205
|
+
verification: value.verification,
|
|
206
|
+
references: value.references,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Digest over the canonical application encoding FeltDB already uses, so an
|
|
211
|
+
* evaluation can prove which words it was evaluating.
|
|
212
|
+
*/
|
|
213
|
+
async function declarationDigest(declaration) {
|
|
214
|
+
const bytes = canonicalApplicationBytes(declaration);
|
|
215
|
+
const digest = await crypto.subtle.digest('SHA-256', bytes.slice().buffer);
|
|
216
|
+
return `sha256:${Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join('')}`;
|
|
217
|
+
}
|
|
218
|
+
function sameDeclaration(left, right) {
|
|
219
|
+
const encode = (value) => new TextDecoder().decode(canonicalApplicationBytes(value));
|
|
220
|
+
return encode(declarationOf(left)) === encode(declarationOf(right));
|
|
221
|
+
}
|
|
222
|
+
function boundedLimit(value) {
|
|
223
|
+
if (value === undefined)
|
|
224
|
+
return ARCHITECTURE_INVARIANT_DEFAULT_LIMIT;
|
|
225
|
+
if (!Number.isInteger(value) || value < 1 || value > ARCHITECTURE_INVARIANT_MAX_LIMIT) {
|
|
226
|
+
throw new FeltDBServiceError(`limit must be an integer between 1 and ${ARCHITECTURE_INVARIANT_MAX_LIMIT}`, FeltDBErrorCode.QUERY_LIMIT_EXCEEDED);
|
|
227
|
+
}
|
|
228
|
+
return value;
|
|
229
|
+
}
|
|
230
|
+
function timestamp(value) {
|
|
231
|
+
return value.toISOString();
|
|
232
|
+
}
|
|
233
|
+
function resolveEvaluatedAt(value, fallback) {
|
|
234
|
+
if (value === undefined)
|
|
235
|
+
return timestamp(fallback);
|
|
236
|
+
const resolved = value instanceof Date ? value : new Date(value);
|
|
237
|
+
if (Number.isNaN(resolved.getTime()))
|
|
238
|
+
throw invalid('evaluatedAt must be a valid date');
|
|
239
|
+
return timestamp(resolved);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* A store that can only write one collection.
|
|
243
|
+
*
|
|
244
|
+
* `evaluations.record()` runs against this, which is what makes "an evaluation
|
|
245
|
+
* cannot mutate an invariant" a property of the substrate rather than a rule
|
|
246
|
+
* the recording path is trusted to follow.
|
|
247
|
+
*/
|
|
248
|
+
export function restrictWritesTo(store, collection) {
|
|
249
|
+
const guard = (target) => {
|
|
250
|
+
if (target !== collection) {
|
|
251
|
+
throw new FeltDBServiceError(`An evaluator may only write ${collection}; ${target} is owned by the declaring authority`, FeltDBErrorCode.FORBIDDEN);
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
return {
|
|
255
|
+
read: (target, recordId) => store.read(target, recordId),
|
|
256
|
+
query: request => store.query(request),
|
|
257
|
+
create: async (target, recordId, value) => { guard(target); return store.create(target, recordId, value); },
|
|
258
|
+
replace: async (target, recordId, value) => { guard(target); return store.replace(target, recordId, value); },
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/** Adapt a FeltDB instance to the durable substrate this service writes through. */
|
|
262
|
+
export function createArchitectureInvariantStore(db) {
|
|
263
|
+
return {
|
|
264
|
+
async read(collection, recordId) {
|
|
265
|
+
return (await db.collection(collection).get(recordId)) ?? undefined;
|
|
266
|
+
},
|
|
267
|
+
async create(collection, recordId, value) {
|
|
268
|
+
await db.collection(collection).insert(value, recordId);
|
|
269
|
+
},
|
|
270
|
+
async replace(collection, recordId, value) {
|
|
271
|
+
await db.collection(collection).update(recordId, value);
|
|
272
|
+
},
|
|
273
|
+
query: request => db.query(request),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function toInvariant(row) {
|
|
277
|
+
const { invariant_id, id: _recordId, __version: _rowVersion, ...rest } = row;
|
|
278
|
+
return { ...rest, id: invariant_id };
|
|
279
|
+
}
|
|
280
|
+
function toEvaluation(row) {
|
|
281
|
+
const { evaluation_id, id: _recordId, __version: _rowVersion, evaluator_id: _evaluatorId, ...rest } = row;
|
|
282
|
+
return { ...rest, id: evaluation_id };
|
|
283
|
+
}
|
|
284
|
+
function toVersion(row) {
|
|
285
|
+
const { id: _recordId, __version: _rowVersion, ...rest } = row;
|
|
286
|
+
return rest;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* The durable architecture invariant substrate.
|
|
290
|
+
*
|
|
291
|
+
* `invariants` declares what must be true. `evaluations` retains what an
|
|
292
|
+
* advisory evaluator observed about it. Neither side can do the other's job.
|
|
293
|
+
*/
|
|
294
|
+
export class FeltDBArchitectureInvariantService {
|
|
295
|
+
scope;
|
|
296
|
+
actor;
|
|
297
|
+
source;
|
|
298
|
+
authorization;
|
|
299
|
+
clock;
|
|
300
|
+
newEvaluationId;
|
|
301
|
+
evaluationStore;
|
|
302
|
+
store;
|
|
303
|
+
constructor(store, options) {
|
|
304
|
+
this.store = store;
|
|
305
|
+
this.scope = {
|
|
306
|
+
tenantId: requireIdentity(options.scope?.tenantId, 'scope.tenantId'),
|
|
307
|
+
applicationId: requireIdentity(options.scope?.applicationId, 'scope.applicationId'),
|
|
308
|
+
};
|
|
309
|
+
if (!options.actor?.id || !options.actor?.kind)
|
|
310
|
+
throw invalid('actor requires a subject kind and id');
|
|
311
|
+
this.actor = { kind: options.actor.kind, id: requireText(options.actor.id, 'actor.id', 200) };
|
|
312
|
+
this.source = requireText(options.source, 'source', 200);
|
|
313
|
+
this.authorization = options.authorization;
|
|
314
|
+
this.clock = options.now ?? (() => new Date());
|
|
315
|
+
this.newEvaluationId = options.newEvaluationId ?? (() => `ae_${crypto.randomUUID().replace(/-/g, '')}`);
|
|
316
|
+
this.evaluationStore = restrictWritesTo(store, FELTDB_ARCHITECTURE_EVALUATION_COLLECTION);
|
|
317
|
+
}
|
|
318
|
+
/** Durable record key. Scoping by tenant and application is part of identity. */
|
|
319
|
+
scopedKey(recordId) {
|
|
320
|
+
return `${this.scope.tenantId}/${this.scope.applicationId}/${recordId}`;
|
|
321
|
+
}
|
|
322
|
+
versionKey(invariantId, version) {
|
|
323
|
+
return `${this.scopedKey(invariantId)}@${version}`;
|
|
324
|
+
}
|
|
325
|
+
resourceUri(collection, recordId) {
|
|
326
|
+
return `flow://${this.scope.applicationId}/${collection}/${recordId}`;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Ask FeltDB's existing authorization boundary, and fail closed.
|
|
330
|
+
*
|
|
331
|
+
* With no gate configured the caller has not asked for narrowing, and this
|
|
332
|
+
* substrate must not invent an authorization answer FeltDB did not give.
|
|
333
|
+
*/
|
|
334
|
+
async authorize(capability, collection, recordId) {
|
|
335
|
+
if (!this.authorization)
|
|
336
|
+
return undefined;
|
|
337
|
+
const decision = await this.authorization.gate.check({
|
|
338
|
+
subject: this.authorization.subject,
|
|
339
|
+
tenant_id: this.scope.tenantId,
|
|
340
|
+
application_id: this.scope.applicationId,
|
|
341
|
+
resource: { uri: this.resourceUri(collection, recordId) },
|
|
342
|
+
capability,
|
|
343
|
+
context: { timestamp: this.clock().getTime() },
|
|
344
|
+
});
|
|
345
|
+
if (decision?.allowed !== true) {
|
|
346
|
+
throw new FeltDBServiceError(`${capability} was not allowed for ${this.resourceUri(collection, recordId)}${decision?.reason ? `: ${decision.reason}` : ''}`, FeltDBErrorCode.FORBIDDEN);
|
|
347
|
+
}
|
|
348
|
+
return decision.decision_id;
|
|
349
|
+
}
|
|
350
|
+
provenance(decisionId, at) {
|
|
351
|
+
return {
|
|
352
|
+
subject: this.authorization?.subject ?? this.actor,
|
|
353
|
+
tenant_id: this.scope.tenantId,
|
|
354
|
+
application_id: this.scope.applicationId,
|
|
355
|
+
recorded_at: timestamp(at),
|
|
356
|
+
source: this.source,
|
|
357
|
+
...(decisionId ? { authorization_decision_id: decisionId } : {}),
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
async readInvariantRow(invariantId) {
|
|
361
|
+
const row = await this.store.read(FELTDB_ARCHITECTURE_INVARIANT_COLLECTION, this.scopedKey(invariantId));
|
|
362
|
+
if (!row || row.tenant_id !== this.scope.tenantId || row.application_id !== this.scope.applicationId) {
|
|
363
|
+
throw new FeltDBServiceError(`Architecture invariant ${invariantId} was not found`, FeltDBErrorCode.NOT_FOUND);
|
|
364
|
+
}
|
|
365
|
+
return row;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Every collection read is a bounded, tenant- and application-scoped query
|
|
369
|
+
* with an explicit order, so a page is reproducible and a cursor resumes
|
|
370
|
+
* where the previous page ended instead of rescanning the collection.
|
|
371
|
+
*/
|
|
372
|
+
scopedQuery(collection, where, orderBy, limit, cursor) {
|
|
373
|
+
return {
|
|
374
|
+
collection,
|
|
375
|
+
where: [
|
|
376
|
+
{ field: 'tenant_id', eq: this.scope.tenantId },
|
|
377
|
+
{ field: 'application_id', eq: this.scope.applicationId },
|
|
378
|
+
...(where ?? []),
|
|
379
|
+
],
|
|
380
|
+
orderBy,
|
|
381
|
+
limit,
|
|
382
|
+
...(cursor ? { cursor } : {}),
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
async writeVersion(invariantId, version, declaration, digest, at, provenance, supersedes, reason) {
|
|
386
|
+
const record = {
|
|
387
|
+
id: this.versionKey(invariantId, version),
|
|
388
|
+
invariant_id: invariantId,
|
|
389
|
+
tenant_id: this.scope.tenantId,
|
|
390
|
+
application_id: this.scope.applicationId,
|
|
391
|
+
contract_version: ARCHITECTURE_INVARIANT_CONTRACT_VERSION,
|
|
392
|
+
version,
|
|
393
|
+
...declaration,
|
|
394
|
+
declaration_digest: digest,
|
|
395
|
+
...(supersedes === undefined ? {} : { supersedes }),
|
|
396
|
+
...(reason ? { change_reason: reason } : {}),
|
|
397
|
+
declared_at: timestamp(at),
|
|
398
|
+
declared_by: provenance.subject,
|
|
399
|
+
provenance,
|
|
400
|
+
};
|
|
401
|
+
await this.store.create(FELTDB_ARCHITECTURE_INVARIANT_VERSION_COLLECTION, record.id, record);
|
|
402
|
+
return toVersion(record);
|
|
403
|
+
}
|
|
404
|
+
invariants = {
|
|
405
|
+
/** Declare an invariant. The declaration becomes version 1. */
|
|
406
|
+
create: async (input) => {
|
|
407
|
+
const invariantId = requireIdentity(input?.id, 'id');
|
|
408
|
+
const declaration = normalizeDeclaration(input);
|
|
409
|
+
const decisionId = await this.authorize(ARCHITECTURE_INVARIANT_CAPABILITIES.declare, FELTDB_ARCHITECTURE_INVARIANT_COLLECTION, invariantId);
|
|
410
|
+
const key = this.scopedKey(invariantId);
|
|
411
|
+
if (await this.store.read(FELTDB_ARCHITECTURE_INVARIANT_COLLECTION, key)) {
|
|
412
|
+
throw new FeltDBServiceError(`Architecture invariant ${invariantId} is already declared for this application`, FeltDBErrorCode.CONFLICT);
|
|
413
|
+
}
|
|
414
|
+
const at = this.clock();
|
|
415
|
+
const provenance = this.provenance(decisionId, at);
|
|
416
|
+
const digest = await declarationDigest(declaration);
|
|
417
|
+
const row = {
|
|
418
|
+
id: key,
|
|
419
|
+
invariant_id: invariantId,
|
|
420
|
+
tenant_id: this.scope.tenantId,
|
|
421
|
+
application_id: this.scope.applicationId,
|
|
422
|
+
contract_version: ARCHITECTURE_INVARIANT_CONTRACT_VERSION,
|
|
423
|
+
...declaration,
|
|
424
|
+
status: 'active',
|
|
425
|
+
version: 1,
|
|
426
|
+
declaration_digest: digest,
|
|
427
|
+
created_at: timestamp(at),
|
|
428
|
+
created_by: provenance.subject,
|
|
429
|
+
updated_at: timestamp(at),
|
|
430
|
+
updated_by: provenance.subject,
|
|
431
|
+
provenance,
|
|
432
|
+
};
|
|
433
|
+
await this.writeVersion(invariantId, 1, declaration, digest, at, provenance);
|
|
434
|
+
await this.store.create(FELTDB_ARCHITECTURE_INVARIANT_COLLECTION, key, row);
|
|
435
|
+
return toInvariant(row);
|
|
436
|
+
},
|
|
437
|
+
/** Read the current declaration. */
|
|
438
|
+
get: async (invariantId) => {
|
|
439
|
+
const id = requireIdentity(invariantId, 'id');
|
|
440
|
+
await this.authorize(ARCHITECTURE_INVARIANT_CAPABILITIES.read, FELTDB_ARCHITECTURE_INVARIANT_COLLECTION, id);
|
|
441
|
+
return toInvariant(await this.readInvariantRow(id));
|
|
442
|
+
},
|
|
443
|
+
/** Read one immutable declaration version. */
|
|
444
|
+
version: async (invariantId, version) => {
|
|
445
|
+
const id = requireIdentity(invariantId, 'id');
|
|
446
|
+
if (!Number.isInteger(version) || version < 1)
|
|
447
|
+
throw invalid('version must be a positive integer');
|
|
448
|
+
await this.authorize(ARCHITECTURE_INVARIANT_CAPABILITIES.read, FELTDB_ARCHITECTURE_INVARIANT_COLLECTION, id);
|
|
449
|
+
const row = await this.store.read(FELTDB_ARCHITECTURE_INVARIANT_VERSION_COLLECTION, this.versionKey(id, version));
|
|
450
|
+
if (!row || row.tenant_id !== this.scope.tenantId || row.application_id !== this.scope.applicationId) {
|
|
451
|
+
throw new FeltDBServiceError(`Architecture invariant ${id} has no version ${version}`, FeltDBErrorCode.NOT_FOUND);
|
|
452
|
+
}
|
|
453
|
+
return toVersion(row);
|
|
454
|
+
},
|
|
455
|
+
/** Page the declaration history of one invariant, oldest version first. */
|
|
456
|
+
versions: async (invariantId, input = {}) => {
|
|
457
|
+
const id = requireIdentity(invariantId, 'id');
|
|
458
|
+
await this.authorize(ARCHITECTURE_INVARIANT_CAPABILITIES.read, FELTDB_ARCHITECTURE_INVARIANT_COLLECTION, id);
|
|
459
|
+
const page = await this.store.query(this.scopedQuery(FELTDB_ARCHITECTURE_INVARIANT_VERSION_COLLECTION, [{ field: 'invariant_id', eq: id }], [{ field: 'version', direction: 'asc' }], boundedLimit(input.limit), input.cursor));
|
|
460
|
+
return {
|
|
461
|
+
versions: page.records.map(toVersion),
|
|
462
|
+
...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),
|
|
463
|
+
exhausted: page.exhausted,
|
|
464
|
+
};
|
|
465
|
+
},
|
|
466
|
+
/** Page declared invariants within this tenant and application. */
|
|
467
|
+
list: async (input = {}) => {
|
|
468
|
+
await this.authorize(ARCHITECTURE_INVARIANT_CAPABILITIES.read, FELTDB_ARCHITECTURE_INVARIANT_COLLECTION, '*');
|
|
469
|
+
const where = [];
|
|
470
|
+
if (input.status !== undefined)
|
|
471
|
+
where.push({ field: 'status', eq: requireMember(input.status, ARCHITECTURE_INVARIANT_STATUSES, 'status') });
|
|
472
|
+
if (input.category !== undefined)
|
|
473
|
+
where.push({ field: 'category', eq: requireIdentity(input.category, 'category') });
|
|
474
|
+
if (input.severity !== undefined)
|
|
475
|
+
where.push({ field: 'severity', eq: requireMember(input.severity, ARCHITECTURE_INVARIANT_SEVERITIES, 'severity') });
|
|
476
|
+
if (input.scope !== undefined)
|
|
477
|
+
where.push({ field: 'scope', eq: requireIdentity(input.scope, 'scope') });
|
|
478
|
+
const page = await this.store.query(this.scopedQuery(FELTDB_ARCHITECTURE_INVARIANT_COLLECTION, where, [{ field: 'invariant_id', direction: 'asc' }], boundedLimit(input.limit), input.cursor));
|
|
479
|
+
return {
|
|
480
|
+
invariants: page.records.map(toInvariant),
|
|
481
|
+
...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),
|
|
482
|
+
exhausted: page.exhausted,
|
|
483
|
+
};
|
|
484
|
+
},
|
|
485
|
+
/**
|
|
486
|
+
* Change what an invariant means.
|
|
487
|
+
*
|
|
488
|
+
* A changed declaration becomes a new version; the previous version keeps
|
|
489
|
+
* its exact words, so evaluations recorded against it stay readable. An
|
|
490
|
+
* update that changes nothing is a no-op and creates no version.
|
|
491
|
+
*/
|
|
492
|
+
update: async (invariantId, input = {}) => {
|
|
493
|
+
const id = requireIdentity(invariantId, 'id');
|
|
494
|
+
const decisionId = await this.authorize(ARCHITECTURE_INVARIANT_CAPABILITIES.declare, FELTDB_ARCHITECTURE_INVARIANT_COLLECTION, id);
|
|
495
|
+
const row = await this.readInvariantRow(id);
|
|
496
|
+
if (input.expectedVersion !== undefined && input.expectedVersion !== row.version) {
|
|
497
|
+
throw new FeltDBServiceError(`Architecture invariant ${id} is at version ${row.version}, not ${input.expectedVersion}`, FeltDBErrorCode.VERSION_CONFLICT);
|
|
498
|
+
}
|
|
499
|
+
if (row.status !== 'active') {
|
|
500
|
+
throw new FeltDBServiceError(`Architecture invariant ${id} is ${row.status} and cannot be redeclared`, FeltDBErrorCode.CONFLICT);
|
|
501
|
+
}
|
|
502
|
+
const declaration = normalizeDeclaration({
|
|
503
|
+
...declarationOf(row),
|
|
504
|
+
...(input.name === undefined ? {} : { name: input.name }),
|
|
505
|
+
...(input.description === undefined ? {} : { description: input.description }),
|
|
506
|
+
...(input.category === undefined ? {} : { category: input.category }),
|
|
507
|
+
...(input.severity === undefined ? {} : { severity: input.severity }),
|
|
508
|
+
...(input.scope === undefined ? {} : { scope: input.scope }),
|
|
509
|
+
...(input.verification === undefined ? {} : { verification: input.verification }),
|
|
510
|
+
...(input.references === undefined ? {} : { references: input.references }),
|
|
511
|
+
});
|
|
512
|
+
if (sameDeclaration(row, declaration))
|
|
513
|
+
return toInvariant(row);
|
|
514
|
+
const at = this.clock();
|
|
515
|
+
const provenance = this.provenance(decisionId, at);
|
|
516
|
+
const version = row.version + 1;
|
|
517
|
+
const digest = await declarationDigest(declaration);
|
|
518
|
+
await this.writeVersion(id, version, declaration, digest, at, provenance, row.version, input.reason);
|
|
519
|
+
const updated = {
|
|
520
|
+
...row,
|
|
521
|
+
...declaration,
|
|
522
|
+
version,
|
|
523
|
+
declaration_digest: digest,
|
|
524
|
+
updated_at: timestamp(at),
|
|
525
|
+
updated_by: provenance.subject,
|
|
526
|
+
provenance,
|
|
527
|
+
};
|
|
528
|
+
await this.store.replace(FELTDB_ARCHITECTURE_INVARIANT_COLLECTION, row.id, updated);
|
|
529
|
+
return toInvariant(updated);
|
|
530
|
+
},
|
|
531
|
+
/**
|
|
532
|
+
* Retire an invariant without rewriting it.
|
|
533
|
+
*
|
|
534
|
+
* Deactivation is lifecycle, not meaning: the declaration keeps its
|
|
535
|
+
* version, and evaluations recorded against it remain readable.
|
|
536
|
+
*/
|
|
537
|
+
deactivate: async (invariantId, input = {}) => {
|
|
538
|
+
const id = requireIdentity(invariantId, 'id');
|
|
539
|
+
const decisionId = await this.authorize(ARCHITECTURE_INVARIANT_CAPABILITIES.declare, FELTDB_ARCHITECTURE_INVARIANT_COLLECTION, id);
|
|
540
|
+
const row = await this.readInvariantRow(id);
|
|
541
|
+
if (input.expectedVersion !== undefined && input.expectedVersion !== row.version) {
|
|
542
|
+
throw new FeltDBServiceError(`Architecture invariant ${id} is at version ${row.version}, not ${input.expectedVersion}`, FeltDBErrorCode.VERSION_CONFLICT);
|
|
543
|
+
}
|
|
544
|
+
if (row.status === 'inactive')
|
|
545
|
+
return toInvariant(row);
|
|
546
|
+
const at = this.clock();
|
|
547
|
+
const provenance = this.provenance(decisionId, at);
|
|
548
|
+
const updated = {
|
|
549
|
+
...row,
|
|
550
|
+
status: 'inactive',
|
|
551
|
+
updated_at: timestamp(at),
|
|
552
|
+
updated_by: provenance.subject,
|
|
553
|
+
deactivated_at: timestamp(at),
|
|
554
|
+
deactivated_by: provenance.subject,
|
|
555
|
+
...(input.reason ? { deactivation_reason: requireText(input.reason, 'reason') } : {}),
|
|
556
|
+
provenance,
|
|
557
|
+
};
|
|
558
|
+
await this.store.replace(FELTDB_ARCHITECTURE_INVARIANT_COLLECTION, row.id, updated);
|
|
559
|
+
return toInvariant(updated);
|
|
560
|
+
},
|
|
561
|
+
};
|
|
562
|
+
evaluations = {
|
|
563
|
+
/**
|
|
564
|
+
* Retain one advisory evaluation.
|
|
565
|
+
*
|
|
566
|
+
* The evaluation binds to the exact invariant version it names, and to
|
|
567
|
+
* that version's declaration digest. A conclusive result must cite
|
|
568
|
+
* evidence; `UNKNOWN` need not, because the absence of evidence is what it
|
|
569
|
+
* reports.
|
|
570
|
+
*/
|
|
571
|
+
record: async (input) => {
|
|
572
|
+
if (!input || typeof input !== 'object')
|
|
573
|
+
throw invalid('an evaluation input object is required');
|
|
574
|
+
const supplied = Object.keys(input);
|
|
575
|
+
const forbidden = EVALUATION_FORBIDDEN_FIELDS.filter(field => supplied.includes(field));
|
|
576
|
+
if (forbidden.length) {
|
|
577
|
+
throw new FeltDBServiceError(`An evaluation may not carry invariant declaration fields (${forbidden.join(', ')}); `
|
|
578
|
+
+ 'the declaring authority owns the invariant definition', FeltDBErrorCode.FORBIDDEN);
|
|
579
|
+
}
|
|
580
|
+
const invariantId = requireIdentity(input.invariantId, 'invariantId');
|
|
581
|
+
if (!Number.isInteger(input.invariantVersion) || input.invariantVersion < 1) {
|
|
582
|
+
throw invalid('invariantVersion must be a positive integer');
|
|
583
|
+
}
|
|
584
|
+
const result = requireMember(input.result, ARCHITECTURE_EVALUATION_RESULTS, 'result');
|
|
585
|
+
const explanation = requireText(input.explanation, 'explanation');
|
|
586
|
+
const evidence = normalizeEvidence(input.evidence, 'evidence');
|
|
587
|
+
if (result !== 'UNKNOWN' && evidence.length === 0) {
|
|
588
|
+
throw invalid(`a ${result} evaluation must reference the evidence it was derived from`);
|
|
589
|
+
}
|
|
590
|
+
if (input.confidence !== undefined
|
|
591
|
+
&& (typeof input.confidence !== 'number' || !Number.isFinite(input.confidence)
|
|
592
|
+
|| input.confidence < 0 || input.confidence > 1)) {
|
|
593
|
+
throw invalid('confidence must be a number between 0 and 1');
|
|
594
|
+
}
|
|
595
|
+
const findings = (input.findings ?? []).map((finding, position) => {
|
|
596
|
+
const supporting = normalizeEvidence(finding?.evidence, `findings[${position}].evidence`);
|
|
597
|
+
return {
|
|
598
|
+
summary: requireText(finding?.summary, `findings[${position}].summary`),
|
|
599
|
+
...(finding?.severity === undefined ? {} : {
|
|
600
|
+
severity: requireMember(finding.severity, ARCHITECTURE_INVARIANT_SEVERITIES, `findings[${position}].severity`),
|
|
601
|
+
}),
|
|
602
|
+
...(supporting.length ? { evidence: supporting } : {}),
|
|
603
|
+
};
|
|
604
|
+
});
|
|
605
|
+
const evaluationId = input.evaluationId === undefined
|
|
606
|
+
? this.newEvaluationId()
|
|
607
|
+
: requireIdentity(input.evaluationId, 'evaluationId');
|
|
608
|
+
const decisionId = await this.authorize(ARCHITECTURE_INVARIANT_CAPABILITIES.evaluate, FELTDB_ARCHITECTURE_EVALUATION_COLLECTION, evaluationId);
|
|
609
|
+
// The declaration is read, never written: the version must already exist,
|
|
610
|
+
// and the evaluation carries its digest so the words can be proved later.
|
|
611
|
+
const declared = await this.evaluationStore.read(FELTDB_ARCHITECTURE_INVARIANT_VERSION_COLLECTION, this.versionKey(invariantId, input.invariantVersion));
|
|
612
|
+
if (!declared || declared.tenant_id !== this.scope.tenantId || declared.application_id !== this.scope.applicationId) {
|
|
613
|
+
throw new FeltDBServiceError(`Architecture invariant ${invariantId} has no version ${input.invariantVersion} to evaluate`, FeltDBErrorCode.NOT_FOUND);
|
|
614
|
+
}
|
|
615
|
+
const key = this.scopedKey(evaluationId);
|
|
616
|
+
if (await this.evaluationStore.read(FELTDB_ARCHITECTURE_EVALUATION_COLLECTION, key)) {
|
|
617
|
+
throw new FeltDBServiceError(`Evaluation ${evaluationId} is already recorded`, FeltDBErrorCode.IDEMPOTENCY_CONFLICT);
|
|
618
|
+
}
|
|
619
|
+
const at = this.clock();
|
|
620
|
+
const row = {
|
|
621
|
+
id: key,
|
|
622
|
+
evaluation_id: evaluationId,
|
|
623
|
+
tenant_id: this.scope.tenantId,
|
|
624
|
+
application_id: this.scope.applicationId,
|
|
625
|
+
contract_version: ARCHITECTURE_INVARIANT_CONTRACT_VERSION,
|
|
626
|
+
invariant_id: invariantId,
|
|
627
|
+
invariant_version: declared.version,
|
|
628
|
+
invariant_declaration_digest: declared.declaration_digest,
|
|
629
|
+
evaluator_id: requireText(input.evaluator, 'evaluator', 200),
|
|
630
|
+
evaluator: {
|
|
631
|
+
id: requireText(input.evaluator, 'evaluator', 200),
|
|
632
|
+
version: input.evaluatorVersion === undefined ? 'unspecified' : requireText(input.evaluatorVersion, 'evaluatorVersion', 200),
|
|
633
|
+
},
|
|
634
|
+
result,
|
|
635
|
+
evidence,
|
|
636
|
+
explanation,
|
|
637
|
+
findings,
|
|
638
|
+
...(input.confidence === undefined ? {} : { confidence: input.confidence }),
|
|
639
|
+
evaluated_at: resolveEvaluatedAt(input.evaluatedAt, at),
|
|
640
|
+
recorded_at: timestamp(at),
|
|
641
|
+
provenance: this.provenance(decisionId, at),
|
|
642
|
+
};
|
|
643
|
+
await this.evaluationStore.create(FELTDB_ARCHITECTURE_EVALUATION_COLLECTION, key, row);
|
|
644
|
+
return toEvaluation(row);
|
|
645
|
+
},
|
|
646
|
+
get: async (evaluationId) => {
|
|
647
|
+
const id = requireIdentity(evaluationId, 'evaluationId');
|
|
648
|
+
await this.authorize(ARCHITECTURE_INVARIANT_CAPABILITIES.readEvaluation, FELTDB_ARCHITECTURE_EVALUATION_COLLECTION, id);
|
|
649
|
+
const row = await this.store.read(FELTDB_ARCHITECTURE_EVALUATION_COLLECTION, this.scopedKey(id));
|
|
650
|
+
if (!row || row.tenant_id !== this.scope.tenantId || row.application_id !== this.scope.applicationId) {
|
|
651
|
+
throw new FeltDBServiceError(`Evaluation ${id} was not found`, FeltDBErrorCode.NOT_FOUND);
|
|
652
|
+
}
|
|
653
|
+
return toEvaluation(row);
|
|
654
|
+
},
|
|
655
|
+
/** Page retained evaluations within this tenant and application. */
|
|
656
|
+
list: async (input = {}) => {
|
|
657
|
+
await this.authorize(ARCHITECTURE_INVARIANT_CAPABILITIES.readEvaluation, FELTDB_ARCHITECTURE_EVALUATION_COLLECTION, '*');
|
|
658
|
+
const where = [];
|
|
659
|
+
if (input.invariantId !== undefined)
|
|
660
|
+
where.push({ field: 'invariant_id', eq: requireIdentity(input.invariantId, 'invariantId') });
|
|
661
|
+
if (input.invariantVersion !== undefined) {
|
|
662
|
+
if (!Number.isInteger(input.invariantVersion) || input.invariantVersion < 1)
|
|
663
|
+
throw invalid('invariantVersion must be a positive integer');
|
|
664
|
+
where.push({ field: 'invariant_version', eq: input.invariantVersion });
|
|
665
|
+
}
|
|
666
|
+
if (input.result !== undefined)
|
|
667
|
+
where.push({ field: 'result', eq: requireMember(input.result, ARCHITECTURE_EVALUATION_RESULTS, 'result') });
|
|
668
|
+
if (input.evaluator !== undefined)
|
|
669
|
+
where.push({ field: 'evaluator_id', eq: requireText(input.evaluator, 'evaluator', 200) });
|
|
670
|
+
const page = await this.store.query(this.scopedQuery(FELTDB_ARCHITECTURE_EVALUATION_COLLECTION, where, [{ field: 'recorded_at', direction: 'desc' }], boundedLimit(input.limit), input.cursor));
|
|
671
|
+
return {
|
|
672
|
+
evaluations: page.records.map(toEvaluation),
|
|
673
|
+
...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),
|
|
674
|
+
exhausted: page.exhausted,
|
|
675
|
+
};
|
|
676
|
+
},
|
|
677
|
+
};
|
|
678
|
+
/**
|
|
679
|
+
* Return the current declaration and its latest evaluation for each invariant.
|
|
680
|
+
*
|
|
681
|
+
* The invariant page is the snapshot boundary: history is never expanded and
|
|
682
|
+
* every evaluation lookup is limited to one record.
|
|
683
|
+
*/
|
|
684
|
+
snapshot = async (input = {}) => {
|
|
685
|
+
const page = await this.invariants.list(input);
|
|
686
|
+
const entries = await Promise.all(page.invariants.map(async (invariant) => {
|
|
687
|
+
const evaluations = await this.evaluations.list({
|
|
688
|
+
invariantId: invariant.id,
|
|
689
|
+
invariantVersion: invariant.version,
|
|
690
|
+
limit: 1,
|
|
691
|
+
});
|
|
692
|
+
return {
|
|
693
|
+
invariant,
|
|
694
|
+
version: await this.invariants.version(invariant.id, invariant.version),
|
|
695
|
+
...(evaluations.evaluations[0] ? { latestEvaluation: evaluations.evaluations[0] } : {}),
|
|
696
|
+
};
|
|
697
|
+
}));
|
|
698
|
+
return {
|
|
699
|
+
entries,
|
|
700
|
+
...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),
|
|
701
|
+
exhausted: page.exhausted,
|
|
702
|
+
};
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
/** Bind the durable architecture invariant substrate to a FeltDB instance. */
|
|
706
|
+
export function createArchitectureInvariantService(db, options) {
|
|
707
|
+
return new FeltDBArchitectureInvariantService(createArchitectureInvariantStore(db), options);
|
|
708
|
+
}
|