@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
package/README.md
CHANGED
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@git-stunts/git-warp)
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<img src="hero.gif" alt="git-warp CLI demo" width="600">
|
|
8
|
+
<img src="docs/images/hero.gif" alt="git-warp CLI demo" width="600">
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
## The Core Idea
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
**git-warp** is a graph database that doesn't need a database server. It stores all its data inside a Git repository by abusing a clever trick: every piece of data is a Git commit that points to the **empty tree** — a special object that exists in every Git repo. Because the commits don't reference any actual files, they're completely invisible to normal Git operations like `git log`, `git diff`, or `git status`. Your codebase stays untouched, but there's a full graph database living alongside it.
|
|
14
|
+
|
|
15
|
+
Writers collaborate without coordination using CRDTs (Conflict-free Replicated Data Types) that guarantee deterministic convergence regardless of what order the patches arrive in.
|
|
14
16
|
|
|
15
17
|
```bash
|
|
16
18
|
npm install @git-stunts/git-warp @git-stunts/plumbing
|
|
@@ -31,19 +33,18 @@ const graph = await WarpGraph.open({
|
|
|
31
33
|
persistence,
|
|
32
34
|
graphName: 'demo',
|
|
33
35
|
writerId: 'writer-1',
|
|
34
|
-
autoMaterialize: true, // auto-materialize on query
|
|
35
36
|
});
|
|
36
37
|
|
|
37
|
-
// Write data
|
|
38
|
-
await
|
|
39
|
-
.addNode('user:alice')
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
38
|
+
// Write data — single await with graph.patch()
|
|
39
|
+
await graph.patch(p => {
|
|
40
|
+
p.addNode('user:alice')
|
|
41
|
+
.setProperty('user:alice', 'name', 'Alice')
|
|
42
|
+
.setProperty('user:alice', 'role', 'admin')
|
|
43
|
+
.addNode('user:bob')
|
|
44
|
+
.setProperty('user:bob', 'name', 'Bob')
|
|
45
|
+
.addEdge('user:alice', 'user:bob', 'manages')
|
|
46
|
+
.setEdgeProperty('user:alice', 'user:bob', 'manages', 'since', '2024');
|
|
47
|
+
});
|
|
47
48
|
|
|
48
49
|
// Query the graph
|
|
49
50
|
const result = await graph.query()
|
|
@@ -54,15 +55,21 @@ const result = await graph.query()
|
|
|
54
55
|
|
|
55
56
|
## How It Works
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
### The Multi-Writer Problem (and How It's Solved)
|
|
59
|
+
|
|
60
|
+
Multiple people (or machines, or processes) can write to the same graph **simultaneously, without any coordination**. There's no central server, no locking, no "wait your turn."
|
|
61
|
+
|
|
62
|
+
Each writer maintains their own independent chain of **patches** — atomic batches of operations like "add this node, set this property, create this edge." These patches are stored as Git commits under refs like `refs/warp/<graphName>/writers/<writerId>`.
|
|
58
63
|
|
|
59
|
-
**
|
|
64
|
+
When you want to read the graph, you **materialize** — which means replaying all patches from all writers and merging them into a single consistent view. The specific CRDT rules are:
|
|
60
65
|
|
|
61
|
-
- **Nodes and edges** use an
|
|
62
|
-
- **Properties** use Last-
|
|
63
|
-
- **Version vectors** track causality across writers
|
|
66
|
+
- **Nodes and edges** use an OR-Set (Observed-Remove Set). If Alice adds a node and Bob concurrently deletes it, the add wins — unless Bob's delete specifically observed Alice's add. This is the "add wins over concurrent remove" principle.
|
|
67
|
+
- **Properties** use LWW (Last-Writer-Wins) registers. If two writers set the same property at the same time, the one with the higher Lamport timestamp wins. Ties are broken by writer ID (lexicographic), then by patch SHA.
|
|
68
|
+
- **Version vectors** track causality across writers so the system knows which operations are concurrent vs. causally ordered.
|
|
64
69
|
|
|
65
|
-
**
|
|
70
|
+
Every operation gets a unique **EventId** — `(lamport, writerId, patchSha, opIndex)` — which creates a total ordering that makes merge results identical no matter which machine runs them.
|
|
71
|
+
|
|
72
|
+
**Checkpoints** snapshot the materialized state into a single commit for fast incremental recovery. Subsequent materializations only need to replay patches created after the checkpoint.
|
|
66
73
|
|
|
67
74
|
## Multi-Writer Collaboration
|
|
68
75
|
|
|
@@ -76,10 +83,9 @@ const graphA = await WarpGraph.open({
|
|
|
76
83
|
writerId: 'alice',
|
|
77
84
|
});
|
|
78
85
|
|
|
79
|
-
await
|
|
80
|
-
.addNode('doc:1')
|
|
81
|
-
|
|
82
|
-
.commit();
|
|
86
|
+
await graphA.patch(p => {
|
|
87
|
+
p.addNode('doc:1').setProperty('doc:1', 'title', 'Draft');
|
|
88
|
+
});
|
|
83
89
|
|
|
84
90
|
// Writer B (on machine B)
|
|
85
91
|
const graphB = await WarpGraph.open({
|
|
@@ -88,10 +94,9 @@ const graphB = await WarpGraph.open({
|
|
|
88
94
|
writerId: 'bob',
|
|
89
95
|
});
|
|
90
96
|
|
|
91
|
-
await
|
|
92
|
-
.addNode('doc:2')
|
|
93
|
-
|
|
94
|
-
.commit();
|
|
97
|
+
await graphB.patch(p => {
|
|
98
|
+
p.addNode('doc:2').setProperty('doc:2', 'title', 'Notes');
|
|
99
|
+
});
|
|
95
100
|
|
|
96
101
|
// After git push/pull, materialize merges both writers
|
|
97
102
|
const state = await graphA.materialize();
|
|
@@ -120,13 +125,11 @@ await graphA.syncWith(graphB);
|
|
|
120
125
|
|
|
121
126
|
## Querying
|
|
122
127
|
|
|
123
|
-
Query methods
|
|
128
|
+
Query methods auto-materialize by default. Just open a graph and start querying:
|
|
124
129
|
|
|
125
130
|
### Simple Queries
|
|
126
131
|
|
|
127
132
|
```javascript
|
|
128
|
-
await graph.materialize();
|
|
129
|
-
|
|
130
133
|
await graph.getNodes(); // ['user:alice', 'user:bob']
|
|
131
134
|
await graph.hasNode('user:alice'); // true
|
|
132
135
|
await graph.getNodeProps('user:alice'); // Map { 'name' => 'Alice', 'role' => 'admin' }
|
|
@@ -492,9 +495,27 @@ npm run test:deno # Deno: API integration tests
|
|
|
492
495
|
npm run test:matrix # All runtimes in parallel
|
|
493
496
|
```
|
|
494
497
|
|
|
498
|
+
## When git-warp is Most Useful
|
|
499
|
+
|
|
500
|
+
- **Distributed configuration management.** A fleet of servers each writing their own state into a shared graph without a central database.
|
|
501
|
+
- **Offline-first field applications.** Collecting data in the field with no connectivity; merging cleanly when back online.
|
|
502
|
+
- **Collaborative knowledge bases.** Researchers curating nodes and relationships independently.
|
|
503
|
+
- **Git-native issue/project tracking.** Embedding a full project graph directly in the repo.
|
|
504
|
+
- **Audit-critical systems.** Tamper-evident records with cryptographic proof (via Audit Receipts).
|
|
505
|
+
- **IoT sensor networks.** Sensors logging readings and relationships, syncing when bandwidth allows.
|
|
506
|
+
- **Game world state.** Modders independently adding content that composes without a central manager.
|
|
507
|
+
|
|
508
|
+
## When NOT to Use It
|
|
509
|
+
|
|
510
|
+
- **High-throughput transactional workloads.** If you need thousands of writes per second with immediate consistency, use Postgres or Redis.
|
|
511
|
+
- **Large binary or blob storage.** Data lives in Git commit messages (default cap 1 MB). Use object storage for images or videos.
|
|
512
|
+
- **Sub-millisecond read latency.** Materialization has overhead. Use an in-memory database for real-time gaming physics or HFT.
|
|
513
|
+
- **Simple key-value storage.** If you don't have relationships or need traversals, a graph database is overkill.
|
|
514
|
+
- **Non-Git environments.** The value proposition depends on Git infrastructure (push/pull, content-addressing).
|
|
515
|
+
|
|
495
516
|
## AIΩN Foundations Series
|
|
496
517
|
|
|
497
|
-
This package is the reference implementation of WARP (Worldline Algebra for Recursive Provenance) graphs as described in the AIΩN Foundations Series. The papers
|
|
518
|
+
This package is the reference implementation of WARP (Worldline Algebra for Recursive Provenance) graphs as described in the AIΩN Foundations Series. The papers formalize the graph as a minimal recursive state object ([Paper I](https://doi.org/10.5281/zenodo.17908005)), equip it with deterministic tick-based semantics ([Paper II](https://doi.org/10.5281/zenodo.17934512)), develop computational holography and provenance payloads ([Paper III](https://doi.org/10.5281/zenodo.17963669)), and introduce observer geometry with the translation cost metric ([Paper IV](https://doi.org/10.5281/zenodo.18038297)).
|
|
498
519
|
|
|
499
520
|
## License
|
|
500
521
|
|
package/SECURITY.md
CHANGED
|
@@ -113,6 +113,70 @@ await graph.syncWith('http://peer:3000', {
|
|
|
113
113
|
});
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
+
## Dependency Risk Assessment
|
|
117
|
+
|
|
118
|
+
| Package | Type | Risk | Notes |
|
|
119
|
+
|---|---|---|---|
|
|
120
|
+
| `@git-stunts/plumbing` | Runtime | Low | Internal package, spawns git processes with strict whitelist |
|
|
121
|
+
| `@git-stunts/alfred` | Runtime | Low | Retry/backoff utility, no I/O |
|
|
122
|
+
| `@git-stunts/trailer-codec` | Runtime | Low | Pure string encoding, no I/O |
|
|
123
|
+
| `cbor-x` | Runtime | Medium | Binary parser processing untrusted sync payloads; mitigated by body size limit in HttpSyncServer (4 MB default) |
|
|
124
|
+
| `roaring` | Runtime | Medium | Native C++ bindings (N-API); largest attack surface but sandboxed by N-API boundary, no network-facing input |
|
|
125
|
+
| `zod` | Runtime | Low | Schema validation, pure JS |
|
|
126
|
+
| `elkjs` | Runtime (lazy) | Low | ELK layout engine, pure JS, lazy-loaded only for `--view` |
|
|
127
|
+
| `chalk` | CLI-only | Negligible | Terminal coloring, no security surface |
|
|
128
|
+
| `boxen` | CLI-only | Negligible | Terminal box drawing |
|
|
129
|
+
| `cli-table3` | CLI-only | Negligible | Terminal table rendering |
|
|
130
|
+
| `string-width` | CLI-only | Negligible | String measurement |
|
|
131
|
+
| `strip-ansi` | Inlined | Negligible | ANSI escape removal; inlined into `src/visualization/utils/ansi.js` since v10.1.2, no longer a direct dependency |
|
|
132
|
+
| `open` | Transitive | Low | Opens URLs in browser; transitive dependency only, invoked by `--view` flag |
|
|
133
|
+
|
|
134
|
+
## Accepted Risks
|
|
135
|
+
|
|
136
|
+
| Risk | Severity | Owner | Expiry | Mitigation |
|
|
137
|
+
|---|---|---|---|---|
|
|
138
|
+
| `roaring` native bindings could have memory-safety bugs | Medium | @jross | 2026-08-01 | N-API sandbox; no untrusted input reaches bitmap code directly |
|
|
139
|
+
| `cbor-x` parser handles untrusted sync payloads | Medium | @jross | 2026-08-01 | 4 MB body size limit in HttpSyncServer; schema validation post-parse |
|
|
140
|
+
| Nonce cache lost on restart allows replay within clock skew window | Low | @jross | 2026-08-01 | 5-minute TTL; recommend TLS in production |
|
|
141
|
+
| No rate limiting on sync endpoint | Low | @jross | 2026-08-01 | Deploy behind reverse proxy with rate limiting |
|
|
142
|
+
|
|
143
|
+
## Threat Model Boundaries
|
|
144
|
+
|
|
145
|
+
| Category | In Scope | Out of Scope |
|
|
146
|
+
|---|---|---|
|
|
147
|
+
| **Data integrity** | CRDT convergence, CAS ref updates, patch ordering | Filesystem corruption, disk failure |
|
|
148
|
+
| **Authentication** | HMAC-SHA256 sync auth, key rotation, replay protection | User identity management, OAuth/OIDC |
|
|
149
|
+
| **Authorization** | Writer whitelist (per-graph allowed writers) | Role-based access control, per-node ACLs |
|
|
150
|
+
| **Transport** | Body signing, tamper detection | TLS termination (delegated to reverse proxy) |
|
|
151
|
+
| **Availability** | Body size limits, streaming for large graphs | DDoS protection, rate limiting (delegated to infrastructure) |
|
|
152
|
+
| **Supply chain** | `npm audit` in CI, pinned dependencies via lockfile | Runtime SCA scanning, SBOM generation |
|
|
153
|
+
|
|
154
|
+
## Writer Authorization
|
|
155
|
+
|
|
156
|
+
The sync server supports an optional writer whitelist that restricts which writer IDs can submit patches through the HTTP sync protocol.
|
|
157
|
+
|
|
158
|
+
### Configuration
|
|
159
|
+
|
|
160
|
+
```js
|
|
161
|
+
await graph.serve({
|
|
162
|
+
port: 3000,
|
|
163
|
+
httpPort: new NodeHttpAdapter(),
|
|
164
|
+
auth: {
|
|
165
|
+
keys: { default: 'your-shared-secret' },
|
|
166
|
+
mode: 'enforce',
|
|
167
|
+
},
|
|
168
|
+
allowedWriters: ['alice', 'bob', 'node-1'],
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Rules
|
|
173
|
+
|
|
174
|
+
- Writer ID matching is **case-sensitive** and follows the pattern `[A-Za-z0-9._-]+` (1–64 characters)
|
|
175
|
+
- Writer IDs are validated at server startup; invalid IDs throw immediately
|
|
176
|
+
- When `allowedWriters` is not set (default), all authenticated writers are permitted
|
|
177
|
+
- When set, sync requests containing patches from unlisted writers receive HTTP 403 (`FORBIDDEN_WRITER`)
|
|
178
|
+
- The `forbiddenWriterRejects` metric tracks rejected requests
|
|
179
|
+
|
|
116
180
|
## Reporting a Vulnerability
|
|
117
181
|
|
|
118
182
|
If you discover a security vulnerability, please send an e-mail to [james@flyingrobots.dev](mailto:james@flyingrobots.dev).
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import HealthCheckService from '../../../src/domain/services/HealthCheckService.js';
|
|
2
|
+
import ClockAdapter from '../../../src/infrastructure/adapters/ClockAdapter.js';
|
|
3
|
+
import { buildCheckpointRef, buildCoverageRef } from '../../../src/domain/utils/RefLayout.js';
|
|
4
|
+
import { EXIT_CODES } from '../infrastructure.js';
|
|
5
|
+
import { openGraph, applyCursorCeiling, emitCursorWarning, readCheckpointDate, createHookInstaller } from '../shared.js';
|
|
6
|
+
|
|
7
|
+
/** @typedef {import('../types.js').CliOptions} CliOptions */
|
|
8
|
+
/** @typedef {import('../types.js').Persistence} Persistence */
|
|
9
|
+
/** @typedef {import('../types.js').WarpGraphInstance} WarpGraphInstance */
|
|
10
|
+
|
|
11
|
+
/** @param {Persistence} persistence */
|
|
12
|
+
async function getHealth(persistence) {
|
|
13
|
+
const clock = ClockAdapter.global();
|
|
14
|
+
const healthService = new HealthCheckService({ persistence: /** @type {import('../../../src/domain/types/WarpPersistence.js').CorePersistence} */ (/** @type {unknown} */ (persistence)), clock });
|
|
15
|
+
return await healthService.getHealth();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** @param {WarpGraphInstance} graph */
|
|
19
|
+
async function getGcMetrics(graph) {
|
|
20
|
+
await graph.materialize();
|
|
21
|
+
return graph.getGCMetrics();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** @param {WarpGraphInstance} graph */
|
|
25
|
+
async function collectWriterHeads(graph) {
|
|
26
|
+
const frontier = await graph.getFrontier();
|
|
27
|
+
return [...frontier.entries()]
|
|
28
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
29
|
+
.map(([writerId, sha]) => ({ writerId, sha }));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {Persistence} persistence
|
|
34
|
+
* @param {string} graphName
|
|
35
|
+
*/
|
|
36
|
+
async function loadCheckpointInfo(persistence, graphName) {
|
|
37
|
+
const checkpointRef = buildCheckpointRef(graphName);
|
|
38
|
+
const checkpointSha = await persistence.readRef(checkpointRef);
|
|
39
|
+
const checkpointDate = await readCheckpointDate(persistence, checkpointSha);
|
|
40
|
+
const checkpointAgeSeconds = computeAgeSeconds(checkpointDate);
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
ref: checkpointRef,
|
|
44
|
+
sha: checkpointSha || null,
|
|
45
|
+
date: checkpointDate,
|
|
46
|
+
ageSeconds: checkpointAgeSeconds,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** @param {string|null} checkpointDate */
|
|
51
|
+
function computeAgeSeconds(checkpointDate) {
|
|
52
|
+
if (!checkpointDate) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const parsed = Date.parse(checkpointDate);
|
|
56
|
+
if (Number.isNaN(parsed)) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return Math.max(0, Math.floor((Date.now() - parsed) / 1000));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {Persistence} persistence
|
|
64
|
+
* @param {string} graphName
|
|
65
|
+
* @param {Array<{writerId: string, sha: string}>} writerHeads
|
|
66
|
+
*/
|
|
67
|
+
async function loadCoverageInfo(persistence, graphName, writerHeads) {
|
|
68
|
+
const coverageRef = buildCoverageRef(graphName);
|
|
69
|
+
const coverageSha = await persistence.readRef(coverageRef);
|
|
70
|
+
const missingWriters = coverageSha
|
|
71
|
+
? await findMissingWriters(persistence, writerHeads, coverageSha)
|
|
72
|
+
: [];
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
ref: coverageRef,
|
|
76
|
+
sha: coverageSha || null,
|
|
77
|
+
missingWriters: missingWriters.sort(),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {Persistence} persistence
|
|
83
|
+
* @param {Array<{writerId: string, sha: string}>} writerHeads
|
|
84
|
+
* @param {string} coverageSha
|
|
85
|
+
*/
|
|
86
|
+
async function findMissingWriters(persistence, writerHeads, coverageSha) {
|
|
87
|
+
const missing = [];
|
|
88
|
+
for (const head of writerHeads) {
|
|
89
|
+
const reachable = await persistence.isAncestor(head.sha, coverageSha);
|
|
90
|
+
if (!reachable) {
|
|
91
|
+
missing.push(head.writerId);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return missing;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {{repo: string, graphName: string, health: *, checkpoint: *, writerHeads: Array<{writerId: string, sha: string}>, coverage: *, gcMetrics: *, hook: *|null, status: *|null}} params
|
|
99
|
+
*/
|
|
100
|
+
function buildCheckPayload({
|
|
101
|
+
repo,
|
|
102
|
+
graphName,
|
|
103
|
+
health,
|
|
104
|
+
checkpoint,
|
|
105
|
+
writerHeads,
|
|
106
|
+
coverage,
|
|
107
|
+
gcMetrics,
|
|
108
|
+
hook,
|
|
109
|
+
status,
|
|
110
|
+
}) {
|
|
111
|
+
return {
|
|
112
|
+
repo,
|
|
113
|
+
graph: graphName,
|
|
114
|
+
health,
|
|
115
|
+
checkpoint,
|
|
116
|
+
writers: {
|
|
117
|
+
count: writerHeads.length,
|
|
118
|
+
heads: writerHeads,
|
|
119
|
+
},
|
|
120
|
+
coverage,
|
|
121
|
+
gc: gcMetrics,
|
|
122
|
+
hook: hook || null,
|
|
123
|
+
status: status || null,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** @param {string} repoPath */
|
|
128
|
+
function getHookStatusForCheck(repoPath) {
|
|
129
|
+
try {
|
|
130
|
+
const installer = createHookInstaller();
|
|
131
|
+
return installer.getHookStatus(repoPath);
|
|
132
|
+
} catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Handles the `check` command: reports graph health, GC, and hook status.
|
|
139
|
+
* @param {{options: CliOptions}} params
|
|
140
|
+
* @returns {Promise<{payload: unknown, exitCode: number}>}
|
|
141
|
+
*/
|
|
142
|
+
export default async function handleCheck({ options }) {
|
|
143
|
+
const { graph, graphName, persistence } = await openGraph(options);
|
|
144
|
+
const cursorInfo = await applyCursorCeiling(graph, persistence, graphName);
|
|
145
|
+
emitCursorWarning(cursorInfo, null);
|
|
146
|
+
const health = await getHealth(persistence);
|
|
147
|
+
const gcMetrics = await getGcMetrics(graph);
|
|
148
|
+
const status = await graph.status();
|
|
149
|
+
const writerHeads = await collectWriterHeads(graph);
|
|
150
|
+
const checkpoint = await loadCheckpointInfo(persistence, graphName);
|
|
151
|
+
const coverage = await loadCoverageInfo(persistence, graphName, writerHeads);
|
|
152
|
+
const hook = getHookStatusForCheck(options.repo);
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
payload: buildCheckPayload({
|
|
156
|
+
repo: options.repo,
|
|
157
|
+
graphName,
|
|
158
|
+
health,
|
|
159
|
+
checkpoint,
|
|
160
|
+
writerHeads,
|
|
161
|
+
coverage,
|
|
162
|
+
gcMetrics,
|
|
163
|
+
hook,
|
|
164
|
+
status,
|
|
165
|
+
}),
|
|
166
|
+
exitCode: EXIT_CODES.OK,
|
|
167
|
+
};
|
|
168
|
+
}
|