@git-stunts/git-warp 11.2.1 → 11.5.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/README.md +24 -1
- package/bin/cli/commands/check.js +2 -2
- package/bin/cli/commands/doctor/checks.js +12 -12
- package/bin/cli/commands/doctor/index.js +2 -2
- package/bin/cli/commands/doctor/types.js +1 -1
- package/bin/cli/commands/history.js +12 -5
- package/bin/cli/commands/install-hooks.js +5 -5
- package/bin/cli/commands/materialize.js +2 -2
- package/bin/cli/commands/patch.js +142 -0
- package/bin/cli/commands/path.js +4 -4
- package/bin/cli/commands/query.js +54 -13
- package/bin/cli/commands/registry.js +4 -0
- package/bin/cli/commands/seek.js +17 -11
- package/bin/cli/commands/tree.js +230 -0
- package/bin/cli/commands/trust.js +3 -3
- package/bin/cli/commands/verify-audit.js +8 -7
- package/bin/cli/commands/view.js +6 -5
- package/bin/cli/infrastructure.js +26 -12
- package/bin/cli/shared.js +2 -2
- package/bin/cli/types.js +19 -8
- package/bin/presenters/index.js +35 -9
- package/bin/presenters/json.js +14 -12
- package/bin/presenters/text.js +155 -33
- package/index.d.ts +118 -22
- package/index.js +2 -0
- package/package.json +5 -3
- package/src/domain/WarpGraph.js +4 -1
- package/src/domain/crdt/ORSet.js +8 -8
- package/src/domain/errors/EmptyMessageError.js +2 -2
- package/src/domain/errors/ForkError.js +1 -1
- package/src/domain/errors/IndexError.js +1 -1
- package/src/domain/errors/OperationAbortedError.js +1 -1
- package/src/domain/errors/QueryError.js +1 -1
- package/src/domain/errors/SchemaUnsupportedError.js +1 -1
- package/src/domain/errors/ShardCorruptionError.js +2 -2
- package/src/domain/errors/ShardLoadError.js +2 -2
- package/src/domain/errors/ShardValidationError.js +4 -4
- package/src/domain/errors/StorageError.js +2 -2
- package/src/domain/errors/SyncError.js +1 -1
- package/src/domain/errors/TraversalError.js +1 -1
- package/src/domain/errors/TrustError.js +1 -1
- package/src/domain/errors/WarpError.js +2 -2
- package/src/domain/errors/WormholeError.js +1 -1
- package/src/domain/services/AuditReceiptService.js +6 -6
- package/src/domain/services/AuditVerifierService.js +52 -38
- package/src/domain/services/BitmapIndexBuilder.js +3 -3
- package/src/domain/services/BitmapIndexReader.js +28 -19
- package/src/domain/services/BoundaryTransitionRecord.js +18 -17
- package/src/domain/services/CheckpointSerializerV5.js +17 -16
- package/src/domain/services/CheckpointService.js +22 -3
- package/src/domain/services/CommitDagTraversalService.js +13 -13
- package/src/domain/services/DagPathFinding.js +7 -7
- package/src/domain/services/DagTopology.js +1 -1
- package/src/domain/services/DagTraversal.js +1 -1
- package/src/domain/services/HealthCheckService.js +1 -1
- package/src/domain/services/HookInstaller.js +1 -1
- package/src/domain/services/HttpSyncServer.js +92 -41
- package/src/domain/services/IndexRebuildService.js +7 -7
- package/src/domain/services/IndexStalenessChecker.js +4 -3
- package/src/domain/services/JoinReducer.js +26 -11
- package/src/domain/services/KeyCodec.js +7 -0
- package/src/domain/services/LogicalTraversal.js +1 -1
- package/src/domain/services/MessageCodecInternal.js +1 -1
- package/src/domain/services/MigrationService.js +1 -1
- package/src/domain/services/ObserverView.js +8 -8
- package/src/domain/services/PatchBuilderV2.js +96 -30
- package/src/domain/services/ProvenanceIndex.js +1 -1
- package/src/domain/services/ProvenancePayload.js +1 -1
- package/src/domain/services/QueryBuilder.js +3 -3
- package/src/domain/services/StateDiff.js +14 -11
- package/src/domain/services/StateSerializerV5.js +2 -2
- package/src/domain/services/StreamingBitmapIndexBuilder.js +26 -24
- package/src/domain/services/SyncAuthService.js +3 -2
- package/src/domain/services/SyncProtocol.js +25 -11
- package/src/domain/services/TemporalQuery.js +9 -6
- package/src/domain/services/TranslationCost.js +7 -5
- package/src/domain/services/WormholeService.js +16 -7
- package/src/domain/trust/TrustCanonical.js +3 -3
- package/src/domain/trust/TrustEvaluator.js +18 -3
- package/src/domain/trust/TrustRecordService.js +30 -23
- package/src/domain/trust/TrustStateBuilder.js +21 -8
- package/src/domain/trust/canonical.js +6 -6
- package/src/domain/types/TickReceipt.js +1 -1
- package/src/domain/types/WarpErrors.js +45 -0
- package/src/domain/types/WarpOptions.js +29 -0
- package/src/domain/types/WarpPersistence.js +41 -0
- package/src/domain/types/WarpTypes.js +2 -2
- package/src/domain/types/WarpTypesV2.js +2 -2
- package/src/domain/utils/MinHeap.js +6 -5
- package/src/domain/utils/canonicalStringify.js +5 -4
- package/src/domain/utils/roaring.js +31 -5
- package/src/domain/warp/PatchSession.js +40 -18
- package/src/domain/warp/_wiredMethods.d.ts +199 -45
- package/src/domain/warp/checkpoint.methods.js +5 -1
- package/src/domain/warp/fork.methods.js +2 -2
- package/src/domain/warp/materialize.methods.js +55 -5
- package/src/domain/warp/materializeAdvanced.methods.js +15 -4
- package/src/domain/warp/patch.methods.js +54 -29
- package/src/domain/warp/provenance.methods.js +5 -3
- package/src/domain/warp/query.methods.js +89 -6
- package/src/domain/warp/sync.methods.js +16 -11
- package/src/globals.d.ts +64 -0
- package/src/infrastructure/adapters/BunHttpAdapter.js +14 -9
- package/src/infrastructure/adapters/CasSeekCacheAdapter.js +9 -4
- package/src/infrastructure/adapters/DenoHttpAdapter.js +5 -6
- package/src/infrastructure/adapters/GitGraphAdapter.js +18 -13
- package/src/infrastructure/adapters/NodeHttpAdapter.js +2 -2
- package/src/infrastructure/adapters/WebCryptoAdapter.js +2 -2
- package/src/visualization/layouts/converters.js +2 -2
- package/src/visualization/layouts/elkAdapter.js +1 -1
- package/src/visualization/layouts/elkLayout.js +10 -7
- package/src/visualization/layouts/index.js +1 -1
- package/src/visualization/renderers/ascii/seek.js +16 -6
- package/src/visualization/renderers/svg/index.js +1 -1
|
@@ -22,8 +22,8 @@ import TrustError from '../errors/TrustError.js';
|
|
|
22
22
|
export class TrustRecordService {
|
|
23
23
|
/**
|
|
24
24
|
* @param {Object} options
|
|
25
|
-
* @param {
|
|
26
|
-
* @param {
|
|
25
|
+
* @param {import('../../ports/CommitPort.js').default & import('../../ports/BlobPort.js').default & import('../../ports/TreePort.js').default & import('../../ports/RefPort.js').default} options.persistence - GraphPersistencePort adapter
|
|
26
|
+
* @param {import('../../ports/CodecPort.js').default} options.codec - CodecPort adapter (CBOR)
|
|
27
27
|
*/
|
|
28
28
|
constructor({ persistence, codec }) {
|
|
29
29
|
this._persistence = persistence;
|
|
@@ -46,7 +46,7 @@ export class TrustRecordService {
|
|
|
46
46
|
* evaluation when the full key set is available.
|
|
47
47
|
*
|
|
48
48
|
* @param {string} graphName
|
|
49
|
-
* @param {Record<string,
|
|
49
|
+
* @param {Record<string, unknown>} record - Complete signed trust record
|
|
50
50
|
* @param {AppendOptions} [options]
|
|
51
51
|
* @returns {Promise<{commitSha: string, ref: string}>}
|
|
52
52
|
*/
|
|
@@ -95,7 +95,7 @@ export class TrustRecordService {
|
|
|
95
95
|
* @param {string} graphName
|
|
96
96
|
* @param {Object} [options]
|
|
97
97
|
* @param {string} [options.tip] - Override tip commit (for pinned reads)
|
|
98
|
-
* @returns {Promise<Array<Record<string,
|
|
98
|
+
* @returns {Promise<Array<Record<string, unknown>>>}
|
|
99
99
|
*/
|
|
100
100
|
async readRecords(graphName, options = {}) {
|
|
101
101
|
const ref = buildTrustRecordRef(graphName);
|
|
@@ -117,13 +117,16 @@ export class TrustRecordService {
|
|
|
117
117
|
|
|
118
118
|
while (current) {
|
|
119
119
|
const info = await this._persistence.getNodeInfo(current);
|
|
120
|
-
const
|
|
121
|
-
await this._persistence.
|
|
122
|
-
(await this._persistence.readTreeOids(
|
|
123
|
-
await this._persistence.getCommitTree(current),
|
|
124
|
-
))['record.cbor'],
|
|
125
|
-
),
|
|
120
|
+
const entries = await this._persistence.readTreeOids(
|
|
121
|
+
await this._persistence.getCommitTree(current),
|
|
126
122
|
);
|
|
123
|
+
const blobOid = entries['record.cbor'];
|
|
124
|
+
if (!blobOid) {
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
const record = /** @type {Record<string, unknown>} */ (this._codec.decode(
|
|
128
|
+
await this._persistence.readBlob(blobOid),
|
|
129
|
+
));
|
|
127
130
|
|
|
128
131
|
records.unshift(record);
|
|
129
132
|
|
|
@@ -145,7 +148,7 @@ export class TrustRecordService {
|
|
|
145
148
|
* - Each record passes schema validation
|
|
146
149
|
* - First record has prev=null
|
|
147
150
|
*
|
|
148
|
-
* @param {Array<Record<string,
|
|
151
|
+
* @param {Array<Record<string, unknown>>} records - Records in chain order (oldest first)
|
|
149
152
|
* @returns {{valid: boolean, errors: Array<{index: number, error: string}>}}
|
|
150
153
|
*/
|
|
151
154
|
verifyChain(records) {
|
|
@@ -177,7 +180,7 @@ export class TrustRecordService {
|
|
|
177
180
|
// Prev-link check
|
|
178
181
|
if (i === 0) {
|
|
179
182
|
if (record.prev !== null) {
|
|
180
|
-
errors.push({ index: i, error: `Genesis record must have prev=null, got ${record.prev}` });
|
|
183
|
+
errors.push({ index: i, error: `Genesis record must have prev=null, got ${JSON.stringify(record.prev)}` });
|
|
181
184
|
}
|
|
182
185
|
} else {
|
|
183
186
|
const expectedPrev = records[i - 1].recordId;
|
|
@@ -200,12 +203,13 @@ export class TrustRecordService {
|
|
|
200
203
|
* cryptographic verification — that requires the issuer's public key
|
|
201
204
|
* from the trust state, which is resolved during evaluation.
|
|
202
205
|
*
|
|
203
|
-
* @param {Record<string,
|
|
206
|
+
* @param {Record<string, unknown>} record
|
|
204
207
|
* @throws {TrustError} if signature envelope is missing or malformed
|
|
205
208
|
* @private
|
|
206
209
|
*/
|
|
207
210
|
_verifySignatureEnvelope(record) {
|
|
208
|
-
|
|
211
|
+
const sig = /** @type {Record<string, unknown>|undefined} */ (record.signature);
|
|
212
|
+
if (!sig || !sig.sig || !sig.alg) {
|
|
209
213
|
throw new TrustError(
|
|
210
214
|
'Trust record missing or malformed signature',
|
|
211
215
|
{ code: 'E_TRUST_SIGNATURE_MISSING' },
|
|
@@ -237,14 +241,14 @@ export class TrustRecordService {
|
|
|
237
241
|
return { tipSha, recordId: null };
|
|
238
242
|
}
|
|
239
243
|
|
|
240
|
-
const record = this._codec.decode(await this._persistence.readBlob(blobOid));
|
|
241
|
-
return { tipSha, recordId: record.recordId ?? null };
|
|
244
|
+
const record = /** @type {Record<string, unknown>} */ (this._codec.decode(await this._persistence.readBlob(blobOid)));
|
|
245
|
+
return { tipSha, recordId: /** @type {string|null} */ (record.recordId) ?? null };
|
|
242
246
|
}
|
|
243
247
|
|
|
244
248
|
/**
|
|
245
249
|
* Persists a trust record as a Git commit.
|
|
246
250
|
* @param {string} ref
|
|
247
|
-
* @param {Record<string,
|
|
251
|
+
* @param {Record<string, unknown>} record
|
|
248
252
|
* @param {string|null} parentSha - Resolved tip SHA (null for genesis)
|
|
249
253
|
* @returns {Promise<string>} commit SHA
|
|
250
254
|
* @private
|
|
@@ -252,16 +256,19 @@ export class TrustRecordService {
|
|
|
252
256
|
async _persistRecord(ref, record, parentSha) {
|
|
253
257
|
// Encode record as CBOR blob
|
|
254
258
|
const encoded = this._codec.encode(record);
|
|
255
|
-
|
|
259
|
+
// Buffer.from() ensures Uint8Array from codec is accepted by writeBlob
|
|
260
|
+
const blobOid = await this._persistence.writeBlob(Buffer.from(encoded));
|
|
256
261
|
|
|
257
|
-
// Create tree with single entry
|
|
258
|
-
const treeOid = await this._persistence.writeTree(
|
|
262
|
+
// Create tree with single entry (mktree format)
|
|
263
|
+
const treeOid = await this._persistence.writeTree([`100644 blob ${blobOid}\trecord.cbor`]);
|
|
259
264
|
|
|
260
265
|
const parents = parentSha ? [parentSha] : [];
|
|
261
|
-
const
|
|
266
|
+
const rType = typeof record.recordType === 'string' ? record.recordType : '';
|
|
267
|
+
const rId = typeof record.recordId === 'string' ? record.recordId.slice(0, 12) : '';
|
|
268
|
+
const message = `trust: ${rType} ${rId}`;
|
|
262
269
|
|
|
263
|
-
const commitSha = await this._persistence.
|
|
264
|
-
|
|
270
|
+
const commitSha = await this._persistence.commitNodeWithTree({
|
|
271
|
+
treeOid,
|
|
265
272
|
parents,
|
|
266
273
|
message,
|
|
267
274
|
});
|
|
@@ -12,6 +12,19 @@
|
|
|
12
12
|
|
|
13
13
|
import { TrustRecordSchema } from './schemas.js';
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {Object} TrustRecord
|
|
17
|
+
* @property {string} recordType - Record type (KEY_ADD, KEY_REVOKE, WRITER_BIND_ADD, WRITER_BIND_REVOKE)
|
|
18
|
+
* @property {string} recordId - Content-addressed record identifier
|
|
19
|
+
* @property {Record<string, string>} subject - Subject fields (keyId, publicKey, writerId, reasonCode vary by type)
|
|
20
|
+
* @property {string} issuedAt - ISO timestamp
|
|
21
|
+
* @property {number} schemaVersion
|
|
22
|
+
* @property {string} issuerKeyId
|
|
23
|
+
* @property {string|null} prev
|
|
24
|
+
* @property {{alg: string, sig: string}} signature
|
|
25
|
+
* @property {Record<string, unknown>} [meta]
|
|
26
|
+
*/
|
|
27
|
+
|
|
15
28
|
/**
|
|
16
29
|
* @typedef {Object} TrustState
|
|
17
30
|
* @property {Map<string, {publicKey: string, addedAt: string}>} activeKeys - keyId → key info
|
|
@@ -30,7 +43,7 @@ import { TrustRecordSchema } from './schemas.js';
|
|
|
30
43
|
* - Binding validity: WRITER_BIND_ADD requires the referenced key to be active
|
|
31
44
|
* - Schema validation: each record is validated against TrustRecordSchema
|
|
32
45
|
*
|
|
33
|
-
* @param {Array<Record<string,
|
|
46
|
+
* @param {Array<Record<string, unknown>>} records - Trust records in chain order
|
|
34
47
|
* @returns {TrustState} Frozen trust state
|
|
35
48
|
*/
|
|
36
49
|
export function buildState(records) {
|
|
@@ -49,13 +62,13 @@ export function buildState(records) {
|
|
|
49
62
|
const parsed = TrustRecordSchema.safeParse(record);
|
|
50
63
|
if (!parsed.success) {
|
|
51
64
|
errors.push({
|
|
52
|
-
recordId: record.recordId
|
|
65
|
+
recordId: typeof record.recordId === 'string' ? record.recordId : '(unknown)',
|
|
53
66
|
error: `Schema validation failed: ${parsed.error.message}`,
|
|
54
67
|
});
|
|
55
68
|
continue;
|
|
56
69
|
}
|
|
57
70
|
|
|
58
|
-
const rec = parsed.data;
|
|
71
|
+
const rec = /** @type {TrustRecord} */ (parsed.data);
|
|
59
72
|
processRecord(rec, activeKeys, revokedKeys, writerBindings, revokedBindings, errors);
|
|
60
73
|
}
|
|
61
74
|
|
|
@@ -63,7 +76,7 @@ export function buildState(records) {
|
|
|
63
76
|
}
|
|
64
77
|
|
|
65
78
|
/**
|
|
66
|
-
* @param {
|
|
79
|
+
* @param {TrustRecord} rec
|
|
67
80
|
* @param {Map<string, {publicKey: string, addedAt: string}>} activeKeys
|
|
68
81
|
* @param {Map<string, {publicKey: string, revokedAt: string, reasonCode: string}>} revokedKeys
|
|
69
82
|
* @param {Map<string, {keyId: string, boundAt: string}>} writerBindings
|
|
@@ -90,7 +103,7 @@ function processRecord(rec, activeKeys, revokedKeys, writerBindings, revokedBind
|
|
|
90
103
|
}
|
|
91
104
|
|
|
92
105
|
/**
|
|
93
|
-
* @param {
|
|
106
|
+
* @param {TrustRecord} rec
|
|
94
107
|
* @param {Map<string, {publicKey: string, addedAt: string}>} activeKeys
|
|
95
108
|
* @param {Map<string, {publicKey: string, revokedAt: string, reasonCode: string}>} revokedKeys
|
|
96
109
|
* @param {Array<{recordId: string, error: string}>} errors
|
|
@@ -118,7 +131,7 @@ function handleKeyAdd(rec, activeKeys, revokedKeys, errors) {
|
|
|
118
131
|
}
|
|
119
132
|
|
|
120
133
|
/**
|
|
121
|
-
* @param {
|
|
134
|
+
* @param {TrustRecord} rec
|
|
122
135
|
* @param {Map<string, {publicKey: string, addedAt: string}>} activeKeys
|
|
123
136
|
* @param {Map<string, {publicKey: string, revokedAt: string, reasonCode: string}>} revokedKeys
|
|
124
137
|
* @param {Array<{recordId: string, error: string}>} errors
|
|
@@ -152,7 +165,7 @@ function handleKeyRevoke(rec, activeKeys, revokedKeys, errors) {
|
|
|
152
165
|
}
|
|
153
166
|
|
|
154
167
|
/**
|
|
155
|
-
* @param {
|
|
168
|
+
* @param {TrustRecord} rec
|
|
156
169
|
* @param {Map<string, {publicKey: string, addedAt: string}>} activeKeys
|
|
157
170
|
* @param {Map<string, {publicKey: string, revokedAt: string, reasonCode: string}>} revokedKeys
|
|
158
171
|
* @param {Map<string, {keyId: string, boundAt: string}>} writerBindings
|
|
@@ -182,7 +195,7 @@ function handleBindAdd(rec, activeKeys, revokedKeys, writerBindings, errors) {
|
|
|
182
195
|
}
|
|
183
196
|
|
|
184
197
|
/**
|
|
185
|
-
* @param {
|
|
198
|
+
* @param {TrustRecord} rec
|
|
186
199
|
* @param {Map<string, {keyId: string, boundAt: string}>} writerBindings
|
|
187
200
|
* @param {Map<string, {keyId: string, revokedAt: string, reasonCode: string}>} revokedBindings
|
|
188
201
|
* @param {Array<{recordId: string, error: string}>} errors
|
|
@@ -24,8 +24,8 @@ export const TRUST_SIGN_DOMAIN = 'git-warp:trust-sign:v1\0';
|
|
|
24
24
|
* Returns the record payload used for recordId computation.
|
|
25
25
|
* Strips `recordId` and `signature` — these are derived, not inputs.
|
|
26
26
|
*
|
|
27
|
-
* @param {Record<string,
|
|
28
|
-
* @returns {Record<string,
|
|
27
|
+
* @param {Record<string, unknown>} record
|
|
28
|
+
* @returns {Record<string, unknown>}
|
|
29
29
|
*/
|
|
30
30
|
export function unsignedRecordForId(record) {
|
|
31
31
|
const out = { ...record };
|
|
@@ -38,8 +38,8 @@ export function unsignedRecordForId(record) {
|
|
|
38
38
|
* Returns the record payload used for signature computation.
|
|
39
39
|
* Strips `signature` only — `recordId` is included in signed payload.
|
|
40
40
|
*
|
|
41
|
-
* @param {Record<string,
|
|
42
|
-
* @returns {Record<string,
|
|
41
|
+
* @param {Record<string, unknown>} record
|
|
42
|
+
* @returns {Record<string, unknown>}
|
|
43
43
|
*/
|
|
44
44
|
export function unsignedRecordForSignature(record) {
|
|
45
45
|
const out = { ...record };
|
|
@@ -50,7 +50,7 @@ export function unsignedRecordForSignature(record) {
|
|
|
50
50
|
/**
|
|
51
51
|
* Computes the canonical string for recordId hashing.
|
|
52
52
|
*
|
|
53
|
-
* @param {Record<string,
|
|
53
|
+
* @param {Record<string, unknown>} record - Full record (recordId and signature will be stripped)
|
|
54
54
|
* @returns {string} Domain-separated canonical JSON string
|
|
55
55
|
*/
|
|
56
56
|
export function recordIdPayload(record) {
|
|
@@ -60,7 +60,7 @@ export function recordIdPayload(record) {
|
|
|
60
60
|
/**
|
|
61
61
|
* Computes the canonical string for signature verification.
|
|
62
62
|
*
|
|
63
|
-
* @param {Record<string,
|
|
63
|
+
* @param {Record<string, unknown>} record - Full record (signature will be stripped)
|
|
64
64
|
* @returns {string} Domain-separated canonical JSON string
|
|
65
65
|
*/
|
|
66
66
|
export function signaturePayload(record) {
|
|
@@ -67,7 +67,7 @@ function validateOp(op, index) {
|
|
|
67
67
|
throw new Error(`ops[${index}] must be an object`);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
const entry = /** @type {Record<string,
|
|
70
|
+
const entry = /** @type {Record<string, unknown>} */ (op);
|
|
71
71
|
validateOpType(entry.op, index);
|
|
72
72
|
validateOpTarget(entry.target, index);
|
|
73
73
|
validateOpResult(entry.result, index);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error narrowing utilities for catch clauses.
|
|
3
|
+
*
|
|
4
|
+
* TypeScript catch variables are `unknown`. These helpers provide
|
|
5
|
+
* type-safe narrowing without wildcard casts.
|
|
6
|
+
*
|
|
7
|
+
* @module domain/types/WarpErrors
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Narrows an unknown value to an Error instance.
|
|
12
|
+
* @param {unknown} err
|
|
13
|
+
* @returns {err is Error}
|
|
14
|
+
*/
|
|
15
|
+
export function isError(err) {
|
|
16
|
+
return err instanceof Error;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Checks if an unknown value has a string `code` property.
|
|
21
|
+
* @param {unknown} err
|
|
22
|
+
* @returns {err is {code: string, message?: string, name?: string}}
|
|
23
|
+
*/
|
|
24
|
+
export function hasErrorCode(err) {
|
|
25
|
+
return (
|
|
26
|
+
typeof err === 'object' &&
|
|
27
|
+
err !== null &&
|
|
28
|
+
'code' in err &&
|
|
29
|
+
typeof (/** @type {Record<string, unknown>} */ (err)).code === 'string'
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Narrows an unknown value to an object with a string `message` property.
|
|
35
|
+
* @param {unknown} err
|
|
36
|
+
* @returns {err is {message: string}}
|
|
37
|
+
*/
|
|
38
|
+
export function hasMessage(err) {
|
|
39
|
+
return (
|
|
40
|
+
typeof err === 'object' &&
|
|
41
|
+
err !== null &&
|
|
42
|
+
'message' in err &&
|
|
43
|
+
typeof (/** @type {Record<string, unknown>} */ (err)).message === 'string'
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared options types for warp methods.
|
|
3
|
+
* @module domain/types/WarpOptions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {Object} ServeOptions
|
|
8
|
+
* @property {number} port
|
|
9
|
+
* @property {string} [host='127.0.0.1']
|
|
10
|
+
* @property {string} [path='/sync']
|
|
11
|
+
* @property {number} [maxRequestBytes=4194304]
|
|
12
|
+
* @property {import('../../ports/HttpServerPort.js').default} httpPort
|
|
13
|
+
* @property {{keys: Record<string, string>, mode?: 'enforce'|'log-only', crypto?: import('../../ports/CryptoPort.js').default, logger?: import('../../ports/LoggerPort.js').default, wallClockMs?: () => number}} [auth]
|
|
14
|
+
* @property {string[]} [allowedWriters]
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {Object} MaterializeOptions
|
|
19
|
+
* @property {boolean} [receipts]
|
|
20
|
+
* @property {number|null} [ceiling]
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {Object} PatchCommitEvent
|
|
25
|
+
* @property {unknown} [patch]
|
|
26
|
+
* @property {string} sha
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role-specific persistence port types.
|
|
3
|
+
*
|
|
4
|
+
* Instead of casting to `any` when accessing persistence methods,
|
|
5
|
+
* use these narrow types to document which port methods are actually needed.
|
|
6
|
+
*
|
|
7
|
+
* NOTE: CommitPort, BlobPort, TreePort, and RefPort each contain both
|
|
8
|
+
* read and write methods. True read/write separation would require
|
|
9
|
+
* splitting each port, which is deferred. For now, the role-named
|
|
10
|
+
* aliases below are identical — they exist to document *intent* at
|
|
11
|
+
* each call site, not to enforce access restrictions.
|
|
12
|
+
*
|
|
13
|
+
* @module domain/types/WarpPersistence
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Full persistence port — commit + blob + tree + ref + config.
|
|
18
|
+
* @typedef {import('../../ports/GraphPersistencePort.js').default} WarpPersistence
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Commit + blob + tree + ref (no config).
|
|
23
|
+
* Used by sync readers, checkpoint creators, patch writers, and
|
|
24
|
+
* materialize paths. Identical to CheckpointPersistence by design
|
|
25
|
+
* (see module-level note).
|
|
26
|
+
* @typedef {import('../../ports/CommitPort.js').default & import('../../ports/BlobPort.js').default & import('../../ports/TreePort.js').default & import('../../ports/RefPort.js').default} CorePersistence
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Ref-only persistence — ref reads, writes, CAS, listing.
|
|
31
|
+
* @typedef {import('../../ports/RefPort.js').default} RefPersistence
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Index storage — blob reads/writes, tree reads/writes, ref reads/writes.
|
|
36
|
+
* Matches the dynamically-composed IndexStoragePort interface.
|
|
37
|
+
* @typedef {import('../../ports/BlobPort.js').default & import('../../ports/TreePort.js').default & import('../../ports/RefPort.js').default} IndexStorage
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
// Export nothing at runtime — types only
|
|
41
|
+
export {};
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
* Inline value reference - value stored directly in the operation
|
|
36
36
|
* @typedef {Object} ValueRefInline
|
|
37
37
|
* @property {'inline'} type - Discriminator for inline values
|
|
38
|
-
* @property {
|
|
38
|
+
* @property {unknown} value - The actual value (any JSON-serializable type)
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
41
|
/**
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
72
|
* Creates an inline value reference
|
|
73
|
-
* @param {
|
|
73
|
+
* @param {unknown} value - The value to store inline
|
|
74
74
|
* @returns {ValueRefInline} Inline value reference
|
|
75
75
|
*/
|
|
76
76
|
export function createInlineValue(value) {
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
* @property {'PropSet'} type - Operation type discriminator
|
|
81
81
|
* @property {NodeId} node - Node ID to set property on
|
|
82
82
|
* @property {string} key - Property key
|
|
83
|
-
* @property {
|
|
83
|
+
* @property {unknown} value - Property value (any JSON-serializable type)
|
|
84
84
|
*/
|
|
85
85
|
|
|
86
86
|
/**
|
|
@@ -156,7 +156,7 @@ export function createEdgeRemoveV2(from, to, label, observedDots) {
|
|
|
156
156
|
* Creates a PropSet operation (no dot - uses EventId)
|
|
157
157
|
* @param {NodeId} node - Node ID to set property on
|
|
158
158
|
* @param {string} key - Property key
|
|
159
|
-
* @param {
|
|
159
|
+
* @param {unknown} value - Property value (any JSON-serializable type)
|
|
160
160
|
* @returns {OpV2PropSet} PropSet operation
|
|
161
161
|
*/
|
|
162
162
|
export function createPropSetV2(node, key, value) {
|
|
@@ -3,20 +3,21 @@
|
|
|
3
3
|
* Items with lowest priority are extracted first.
|
|
4
4
|
*
|
|
5
5
|
* @class MinHeap
|
|
6
|
+
* @template T
|
|
6
7
|
*/
|
|
7
8
|
class MinHeap {
|
|
8
9
|
/**
|
|
9
10
|
* Creates an empty MinHeap.
|
|
10
11
|
*/
|
|
11
12
|
constructor() {
|
|
12
|
-
/** @type {Array<{item:
|
|
13
|
+
/** @type {Array<{item: T, priority: number}>} */
|
|
13
14
|
this.heap = [];
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Insert an item with given priority.
|
|
18
19
|
*
|
|
19
|
-
* @param {
|
|
20
|
+
* @param {T} item - The item to insert
|
|
20
21
|
* @param {number} priority - Priority value (lower = higher priority)
|
|
21
22
|
* @returns {void}
|
|
22
23
|
*/
|
|
@@ -28,14 +29,14 @@ class MinHeap {
|
|
|
28
29
|
/**
|
|
29
30
|
* Extract and return the item with minimum priority.
|
|
30
31
|
*
|
|
31
|
-
* @returns {
|
|
32
|
+
* @returns {T | undefined} The item with lowest priority, or undefined if empty
|
|
32
33
|
*/
|
|
33
34
|
extractMin() {
|
|
34
35
|
if (this.heap.length === 0) { return undefined; }
|
|
35
|
-
if (this.heap.length === 1) { return /** @type {{item:
|
|
36
|
+
if (this.heap.length === 1) { return /** @type {{item: T, priority: number}} */ (this.heap.pop()).item; }
|
|
36
37
|
|
|
37
38
|
const min = this.heap[0];
|
|
38
|
-
this.heap[0] = /** @type {{item:
|
|
39
|
+
this.heap[0] = /** @type {{item: T, priority: number}} */ (this.heap.pop());
|
|
39
40
|
this._bubbleDown(0);
|
|
40
41
|
return min.item;
|
|
41
42
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - Array elements that are undefined/function/symbol become "null"
|
|
8
8
|
* - Object properties with undefined/function/symbol values are omitted
|
|
9
9
|
*
|
|
10
|
-
* @param {
|
|
10
|
+
* @param {unknown} value - Any JSON-serializable value
|
|
11
11
|
* @returns {string} Canonical JSON string with sorted keys
|
|
12
12
|
*/
|
|
13
13
|
export function canonicalStringify(value) {
|
|
@@ -28,14 +28,15 @@ export function canonicalStringify(value) {
|
|
|
28
28
|
return `[${elements.join(',')}]`;
|
|
29
29
|
}
|
|
30
30
|
if (typeof value === 'object') {
|
|
31
|
+
const obj = /** @type {Record<string, unknown>} */ (value);
|
|
31
32
|
// Filter out keys with undefined/function/symbol values, then sort
|
|
32
|
-
const keys = Object.keys(
|
|
33
|
+
const keys = Object.keys(obj)
|
|
33
34
|
.filter(k => {
|
|
34
|
-
const v =
|
|
35
|
+
const v = obj[k];
|
|
35
36
|
return v !== undefined && typeof v !== 'function' && typeof v !== 'symbol';
|
|
36
37
|
})
|
|
37
38
|
.sort();
|
|
38
|
-
const pairs = keys.map(k => `${JSON.stringify(k)}:${canonicalStringify(
|
|
39
|
+
const pairs = keys.map(k => `${JSON.stringify(k)}:${canonicalStringify(obj[k])}`);
|
|
39
40
|
return `{${pairs.join(',')}}`;
|
|
40
41
|
}
|
|
41
42
|
return JSON.stringify(value);
|
|
@@ -30,9 +30,35 @@
|
|
|
30
30
|
*/
|
|
31
31
|
const NOT_CHECKED = Symbol('NOT_CHECKED');
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Shape of the lazily-loaded roaring module after ESM/CJS unwrapping.
|
|
35
|
+
* Uses Function instead of typeof import('roaring').RoaringBitmap32 to avoid
|
|
36
|
+
* duplicate import() references that crash Deno's JSR rewriter (deno_ast
|
|
37
|
+
* overlapping TextChange bug). The single import() reference lives on
|
|
38
|
+
* getRoaringBitmap32()'s @returns tag.
|
|
39
|
+
* @typedef {Object} RoaringModule
|
|
40
|
+
* @property {Function & { isNativelyInstalled?: () => boolean }} RoaringBitmap32
|
|
41
|
+
* @property {{ RoaringBitmap32: Function }} [default]
|
|
42
|
+
* @property {boolean} [isNativelyInstalled]
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Minimum structural contract of a RoaringBitmap32 as used by bitmap index code.
|
|
47
|
+
* Named "Subset" because it only covers methods actually called by index builders/readers.
|
|
48
|
+
* Using import('roaring').RoaringBitmap32 directly fails under checkJs + skipLibCheck
|
|
49
|
+
* because tsc doesn't fully resolve inherited methods from ReadonlyRoaringBitmap32.
|
|
50
|
+
* @typedef {Object} RoaringBitmapSubset
|
|
51
|
+
* @property {number} size
|
|
52
|
+
* @property {function(number): void} add
|
|
53
|
+
* @property {function(number): boolean} has
|
|
54
|
+
* @property {function(Iterable<number>): void} orInPlace
|
|
55
|
+
* @property {function(boolean): Uint8Array} serialize
|
|
56
|
+
* @property {function(): number[]} toArray
|
|
57
|
+
*/
|
|
58
|
+
|
|
33
59
|
/**
|
|
34
60
|
* Cached reference to the loaded roaring module.
|
|
35
|
-
* @type {
|
|
61
|
+
* @type {RoaringModule | null}
|
|
36
62
|
* @private
|
|
37
63
|
*/
|
|
38
64
|
let roaringModule = null;
|
|
@@ -51,7 +77,7 @@ let nativeAvailability = NOT_CHECKED;
|
|
|
51
77
|
* Uses a top-level-await-friendly pattern with dynamic import.
|
|
52
78
|
* The module is cached after first load.
|
|
53
79
|
*
|
|
54
|
-
* @returns {
|
|
80
|
+
* @returns {RoaringModule} The roaring module exports
|
|
55
81
|
* @throws {Error} If the roaring package is not installed or fails to load
|
|
56
82
|
* @private
|
|
57
83
|
*/
|
|
@@ -67,7 +93,7 @@ function loadRoaring() {
|
|
|
67
93
|
* This is called automatically via top-level await when the module is imported,
|
|
68
94
|
* but can also be called manually with a pre-loaded module for testing.
|
|
69
95
|
*
|
|
70
|
-
* @param {
|
|
96
|
+
* @param {RoaringModule} [mod] - Pre-loaded roaring module (for testing/DI)
|
|
71
97
|
* @returns {Promise<void>}
|
|
72
98
|
*/
|
|
73
99
|
export async function initRoaring(mod) {
|
|
@@ -76,7 +102,7 @@ export async function initRoaring(mod) {
|
|
|
76
102
|
return;
|
|
77
103
|
}
|
|
78
104
|
if (!roaringModule) {
|
|
79
|
-
roaringModule = await import('roaring');
|
|
105
|
+
roaringModule = /** @type {RoaringModule} */ (await import('roaring'));
|
|
80
106
|
// Handle both ESM default export and CJS module.exports
|
|
81
107
|
if (roaringModule.default && roaringModule.default.RoaringBitmap32) {
|
|
82
108
|
roaringModule = roaringModule.default;
|
|
@@ -116,7 +142,7 @@ try {
|
|
|
116
142
|
* const intersection = RoaringBitmap32.and(a, b); // [2, 3]
|
|
117
143
|
*/
|
|
118
144
|
export function getRoaringBitmap32() {
|
|
119
|
-
return loadRoaring().RoaringBitmap32;
|
|
145
|
+
return /** @type {typeof import('roaring').RoaringBitmap32} */ (loadRoaring().RoaringBitmap32);
|
|
120
146
|
}
|
|
121
147
|
|
|
122
148
|
/**
|
|
@@ -120,7 +120,7 @@ export class PatchSession {
|
|
|
120
120
|
*
|
|
121
121
|
* @param {string} nodeId - The node ID
|
|
122
122
|
* @param {string} key - Property key
|
|
123
|
-
* @param {
|
|
123
|
+
* @param {unknown} value - Property value (must be JSON-serializable)
|
|
124
124
|
* @returns {this} This session for chaining
|
|
125
125
|
* @throws {Error} If this session has already been committed
|
|
126
126
|
*/
|
|
@@ -137,7 +137,7 @@ export class PatchSession {
|
|
|
137
137
|
* @param {string} to - Target node ID
|
|
138
138
|
* @param {string} label - Edge label/type
|
|
139
139
|
* @param {string} key - Property key
|
|
140
|
-
* @param {
|
|
140
|
+
* @param {unknown} value - Property value (must be JSON-serializable)
|
|
141
141
|
* @returns {this} This session for chaining
|
|
142
142
|
* @throws {Error} If this session has already been committed
|
|
143
143
|
*/
|
|
@@ -148,6 +148,37 @@ export class PatchSession {
|
|
|
148
148
|
return this;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Attaches content to a node.
|
|
153
|
+
*
|
|
154
|
+
* @param {string} nodeId - The node ID to attach content to
|
|
155
|
+
* @param {Buffer|string} content - The content to attach
|
|
156
|
+
* @returns {Promise<this>} This session for chaining
|
|
157
|
+
* @throws {Error} If this session has already been committed
|
|
158
|
+
*/
|
|
159
|
+
async attachContent(nodeId, content) {
|
|
160
|
+
this._ensureNotCommitted();
|
|
161
|
+
await this._builder.attachContent(nodeId, content);
|
|
162
|
+
return this;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Attaches content to an edge.
|
|
167
|
+
*
|
|
168
|
+
* @param {string} from - Source node ID
|
|
169
|
+
* @param {string} to - Target node ID
|
|
170
|
+
* @param {string} label - Edge label/type
|
|
171
|
+
* @param {Buffer|string} content - The content to attach
|
|
172
|
+
* @returns {Promise<this>} This session for chaining
|
|
173
|
+
* @throws {Error} If this session has already been committed
|
|
174
|
+
*/
|
|
175
|
+
// eslint-disable-next-line max-params -- direct delegate matching PatchBuilderV2 signature
|
|
176
|
+
async attachEdgeContent(from, to, label, content) {
|
|
177
|
+
this._ensureNotCommitted();
|
|
178
|
+
await this._builder.attachEdgeContent(from, to, label, content);
|
|
179
|
+
return this;
|
|
180
|
+
}
|
|
181
|
+
|
|
151
182
|
/**
|
|
152
183
|
* Builds the PatchV2 object without committing.
|
|
153
184
|
*
|
|
@@ -194,23 +225,14 @@ export class PatchSession {
|
|
|
194
225
|
const sha = await this._builder.commit();
|
|
195
226
|
this._committed = true;
|
|
196
227
|
return sha;
|
|
197
|
-
} catch (
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
err.message,
|
|
204
|
-
err
|
|
205
|
-
);
|
|
228
|
+
} catch (err) {
|
|
229
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
230
|
+
const cause = err instanceof Error ? err : undefined;
|
|
231
|
+
if (errMsg.includes('Concurrent commit detected') ||
|
|
232
|
+
errMsg.includes('has advanced')) {
|
|
233
|
+
throw new WriterError('WRITER_REF_ADVANCED', errMsg, cause);
|
|
206
234
|
}
|
|
207
|
-
|
|
208
|
-
// Wrap other errors
|
|
209
|
-
throw new WriterError(
|
|
210
|
-
'PERSIST_WRITE_FAILED',
|
|
211
|
-
`Failed to persist patch: ${err.message}`,
|
|
212
|
-
err
|
|
213
|
-
);
|
|
235
|
+
throw new WriterError('PERSIST_WRITE_FAILED', `Failed to persist patch: ${errMsg}`, cause);
|
|
214
236
|
}
|
|
215
237
|
}
|
|
216
238
|
|