@git-stunts/git-warp 10.8.0 → 11.3.3
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 +53 -32
- package/SECURITY.md +64 -0
- package/bin/cli/commands/check.js +168 -0
- package/bin/cli/commands/doctor/checks.js +422 -0
- package/bin/cli/commands/doctor/codes.js +46 -0
- package/bin/cli/commands/doctor/index.js +239 -0
- package/bin/cli/commands/doctor/types.js +89 -0
- package/bin/cli/commands/history.js +80 -0
- package/bin/cli/commands/info.js +139 -0
- package/bin/cli/commands/install-hooks.js +128 -0
- package/bin/cli/commands/materialize.js +99 -0
- package/bin/cli/commands/patch.js +142 -0
- package/bin/cli/commands/path.js +88 -0
- package/bin/cli/commands/query.js +235 -0
- package/bin/cli/commands/registry.js +32 -0
- package/bin/cli/commands/seek.js +598 -0
- package/bin/cli/commands/tree.js +230 -0
- package/bin/cli/commands/trust.js +154 -0
- package/bin/cli/commands/verify-audit.js +114 -0
- package/bin/cli/commands/view.js +46 -0
- package/bin/cli/infrastructure.js +350 -0
- package/bin/cli/schemas.js +177 -0
- package/bin/cli/shared.js +244 -0
- package/bin/cli/types.js +96 -0
- package/bin/presenters/index.js +41 -9
- package/bin/presenters/json.js +14 -12
- package/bin/presenters/text.js +286 -28
- package/bin/warp-graph.js +5 -2346
- package/index.d.ts +111 -21
- package/index.js +2 -0
- package/package.json +10 -8
- package/src/domain/WarpGraph.js +109 -3252
- 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 +3 -3
- 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 +29 -0
- package/src/domain/errors/WarpError.js +2 -2
- package/src/domain/errors/WormholeError.js +1 -1
- package/src/domain/errors/index.js +1 -0
- package/src/domain/services/AuditMessageCodec.js +137 -0
- package/src/domain/services/AuditReceiptService.js +471 -0
- package/src/domain/services/AuditVerifierService.js +707 -0
- 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 +2 -2
- 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 +120 -55
- package/src/domain/services/IndexRebuildService.js +7 -7
- package/src/domain/services/IndexStalenessChecker.js +4 -3
- package/src/domain/services/JoinReducer.js +11 -11
- package/src/domain/services/LogicalTraversal.js +1 -1
- package/src/domain/services/MessageCodecInternal.js +4 -1
- package/src/domain/services/MessageSchemaDetector.js +2 -2
- package/src/domain/services/MigrationService.js +1 -1
- package/src/domain/services/ObserverView.js +8 -8
- package/src/domain/services/PatchBuilderV2.js +42 -26
- 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 +71 -4
- 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/WarpMessageCodec.js +4 -1
- package/src/domain/services/WormholeService.js +16 -7
- package/src/domain/trust/TrustCanonical.js +42 -0
- package/src/domain/trust/TrustCrypto.js +111 -0
- package/src/domain/trust/TrustEvaluator.js +195 -0
- package/src/domain/trust/TrustRecordService.js +281 -0
- package/src/domain/trust/TrustStateBuilder.js +222 -0
- package/src/domain/trust/canonical.js +68 -0
- package/src/domain/trust/reasonCodes.js +64 -0
- package/src/domain/trust/schemas.js +160 -0
- package/src/domain/trust/verdict.js +42 -0
- 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/types/git-cas.d.ts +20 -0
- package/src/domain/utils/MinHeap.js +6 -5
- package/src/domain/utils/RefLayout.js +59 -0
- package/src/domain/utils/canonicalStringify.js +5 -4
- package/src/domain/utils/roaring.js +31 -5
- package/src/domain/warp/PatchSession.js +26 -17
- package/src/domain/warp/Writer.js +18 -3
- package/src/domain/warp/_internal.js +26 -0
- package/src/domain/warp/_wire.js +58 -0
- package/src/domain/warp/_wiredMethods.d.ts +254 -0
- package/src/domain/warp/checkpoint.methods.js +401 -0
- package/src/domain/warp/fork.methods.js +323 -0
- package/src/domain/warp/materialize.methods.js +238 -0
- package/src/domain/warp/materializeAdvanced.methods.js +350 -0
- package/src/domain/warp/patch.methods.js +554 -0
- package/src/domain/warp/provenance.methods.js +286 -0
- package/src/domain/warp/query.methods.js +280 -0
- package/src/domain/warp/subscribe.methods.js +272 -0
- package/src/domain/warp/sync.methods.js +554 -0
- 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 +79 -11
- package/src/infrastructure/adapters/InMemoryGraphAdapter.js +36 -0
- package/src/infrastructure/adapters/NodeHttpAdapter.js +2 -2
- package/src/infrastructure/adapters/WebCryptoAdapter.js +2 -2
- package/src/ports/CommitPort.js +10 -0
- package/src/ports/RefPort.js +17 -0
- 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
- package/src/hooks/post-merge.sh +0 -60
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
* @module infrastructure/adapters/CasSeekCacheAdapter
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Minimal interface for the ContentAddressableStore from @git-stunts/git-cas.
|
|
21
|
+
* @typedef {{ readManifest: Function, restore: Function, store: Function, createTree: Function }} CasStore
|
|
22
|
+
*/
|
|
23
|
+
|
|
19
24
|
import SeekCachePort from '../../ports/SeekCachePort.js';
|
|
20
25
|
import { buildSeekCacheRef } from '../../domain/utils/RefLayout.js';
|
|
21
26
|
import { Readable } from 'node:stream';
|
|
@@ -59,7 +64,7 @@ export default class CasSeekCacheAdapter extends SeekCachePort {
|
|
|
59
64
|
/**
|
|
60
65
|
* Lazily initializes the ContentAddressableStore.
|
|
61
66
|
* @private
|
|
62
|
-
* @returns {Promise
|
|
67
|
+
* @returns {Promise<CasStore>}
|
|
63
68
|
*/
|
|
64
69
|
async _getCas() {
|
|
65
70
|
if (!this._casPromise) {
|
|
@@ -73,7 +78,7 @@ export default class CasSeekCacheAdapter extends SeekCachePort {
|
|
|
73
78
|
|
|
74
79
|
/**
|
|
75
80
|
* @private
|
|
76
|
-
* @returns {Promise
|
|
81
|
+
* @returns {Promise<CasStore>}
|
|
77
82
|
*/
|
|
78
83
|
async _initCas() {
|
|
79
84
|
const { default: ContentAddressableStore } = await import(
|
|
@@ -132,7 +137,7 @@ export default class CasSeekCacheAdapter extends SeekCachePort {
|
|
|
132
137
|
* @returns {Promise<CacheIndex>} The mutated index
|
|
133
138
|
*/
|
|
134
139
|
async _mutateIndex(mutate) {
|
|
135
|
-
/** @type {
|
|
140
|
+
/** @type {unknown} */
|
|
136
141
|
let lastErr;
|
|
137
142
|
for (let attempt = 0; attempt < MAX_CAS_RETRIES; attempt++) {
|
|
138
143
|
const index = await this._readIndex();
|
|
@@ -144,7 +149,7 @@ export default class CasSeekCacheAdapter extends SeekCachePort {
|
|
|
144
149
|
lastErr = err;
|
|
145
150
|
// Transient write failure — retry with fresh read
|
|
146
151
|
if (attempt === MAX_CAS_RETRIES - 1) {
|
|
147
|
-
throw new Error(`CasSeekCacheAdapter: index update failed after retries: ${lastErr.message}`);
|
|
152
|
+
throw new Error(`CasSeekCacheAdapter: index update failed after retries: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`);
|
|
148
153
|
}
|
|
149
154
|
}
|
|
150
155
|
}
|
|
@@ -100,8 +100,8 @@ function createHandler(requestHandler, logger) {
|
|
|
100
100
|
const plain = await toPlainRequest(request);
|
|
101
101
|
const response = await requestHandler(plain);
|
|
102
102
|
return toDenoResponse(response);
|
|
103
|
-
} catch (
|
|
104
|
-
if (err.status === 413) {
|
|
103
|
+
} catch (err) {
|
|
104
|
+
if (typeof err === 'object' && err !== null && /** @type {{status?: number}} */ (err).status === 413) {
|
|
105
105
|
const msg = new TextEncoder().encode('Payload Too Large');
|
|
106
106
|
return new Response(msg, {
|
|
107
107
|
status: 413,
|
|
@@ -140,7 +140,7 @@ function closeImpl(state, callback) {
|
|
|
140
140
|
callback();
|
|
141
141
|
}
|
|
142
142
|
},
|
|
143
|
-
/** @param {
|
|
143
|
+
/** @param {unknown} err */ (err) => {
|
|
144
144
|
state.server = null;
|
|
145
145
|
if (callback) {
|
|
146
146
|
callback(err);
|
|
@@ -210,7 +210,7 @@ export default class DenoHttpAdapter extends HttpServerPort {
|
|
|
210
210
|
const hostname = typeof host === 'string' ? host : undefined;
|
|
211
211
|
|
|
212
212
|
try {
|
|
213
|
-
/** @type {
|
|
213
|
+
/** @type {DenoServeOptions} */
|
|
214
214
|
const serveOptions = {
|
|
215
215
|
port,
|
|
216
216
|
onListen() {
|
|
@@ -223,9 +223,8 @@ export default class DenoHttpAdapter extends HttpServerPort {
|
|
|
223
223
|
serveOptions.hostname = hostname;
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
-
// @ts-expect-error — Deno global is only available in Deno runtime
|
|
227
226
|
state.server = globalThis.Deno.serve(serveOptions, handler);
|
|
228
|
-
} catch (
|
|
227
|
+
} catch (err) {
|
|
229
228
|
if (cb) {
|
|
230
229
|
cb(err);
|
|
231
230
|
} else {
|
|
@@ -114,6 +114,26 @@ function getExitCode(err) {
|
|
|
114
114
|
return err?.details?.code ?? err?.exitCode ?? err?.code;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Checks if a Git error indicates a dangling or missing object.
|
|
119
|
+
* Exit code 128 with specific stderr patterns means the ref exists but
|
|
120
|
+
* points to a missing object. Other exit-128 failures (bad repo, corrupt
|
|
121
|
+
* index, permission errors) are NOT considered dangling and will re-throw.
|
|
122
|
+
* @param {GitError} err
|
|
123
|
+
* @returns {boolean}
|
|
124
|
+
*/
|
|
125
|
+
function isDanglingObjectError(err) {
|
|
126
|
+
if (getExitCode(err) !== 128) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
const stderr = (err.details?.stderr || '').toLowerCase();
|
|
130
|
+
return (
|
|
131
|
+
stderr.includes('bad object') ||
|
|
132
|
+
stderr.includes('not a valid object name') ||
|
|
133
|
+
stderr.includes('does not point to a valid object')
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
117
137
|
/**
|
|
118
138
|
* Checks whether a Git ref exists without resolving it.
|
|
119
139
|
* @param {function(Object): Promise<string>} execute - The git command executor function
|
|
@@ -125,8 +145,12 @@ async function refExists(execute, ref) {
|
|
|
125
145
|
try {
|
|
126
146
|
await execute({ args: ['show-ref', '--verify', '--quiet', ref] });
|
|
127
147
|
return true;
|
|
128
|
-
} catch (
|
|
129
|
-
|
|
148
|
+
} catch (err) {
|
|
149
|
+
const gitErr = /** @type {GitError} */ (err);
|
|
150
|
+
if (getExitCode(gitErr) === 1) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
if (isDanglingObjectError(gitErr)) {
|
|
130
154
|
return false;
|
|
131
155
|
}
|
|
132
156
|
throw err;
|
|
@@ -325,6 +349,20 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
325
349
|
};
|
|
326
350
|
}
|
|
327
351
|
|
|
352
|
+
/**
|
|
353
|
+
* Retrieves the tree OID for a given commit SHA.
|
|
354
|
+
* @param {string} sha - The commit SHA to query
|
|
355
|
+
* @returns {Promise<string>} The tree OID pointed to by the commit
|
|
356
|
+
* @throws {Error} If the SHA is invalid
|
|
357
|
+
*/
|
|
358
|
+
async getCommitTree(sha) {
|
|
359
|
+
this._validateOid(sha);
|
|
360
|
+
const output = await this._executeWithRetry({
|
|
361
|
+
args: ['rev-parse', `${sha}^{tree}`]
|
|
362
|
+
});
|
|
363
|
+
return output.trim();
|
|
364
|
+
}
|
|
365
|
+
|
|
328
366
|
/**
|
|
329
367
|
* Returns raw git log output for a ref.
|
|
330
368
|
* @param {Object} options
|
|
@@ -493,7 +531,7 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
493
531
|
/**
|
|
494
532
|
* Reads the OID a ref points to.
|
|
495
533
|
* @param {string} ref - The ref name
|
|
496
|
-
* @returns {Promise<string|null>} The OID, or null if the ref does not exist
|
|
534
|
+
* @returns {Promise<string|null>} The OID, or null if the ref does not exist or points to a dangling/missing object
|
|
497
535
|
* @throws {Error} If the ref format is invalid
|
|
498
536
|
*/
|
|
499
537
|
async readRef(ref) {
|
|
@@ -507,14 +545,44 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
507
545
|
args: ['rev-parse', ref]
|
|
508
546
|
});
|
|
509
547
|
return oid.trim();
|
|
510
|
-
} catch (
|
|
511
|
-
|
|
548
|
+
} catch (err) {
|
|
549
|
+
const gitErr = /** @type {GitError} */ (err);
|
|
550
|
+
if (getExitCode(gitErr) === 1) {
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
if (isDanglingObjectError(gitErr)) {
|
|
512
554
|
return null;
|
|
513
555
|
}
|
|
514
556
|
throw err;
|
|
515
557
|
}
|
|
516
558
|
}
|
|
517
559
|
|
|
560
|
+
/**
|
|
561
|
+
* Atomically updates a ref using compare-and-swap semantics.
|
|
562
|
+
*
|
|
563
|
+
* Uses `git update-ref ref newOid expectedOid` which is atomic CAS.
|
|
564
|
+
* Fails if the ref does not currently point to expectedOid.
|
|
565
|
+
*
|
|
566
|
+
* @param {string} ref - The ref name
|
|
567
|
+
* @param {string} newOid - The new OID to set
|
|
568
|
+
* @param {string|null} expectedOid - The expected current OID, or null if the ref must not exist
|
|
569
|
+
* @returns {Promise<void>}
|
|
570
|
+
* @throws {Error} If the ref does not match the expected value (CAS mismatch)
|
|
571
|
+
*/
|
|
572
|
+
async compareAndSwapRef(ref, newOid, expectedOid) {
|
|
573
|
+
this._validateRef(ref);
|
|
574
|
+
this._validateOid(newOid);
|
|
575
|
+
// null means "ref must not exist" → use zero OID
|
|
576
|
+
const oldArg = expectedOid || '0'.repeat(newOid.length);
|
|
577
|
+
if (expectedOid) {
|
|
578
|
+
this._validateOid(expectedOid);
|
|
579
|
+
}
|
|
580
|
+
// Direct call — CAS failures are semantically expected and must NOT be retried.
|
|
581
|
+
await this.plumbing.execute({
|
|
582
|
+
args: ['update-ref', ref, newOid, oldArg],
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
|
|
518
586
|
/**
|
|
519
587
|
* Deletes a ref.
|
|
520
588
|
* @param {string} ref - The ref name to delete
|
|
@@ -562,8 +630,8 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
562
630
|
try {
|
|
563
631
|
await this._executeWithRetry({ args: ['cat-file', '-e', sha] });
|
|
564
632
|
return true;
|
|
565
|
-
} catch (
|
|
566
|
-
if (getExitCode(err) === 1) {
|
|
633
|
+
} catch (err) {
|
|
634
|
+
if (getExitCode(/** @type {GitError} */ (err)) === 1) {
|
|
567
635
|
return false;
|
|
568
636
|
}
|
|
569
637
|
throw err;
|
|
@@ -638,8 +706,8 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
638
706
|
args: ['merge-base', '--is-ancestor', potentialAncestor, descendant]
|
|
639
707
|
});
|
|
640
708
|
return true; // Exit code 0 means it IS an ancestor
|
|
641
|
-
} catch (
|
|
642
|
-
if (this._getExitCode(err) === 1) {
|
|
709
|
+
} catch (err) {
|
|
710
|
+
if (this._getExitCode(/** @type {GitError} */ (err)) === 1) {
|
|
643
711
|
return false; // Exit code 1 means it is NOT an ancestor
|
|
644
712
|
}
|
|
645
713
|
throw err; // Re-throw unexpected errors
|
|
@@ -660,8 +728,8 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
660
728
|
});
|
|
661
729
|
// Preserve empty-string values; only drop trailing newline
|
|
662
730
|
return value.replace(/\n$/, '');
|
|
663
|
-
} catch (
|
|
664
|
-
if (this._isConfigKeyNotFound(err)) {
|
|
731
|
+
} catch (err) {
|
|
732
|
+
if (this._isConfigKeyNotFound(/** @type {GitError} */ (err))) {
|
|
665
733
|
return null;
|
|
666
734
|
}
|
|
667
735
|
throw err;
|
|
@@ -253,6 +253,19 @@ export default class InMemoryGraphAdapter extends GraphPersistencePort {
|
|
|
253
253
|
};
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
+
/**
|
|
257
|
+
* @param {string} sha
|
|
258
|
+
* @returns {Promise<string>}
|
|
259
|
+
*/
|
|
260
|
+
async getCommitTree(sha) {
|
|
261
|
+
validateOid(sha);
|
|
262
|
+
const commit = this._commits.get(sha);
|
|
263
|
+
if (!commit) {
|
|
264
|
+
throw new Error(`Commit not found: ${sha}`);
|
|
265
|
+
}
|
|
266
|
+
return commit.treeOid;
|
|
267
|
+
}
|
|
268
|
+
|
|
256
269
|
/**
|
|
257
270
|
* @param {string} sha
|
|
258
271
|
* @returns {Promise<boolean>}
|
|
@@ -356,6 +369,29 @@ export default class InMemoryGraphAdapter extends GraphPersistencePort {
|
|
|
356
369
|
this._refs.delete(ref);
|
|
357
370
|
}
|
|
358
371
|
|
|
372
|
+
/**
|
|
373
|
+
* Atomically updates a ref using compare-and-swap semantics.
|
|
374
|
+
* @param {string} ref - The ref name
|
|
375
|
+
* @param {string} newOid - The new OID to set
|
|
376
|
+
* @param {string|null} expectedOid - Expected current OID, or null if ref must not exist
|
|
377
|
+
* @returns {Promise<void>}
|
|
378
|
+
* @throws {Error} If the ref does not match the expected value (CAS mismatch)
|
|
379
|
+
*/
|
|
380
|
+
async compareAndSwapRef(ref, newOid, expectedOid) {
|
|
381
|
+
validateRef(ref);
|
|
382
|
+
validateOid(newOid);
|
|
383
|
+
if (expectedOid) {
|
|
384
|
+
validateOid(expectedOid);
|
|
385
|
+
}
|
|
386
|
+
const current = this._refs.get(ref) || null;
|
|
387
|
+
if (current !== expectedOid) {
|
|
388
|
+
throw new Error(
|
|
389
|
+
`CAS mismatch on ${ref}: expected ${expectedOid || '(none)'}, got ${current || '(none)'}`,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
this._refs.set(ref, newOid);
|
|
393
|
+
}
|
|
394
|
+
|
|
359
395
|
/**
|
|
360
396
|
* @param {string} prefix
|
|
361
397
|
* @returns {Promise<string[]>}
|
|
@@ -71,7 +71,7 @@ export default class NodeHttpAdapter extends HttpServerPort {
|
|
|
71
71
|
const logger = this._logger;
|
|
72
72
|
const server = createServer((req, res) => {
|
|
73
73
|
dispatch(req, res, { handler: requestHandler, logger }).catch(
|
|
74
|
-
/** @param {
|
|
74
|
+
/** @param {unknown} err */ (err) => {
|
|
75
75
|
logger.error('[NodeHttpAdapter] unhandled dispatch error:', err);
|
|
76
76
|
});
|
|
77
77
|
});
|
|
@@ -85,7 +85,7 @@ export default class NodeHttpAdapter extends HttpServerPort {
|
|
|
85
85
|
listen(port, host, callback) {
|
|
86
86
|
const cb = typeof host === 'function' ? host : callback;
|
|
87
87
|
const bindHost = typeof host === 'string' ? host : undefined;
|
|
88
|
-
/** @param {
|
|
88
|
+
/** @param {unknown} err */
|
|
89
89
|
const onError = (err) => {
|
|
90
90
|
if (cb) {
|
|
91
91
|
cb(err);
|
|
@@ -38,8 +38,8 @@ function toWebCryptoAlgo(algorithm) {
|
|
|
38
38
|
function toUint8Array(data) {
|
|
39
39
|
if (data instanceof Uint8Array) { return data; }
|
|
40
40
|
if (typeof data === 'string') { return new TextEncoder().encode(data); }
|
|
41
|
-
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(
|
|
42
|
-
const buf = /** @type {Buffer} */ (
|
|
41
|
+
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(data)) {
|
|
42
|
+
const buf = /** @type {Buffer} */ (data);
|
|
43
43
|
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
44
44
|
}
|
|
45
45
|
throw new Error('WebCryptoAdapter: data must be string, Buffer, or Uint8Array');
|
package/src/ports/CommitPort.js
CHANGED
|
@@ -90,6 +90,16 @@ export default class CommitPort {
|
|
|
90
90
|
throw new Error('CommitPort.nodeExists() not implemented');
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Retrieves the tree OID for a given commit SHA.
|
|
95
|
+
* @param {string} _sha - The commit SHA to query
|
|
96
|
+
* @returns {Promise<string>} The tree OID pointed to by the commit
|
|
97
|
+
* @throws {Error} If not implemented by a concrete adapter
|
|
98
|
+
*/
|
|
99
|
+
async getCommitTree(_sha) {
|
|
100
|
+
throw new Error('CommitPort.getCommitTree() not implemented');
|
|
101
|
+
}
|
|
102
|
+
|
|
93
103
|
/**
|
|
94
104
|
* Pings the repository to verify accessibility.
|
|
95
105
|
* @returns {Promise<{ok: boolean, latencyMs: number}>} Health check result with latency
|
package/src/ports/RefPort.js
CHANGED
|
@@ -48,4 +48,21 @@ export default class RefPort {
|
|
|
48
48
|
async listRefs(_prefix) {
|
|
49
49
|
throw new Error('RefPort.listRefs() not implemented');
|
|
50
50
|
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Atomically updates a ref using compare-and-swap semantics.
|
|
54
|
+
*
|
|
55
|
+
* The ref is updated to `_newOid` only if it currently points to `_expectedOid`.
|
|
56
|
+
* If `_expectedOid` is `null`, the ref must not exist (genesis CAS).
|
|
57
|
+
*
|
|
58
|
+
* @param {string} _ref - The ref name
|
|
59
|
+
* @param {string} _newOid - The new OID to set
|
|
60
|
+
* @param {string|null} _expectedOid - The expected current OID, or null if the ref must not exist
|
|
61
|
+
* @returns {Promise<void>}
|
|
62
|
+
* @throws {Error} If the ref does not match the expected value (CAS mismatch)
|
|
63
|
+
* @throws {Error} If not implemented by a concrete adapter
|
|
64
|
+
*/
|
|
65
|
+
async compareAndSwapRef(_ref, _newOid, _expectedOid) {
|
|
66
|
+
throw new Error('RefPort.compareAndSwapRef() not implemented');
|
|
67
|
+
}
|
|
51
68
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* @typedef {{ id: string, label: string, props?: Record<string,
|
|
10
|
+
* @typedef {{ id: string, label: string, props?: Record<string, unknown> }} GraphDataNode
|
|
11
11
|
* @typedef {{ from: string, to: string, label?: string }} GraphDataEdge
|
|
12
12
|
* @typedef {{ nodes: GraphDataNode[], edges: GraphDataEdge[] }} GraphData
|
|
13
13
|
*/
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* Converts a query result payload + edge array into graph data.
|
|
17
17
|
* Edges are filtered to only those connecting matched nodes.
|
|
18
18
|
*
|
|
19
|
-
* @param {{ nodes?: Array<{ id: string, props?: Record<string,
|
|
19
|
+
* @param {{ nodes?: Array<{ id: string, props?: Record<string, unknown> }> } | null} payload - Query result
|
|
20
20
|
* @param {Array<{ from: string, to: string, label?: string }>} edges - Edge array from graph.getEdges()
|
|
21
21
|
* @returns {GraphData}
|
|
22
22
|
*/
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* @typedef {{ id: string, label: string, props?: Record<string,
|
|
6
|
+
* @typedef {{ id: string, label: string, props?: Record<string, unknown> }} GraphDataNode
|
|
7
7
|
* @typedef {{ from: string, to: string, label?: string }} GraphDataEdge
|
|
8
8
|
* @typedef {{ nodes: GraphDataNode[], edges: GraphDataEdge[] }} GraphData
|
|
9
9
|
* @typedef {{ text: string }} ElkLabel
|
|
@@ -7,20 +7,23 @@
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* @typedef {{ id: string, x?: number, y?: number, width?: number, height?: number, labels?: Array<{ text: string }> }} ElkResultChild
|
|
10
|
-
* @typedef {{ id: string, sources?: string[], targets?: string[], labels?: Array<{ text: string }>, sections?:
|
|
10
|
+
* @typedef {{ id: string, sources?: string[], targets?: string[], labels?: Array<{ text: string }>, sections?: unknown[] }} ElkResultEdge
|
|
11
11
|
* @typedef {{ children?: ElkResultChild[], edges?: ElkResultEdge[], width?: number, height?: number }} ElkResult
|
|
12
|
-
* @typedef {{ id: string, x: number, y: number, width: number, height: number, label
|
|
13
|
-
* @typedef {{
|
|
12
|
+
* @typedef {{ id: string, x: number, y: number, width: number, height: number, label?: string }} PosNode
|
|
13
|
+
* @typedef {{ x: number, y: number }} LayoutPoint
|
|
14
|
+
* @typedef {{ startPoint?: LayoutPoint, endPoint?: LayoutPoint, bendPoints?: LayoutPoint[] }} LayoutSection
|
|
15
|
+
* @typedef {{ id: string, source: string, target: string, label?: string, sections?: LayoutSection[] }} PosEdge
|
|
14
16
|
* @typedef {{ nodes: PosNode[], edges: PosEdge[], width: number, height: number }} PositionedGraph
|
|
15
17
|
* @typedef {{ id: string, children?: Array<{ id: string, width?: number, height?: number, labels?: Array<{ text: string }> }>, edges?: Array<{ id: string, sources?: string[], targets?: string[], labels?: Array<{ text: string }> }>, layoutOptions?: Record<string, string> }} ElkGraphInput
|
|
18
|
+
* @typedef {{ layout: (graph: ElkGraphInput) => Promise<ElkResult> }} ElkEngine
|
|
16
19
|
*/
|
|
17
20
|
|
|
18
|
-
/** @type {Promise<
|
|
21
|
+
/** @type {Promise<unknown> | null} */
|
|
19
22
|
let elkPromise = null;
|
|
20
23
|
|
|
21
24
|
/**
|
|
22
25
|
* Returns (or creates) a singleton ELK instance.
|
|
23
|
-
* @returns {Promise<
|
|
26
|
+
* @returns {Promise<unknown>} ELK instance
|
|
24
27
|
*/
|
|
25
28
|
function getElk() {
|
|
26
29
|
if (!elkPromise) {
|
|
@@ -39,7 +42,7 @@ export async function runLayout(elkGraph) {
|
|
|
39
42
|
/** @type {ElkResult | undefined} */
|
|
40
43
|
let result;
|
|
41
44
|
try {
|
|
42
|
-
const elk = await getElk();
|
|
45
|
+
const elk = /** @type {ElkEngine} */ (await getElk());
|
|
43
46
|
result = await elk.layout(elkGraph);
|
|
44
47
|
} catch {
|
|
45
48
|
return fallbackLayout(elkGraph);
|
|
@@ -67,7 +70,7 @@ function toPositionedGraph(result) {
|
|
|
67
70
|
source: e.sources?.[0] ?? '',
|
|
68
71
|
target: e.targets?.[0] ?? '',
|
|
69
72
|
label: e.labels?.[0]?.text,
|
|
70
|
-
sections: e.sections ?? [],
|
|
73
|
+
sections: /** @type {LayoutSection[]} */ (e.sections ?? []),
|
|
71
74
|
}));
|
|
72
75
|
|
|
73
76
|
return {
|
|
@@ -21,7 +21,7 @@ import { runLayout } from './elkLayout.js';
|
|
|
21
21
|
*
|
|
22
22
|
* @param {{ nodes: Array<{ id: string, label: string }>, edges: Array<{ from: string, to: string, label?: string }> }} graphData - Normalised graph data
|
|
23
23
|
* @param {{ type?: 'query'|'path'|'slice', layoutOptions?: Record<string, string> }} [options]
|
|
24
|
-
* @returns {Promise<
|
|
24
|
+
* @returns {Promise<import('./elkLayout.js').PositionedGraph>} PositionedGraph
|
|
25
25
|
*/
|
|
26
26
|
export async function layoutGraph(graphData, options = {}) {
|
|
27
27
|
const elkGraph = toElkGraph(graphData, options);
|
|
@@ -30,7 +30,7 @@ import { formatOpSummary } from './opSummary.js';
|
|
|
30
30
|
* @property {number} patchCount
|
|
31
31
|
* @property {Map<string, WriterInfo> | Record<string, WriterInfo>} perWriter
|
|
32
32
|
* @property {{ nodes?: number, edges?: number }} [diff]
|
|
33
|
-
* @property {Record<string,
|
|
33
|
+
* @property {Record<string, unknown>} [tickReceipt]
|
|
34
34
|
* @property {import('../../../domain/services/StateDiff.js').StateDiffResult | null} [structuralDiff]
|
|
35
35
|
* @property {string} [diffBaseline]
|
|
36
36
|
* @property {number | null} [baselineTick]
|
|
@@ -73,7 +73,7 @@ function pluralize(n, singular, plural) {
|
|
|
73
73
|
return n === 1 ? singular : plural;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
/** @param {Record<string,
|
|
76
|
+
/** @param {Record<string, unknown> | undefined} tickReceipt @returns {string[]} */
|
|
77
77
|
function buildReceiptLines(tickReceipt) {
|
|
78
78
|
if (!tickReceipt || typeof tickReceipt !== 'object') {
|
|
79
79
|
return [];
|
|
@@ -85,8 +85,12 @@ function buildReceiptLines(tickReceipt) {
|
|
|
85
85
|
|
|
86
86
|
const lines = [];
|
|
87
87
|
for (const [writerId, entry] of entries) {
|
|
88
|
-
|
|
89
|
-
const
|
|
88
|
+
/** @type {Record<string, unknown>} */
|
|
89
|
+
const rec = /** @type {Record<string, unknown>} */ (entry);
|
|
90
|
+
const sha = typeof rec.sha === 'string' ? rec.sha : null;
|
|
91
|
+
const opSummary = rec.opSummary && typeof rec.opSummary === 'object'
|
|
92
|
+
? /** @type {Record<string, number>} */ (rec.opSummary)
|
|
93
|
+
: /** @type {Record<string, number>} */ (rec);
|
|
90
94
|
const name = padRight(formatWriterName(writerId, NAME_W), NAME_W);
|
|
91
95
|
const shaStr = sha ? ` ${formatSha(sha)}` : '';
|
|
92
96
|
lines.push(` ${name}${shaStr} ${formatOpSummary(opSummary, 40)}`);
|
|
@@ -443,14 +447,20 @@ function collectDiffEntries(diff) {
|
|
|
443
447
|
|
|
444
448
|
/**
|
|
445
449
|
* Formats a property value for display (truncated if too long).
|
|
446
|
-
* @param {
|
|
450
|
+
* @param {unknown} value
|
|
447
451
|
* @returns {string}
|
|
448
452
|
*/
|
|
449
453
|
function formatPropValue(value) {
|
|
450
454
|
if (value === undefined) {
|
|
451
455
|
return 'undefined';
|
|
452
456
|
}
|
|
453
|
-
|
|
457
|
+
if (typeof value === 'string') {
|
|
458
|
+
const s = `"${value}"`;
|
|
459
|
+
return s.length > 40 ? `${s.slice(0, 37)}...` : s;
|
|
460
|
+
}
|
|
461
|
+
const s = typeof value === 'number' || typeof value === 'boolean'
|
|
462
|
+
? String(value)
|
|
463
|
+
: JSON.stringify(value) ?? '';
|
|
454
464
|
return s.length > 40 ? `${s.slice(0, 37)}...` : s;
|
|
455
465
|
}
|
|
456
466
|
|
|
@@ -119,7 +119,7 @@ function renderEdge(edge) {
|
|
|
119
119
|
/**
|
|
120
120
|
* Renders a PositionedGraph as an SVG string.
|
|
121
121
|
*
|
|
122
|
-
* @param {{ nodes?: Array<{ id: string, x: number, y: number, width: number, height: number, label?: string }>, edges?: Array<{ sections?: Array<
|
|
122
|
+
* @param {{ nodes?: Array<{ id: string, x: number, y: number, width: number, height: number, label?: string }>, edges?: Array<{ sections?: Array<{ startPoint?: { x: number, y: number }, endPoint?: { x: number, y: number }, bendPoints?: Array<{ x: number, y: number }> }>, label?: string }>, width?: number, height?: number }} positionedGraph - PositionedGraph from runLayout()
|
|
123
123
|
* @param {{ title?: string }} [options]
|
|
124
124
|
* @returns {string} Complete SVG markup
|
|
125
125
|
*/
|
package/src/hooks/post-merge.sh
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
# --- @git-stunts/git-warp post-merge hook __WARP_HOOK_VERSION__ ---
|
|
3
|
-
# warp-hook-version: __WARP_HOOK_VERSION__
|
|
4
|
-
#
|
|
5
|
-
# Post-merge hook: notify (or auto-materialize) when warp refs changed.
|
|
6
|
-
#
|
|
7
|
-
# Compares refs/warp/ before and after merge by maintaining
|
|
8
|
-
# a snapshot file at .git/warp-ref-snapshot. If any warp writer refs
|
|
9
|
-
# changed and warp.autoMaterialize is true, runs `git warp materialize`.
|
|
10
|
-
# Otherwise prints an informational message advising re-materialization.
|
|
11
|
-
# Always exits 0 — never blocks a merge.
|
|
12
|
-
|
|
13
|
-
GIT_DIR=$(git rev-parse --git-dir 2>/dev/null) || exit 0
|
|
14
|
-
SNAPSHOT="${GIT_DIR}/warp-ref-snapshot"
|
|
15
|
-
|
|
16
|
-
# Capture current warp refs (sorted for stable comparison)
|
|
17
|
-
CURRENT=$(git for-each-ref --format='%(refname) %(objectname)' --sort=refname refs/warp/ 2>/dev/null) || true
|
|
18
|
-
|
|
19
|
-
if [ -z "$CURRENT" ]; then
|
|
20
|
-
# No warp refs exist — clean up any stale snapshot and exit
|
|
21
|
-
rm -f "$SNAPSHOT"
|
|
22
|
-
exit 0
|
|
23
|
-
fi
|
|
24
|
-
|
|
25
|
-
CHANGED=0
|
|
26
|
-
|
|
27
|
-
if [ -f "$SNAPSHOT" ]; then
|
|
28
|
-
PREVIOUS=$(cat "$SNAPSHOT")
|
|
29
|
-
if [ "$CURRENT" != "$PREVIOUS" ]; then
|
|
30
|
-
CHANGED=1
|
|
31
|
-
fi
|
|
32
|
-
else
|
|
33
|
-
# First encounter — refs exist but no snapshot yet
|
|
34
|
-
CHANGED=1
|
|
35
|
-
fi
|
|
36
|
-
|
|
37
|
-
# Save current state for next comparison
|
|
38
|
-
printf '%s\n' "$CURRENT" > "$SNAPSHOT"
|
|
39
|
-
|
|
40
|
-
if [ "$CHANGED" -eq 0 ]; then
|
|
41
|
-
exit 0
|
|
42
|
-
fi
|
|
43
|
-
|
|
44
|
-
AUTO_MAT=$(git config --bool warp.autoMaterialize 2>/dev/null) || true
|
|
45
|
-
|
|
46
|
-
if [ "$AUTO_MAT" = "true" ]; then
|
|
47
|
-
echo "[warp] Refs changed — auto-materializing..."
|
|
48
|
-
if command -v git-warp >/dev/null 2>&1; then
|
|
49
|
-
git-warp materialize || echo "[warp] Warning: auto-materialize failed."
|
|
50
|
-
elif command -v warp-graph >/dev/null 2>&1; then
|
|
51
|
-
warp-graph materialize || echo "[warp] Warning: auto-materialize failed."
|
|
52
|
-
else
|
|
53
|
-
echo "[warp] Warning: neither git-warp nor warp-graph found in PATH."
|
|
54
|
-
fi
|
|
55
|
-
else
|
|
56
|
-
echo "[warp] Writer refs changed during merge. Call materialize() to see updates."
|
|
57
|
-
fi
|
|
58
|
-
|
|
59
|
-
exit 0
|
|
60
|
-
# --- end @git-stunts/git-warp ---
|