@githolon/testing 0.13.0 → 0.15.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/package.json +1 -1
- package/vendor/engine/engine.mjs +455 -183
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/testing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "@githolon/testing — law TDD for Nomos domains: boot the REAL engine plane in-process (the exact machinery every cloud DO, heavy container and web client runs), dispatch directives, assert rows, assert TYPED REFUSALS, fork for what-ifs — fully offline inside vitest.",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.md",
|
package/vendor/engine/engine.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// from the container instance base, ledgers ARRIVE on each op (containers have no
|
|
13
13
|
// Workers bindings — the control-plane DO mints tokens and sends them), runs in Node.
|
|
14
14
|
//
|
|
15
|
-
// CONTROL PLANE STAYS IN THE DO: ownership secrets,
|
|
15
|
+
// CONTROL PLANE STAYS IN THE DO: ownership secrets, the DLQ store,
|
|
16
16
|
// quotas/rates, alarms. The engine RETURNS data (e.g. dead-letter entries with intent
|
|
17
17
|
// bytes) and the host persists. Runtime-neutral: workerd + Node 22 (globalThis.crypto,
|
|
18
18
|
// fetch, TextEncoder are the only ambient dependencies).
|
|
@@ -39,7 +39,14 @@ function call(ex, mode, fields, STDERR) {
|
|
|
39
39
|
const reqPtr = ex.git_holon_alloc(req.length);
|
|
40
40
|
try {
|
|
41
41
|
new Uint8Array(ex.memory.buffer, reqPtr, req.length).set(req);
|
|
42
|
-
|
|
42
|
+
let packed;
|
|
43
|
+
try {
|
|
44
|
+
packed = ex.git_holon_call(reqPtr, req.length);
|
|
45
|
+
} catch (e) {
|
|
46
|
+
const tail = STDERR.length ? ` | stderr: ${STDERR.slice(-8).join(" / ")}` : "";
|
|
47
|
+
throw new Error(`${String((e && e.stack) || e)}${tail}`);
|
|
48
|
+
}
|
|
49
|
+
const { ptr, len } = unpack(packed);
|
|
43
50
|
try {
|
|
44
51
|
const e = JSON.parse(dec.decode(new Uint8Array(ex.memory.buffer, ptr, len).slice()));
|
|
45
52
|
if (!e.ok) throw new Error((e.error || "holon error") + (STDERR.length ? ` | ${STDERR.slice(-4).join(" / ")}` : ""));
|
|
@@ -48,9 +55,9 @@ function call(ex, mode, fields, STDERR) {
|
|
|
48
55
|
} finally { ex.git_holon_dealloc(reqPtr, req.length); }
|
|
49
56
|
}
|
|
50
57
|
|
|
51
|
-
function seedManifests(
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
function seedManifests(_m, _strings) {
|
|
59
|
+
// Runtime manifests are committed inside USDA packages and derived by the holon
|
|
60
|
+
// from active installed law. Hosts must not seed admission/projection sidecars.
|
|
54
61
|
}
|
|
55
62
|
|
|
56
63
|
// (#39 BAILIFF: no host-chosen `authorityScope` literal. It was a dead, hardcoded "workspace/root" stamped
|
|
@@ -61,13 +68,14 @@ export const installPayload = (hash, usda, installedBy, dispositions) => ({ doma
|
|
|
61
68
|
|
|
62
69
|
/**
|
|
63
70
|
* Boot the resident wasm + /work tree. `wasmModule` is a PRECOMPILED WebAssembly.Module
|
|
64
|
-
* (the host compiles its way); `
|
|
65
|
-
*
|
|
71
|
+
* (the host compiles its way); `replica` = the host's unique 63-bit id.
|
|
72
|
+
*
|
|
73
|
+
* Runtime read/admission metadata is sovereign holon state: committed USDA packages
|
|
74
|
+
* are folded by the githolon, and the host never stages manifest sidecars.
|
|
66
75
|
*/
|
|
67
|
-
export async function createEngine({ wasmModule, bootstrapPkg, nomosPkg,
|
|
76
|
+
export async function createEngine({ wasmModule, bootstrapPkg, nomosPkg, replica, installedBy = "nomos-cloud" }) {
|
|
68
77
|
const hashes = { bootstrap: await sha256hex(bootstrapPkg), nomos: await sha256hex(nomosPkg) };
|
|
69
78
|
const root = new Map();
|
|
70
|
-
seedManifests(root, manifestStrings);
|
|
71
79
|
root.set("domain.package.usda", new File(enc.encode(bootstrapPkg)));
|
|
72
80
|
root.set("ws", new Directory(new Map()));
|
|
73
81
|
const preopen = new PreopenDirectory("/work", root);
|
|
@@ -80,39 +88,19 @@ export async function createEngine({ wasmModule, bootstrapPkg, nomosPkg, manifes
|
|
|
80
88
|
for (const n of ["memory", "git_holon_alloc", "git_holon_dealloc", "git_holon_call"]) if (!inst.exports[n]) throw new Error(`missing export ${n}`);
|
|
81
89
|
const eng = {
|
|
82
90
|
ex: inst.exports, preopen, STDERR, seq: 0, replica, hashes,
|
|
83
|
-
bootstrapPkg, nomosPkg,
|
|
91
|
+
bootstrapPkg, nomosPkg, installedBy,
|
|
84
92
|
fs: makeGitFs(preopen, "/work", { File, Directory }),
|
|
85
93
|
mounted: new Map(), // ws -> { restored, remoteMain, restoreError }
|
|
86
94
|
mainIntents: new Map(), // ws -> { head, ids:Set, oids:Set } (admission idempotence)
|
|
87
|
-
knownDomainsCache: null,
|
|
88
95
|
};
|
|
89
96
|
return eng;
|
|
90
97
|
}
|
|
91
98
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
eng.scopedReadsCache = null;
|
|
98
|
-
eng.placementDirectivesCache = null;
|
|
99
|
-
eng.tenantTypesCache = null;
|
|
100
|
-
eng.globalAggregatesCache = null;
|
|
101
|
-
const root = eng.preopen.dir.contents;
|
|
102
|
-
seedManifests(root, strings);
|
|
103
|
-
const wsRoot = root.get("ws");
|
|
104
|
-
for (const ws of eng.mounted.keys()) {
|
|
105
|
-
const wsDir = wsRoot.contents.get(ws);
|
|
106
|
-
if (wsDir) seedManifests(wsDir.contents, strings);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** The domains DEPLOYED to this workspace = the effective identity map's keys. */
|
|
111
|
-
export function knownDomains(eng) {
|
|
112
|
-
if (eng.knownDomainsCache) return eng.knownDomainsCache;
|
|
113
|
-
eng.knownDomainsCache = new Set(Object.keys(JSON.parse(eng.manifestStrings.identity)));
|
|
114
|
-
return eng.knownDomainsCache;
|
|
115
|
-
}
|
|
99
|
+
// There is NO host→holon manifest channel. The wasm derives every read/identity
|
|
100
|
+
// manifest from the FOLDED ledger (installed_read_manifest_bytes_from_state) and
|
|
101
|
+
// refuses a package lacking nomosReadProjection. A host cannot provide a manifest:
|
|
102
|
+
// the op does not exist (Read Confinement — the kernel's transition function, not a
|
|
103
|
+
// host convention). `setManifests` is gone, not no-op'd, so the lie is unrepresentable.
|
|
116
104
|
|
|
117
105
|
// ── TRANSPARENT SHARDING: the WRONG-HOME GATE (the edge bailiff — sharding slice 2) ──
|
|
118
106
|
//
|
|
@@ -128,18 +116,10 @@ export function knownDomains(eng) {
|
|
|
128
116
|
|
|
129
117
|
/** directive routes off the effective identity manifests: `${domain} ${directiveId}` → route. */
|
|
130
118
|
export function directiveRoutes(eng) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
for (const [domain, manifestJson] of Object.entries(identity)) {
|
|
136
|
-
try {
|
|
137
|
-
const m = typeof manifestJson === "string" ? JSON.parse(manifestJson) : manifestJson;
|
|
138
|
-
for (const r of (m && m.routes) || []) routes.set(`${domain} ${r.directive}`, r);
|
|
139
|
-
} catch {}
|
|
140
|
-
}
|
|
141
|
-
eng.directiveRoutesCache = routes;
|
|
142
|
-
return routes;
|
|
119
|
+
void eng;
|
|
120
|
+
// Host-side manifest taxonomy is retired. Shard routing needs a holon-owned
|
|
121
|
+
// taxonomy API before it can be re-enabled without leaking law into the host.
|
|
122
|
+
return new Map();
|
|
143
123
|
}
|
|
144
124
|
|
|
145
125
|
/** The 48-bit route tag of a home key (must agree byte-for-byte with the client mint). */
|
|
@@ -239,39 +219,15 @@ const FRAMEWORK_AGG_TYPES = new Set([
|
|
|
239
219
|
* the suffix re-derivation, deep-verify's recount): control-plane rows (PolicyBundle,
|
|
240
220
|
* DomainInstallation) and framework law-state never skew the capacity metric. */
|
|
241
221
|
function tenantTypes(eng) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
try { readManifest = JSON.parse(eng.manifestStrings.read); } catch {}
|
|
245
|
-
// §5.4 (slice 4): GLOBAL reference data is replicated custody on every shard —
|
|
246
|
-
// its rows arrive by the reverse lane (replicateIntents — a host leg the §5.2
|
|
247
|
-
// capture never sees), so counting them as tenant load would let the three
|
|
248
|
-
// row-count lanes drift (capture vs suffix vs deep-verify recount). Excluded
|
|
249
|
-
// from the capacity metric on EVERY lane, by the one shared definition here.
|
|
250
|
-
const globals = globalAggregates(eng);
|
|
251
|
-
const out = new Set(Object.keys(readManifest.aggregateFieldKinds || {}).filter((t) => !FRAMEWORK_AGG_TYPES.has(t) && !globals.has(t)));
|
|
252
|
-
eng.tenantTypesCache = out;
|
|
253
|
-
return out;
|
|
222
|
+
void eng;
|
|
223
|
+
return new Set();
|
|
254
224
|
}
|
|
255
225
|
|
|
256
226
|
/** The derived PLACEMENT directives off the effective identity manifests:
|
|
257
227
|
* packed type `site` ⇒ `birthSite` keyed by payload field `siteId` (the law's naming). */
|
|
258
228
|
export function placementDirectives(eng) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const pascal = (s) => s.replace(/[_\s-]+(\w)/g, (_m, c) => c.toUpperCase()).replace(/^./, (c) => c.toUpperCase());
|
|
262
|
-
let identity = {};
|
|
263
|
-
try { identity = JSON.parse(eng.manifestStrings.identity); } catch {}
|
|
264
|
-
for (const manifestJson of Object.values(identity)) {
|
|
265
|
-
let m;
|
|
266
|
-
try { m = typeof manifestJson === "string" ? JSON.parse(manifestJson) : manifestJson; } catch { continue; }
|
|
267
|
-
for (const t of (m && m.workspaceTypes) || []) {
|
|
268
|
-
if (!t || t.mode !== "packed") continue;
|
|
269
|
-
const p = pascal(String(t.id));
|
|
270
|
-
out.set(`birth${p}`, { axis: t.id, keyField: `${p.length ? p[0].toLowerCase() + p.slice(1) : p}Id` });
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
eng.placementDirectivesCache = out;
|
|
274
|
-
return out;
|
|
229
|
+
void eng;
|
|
230
|
+
return new Map();
|
|
275
231
|
}
|
|
276
232
|
|
|
277
233
|
/** The ESTATE-SCOPED maintained reads off the effective identity manifests.
|
|
@@ -280,31 +236,8 @@ export function placementDirectives(eng) {
|
|
|
280
236
|
* predicate-aware in the kernel); the SUFFIX re-derivation evaluates it host-side
|
|
281
237
|
* ({@link evalCanonicalPred}) so both lanes agree byte-for-byte. */
|
|
282
238
|
export function scopedReads(eng) {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
let identity = {};
|
|
286
|
-
try { identity = JSON.parse(eng.manifestStrings.identity); } catch {}
|
|
287
|
-
for (const manifestJson of Object.values(identity)) {
|
|
288
|
-
let m;
|
|
289
|
-
try { m = typeof manifestJson === "string" ? JSON.parse(manifestJson) : manifestJson; } catch { continue; }
|
|
290
|
-
for (const c of (m && m.counts) || []) {
|
|
291
|
-
if (c && typeof c.scope === "string") out.push({ id: c.id, kind: "count", of: c.of, by: c.by ?? null, ...(c.where ? { where: c.where } : {}) });
|
|
292
|
-
}
|
|
293
|
-
for (const s of (m && m.sums) || []) {
|
|
294
|
-
if (s && typeof s.scope === "string") out.push({ id: s.id, kind: "sum", of: s.of, by: s.by ?? null, field: s.sumField, ...(s.where ? { where: s.where } : {}) });
|
|
295
|
-
}
|
|
296
|
-
// SLICE 8 — scoped EXTREMUMS: per-shard min/max ride the same delta lane
|
|
297
|
-
// (events carry post-intent ABSOLUTES; the gate extremizes-on-monotone and
|
|
298
|
-
// requires declared re-derivation on retractions). The oracle is the
|
|
299
|
-
// projection's long-maintained extrema lane (#47's `extremum` RPC).
|
|
300
|
-
for (const [key, kind] of [["mins", "min"], ["maxes", "max"]]) {
|
|
301
|
-
for (const e of (m && m[key]) || []) {
|
|
302
|
-
if (e && typeof e.scope === "string") out.push({ id: e.id, kind, of: e.of, by: e.by ?? null, field: e.valueField, ...(e.where ? { where: e.where } : {}) });
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
eng.scopedReadsCache = out;
|
|
307
|
-
return out;
|
|
239
|
+
void eng;
|
|
240
|
+
return [];
|
|
308
241
|
}
|
|
309
242
|
|
|
310
243
|
/**
|
|
@@ -333,19 +266,8 @@ export function evalCanonicalPred(pred, get) {
|
|
|
333
266
|
* DOWNWARD into every shard chain by re-admission (the reverse delta lane).
|
|
334
267
|
*/
|
|
335
268
|
export function globalAggregates(eng) {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
let identity = {};
|
|
339
|
-
try { identity = JSON.parse(eng.manifestStrings.identity); } catch {}
|
|
340
|
-
for (const manifestJson of Object.values(identity)) {
|
|
341
|
-
let m;
|
|
342
|
-
try { m = typeof manifestJson === "string" ? JSON.parse(manifestJson) : manifestJson; } catch { continue; }
|
|
343
|
-
for (const t of (m && m.workspaceTypes) || []) {
|
|
344
|
-
for (const g of (t && t.globals) || []) if (typeof g === "string") out.add(g);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
eng.globalAggregatesCache = out;
|
|
348
|
-
return out;
|
|
269
|
+
void eng;
|
|
270
|
+
return new Set();
|
|
349
271
|
}
|
|
350
272
|
|
|
351
273
|
/** Parse an intent blob's sealed events: [{aggregate, ops: Map(field -> {Str|Int})}]. */
|
|
@@ -1018,6 +940,87 @@ export async function replicateIntents(eng, ws, ledger, intents, { push = true }
|
|
|
1018
940
|
return { replicated, skipped, refused };
|
|
1019
941
|
}
|
|
1020
942
|
|
|
943
|
+
/**
|
|
944
|
+
* Open browser/session lane without a client-side Artifacts receive-pack: the
|
|
945
|
+
* client offers the exact intent bytes it authored locally; the host applies the
|
|
946
|
+
* same structural fences as session admission, folds accepted bytes, and seals
|
|
947
|
+
* main once. This is still untrusted work: owner-only/control-plane/receipt
|
|
948
|
+
* writes are refused or dead-lettered exactly like session refs.
|
|
949
|
+
*/
|
|
950
|
+
export async function admitIntentOffers(eng, ws, ledger, offers, { refuseDomains = [], receiptFence = null } = {}) {
|
|
951
|
+
const refuse = new Set(refuseDomains);
|
|
952
|
+
const main = await mainIntentIds(eng, ws);
|
|
953
|
+
const sessions = new Map();
|
|
954
|
+
const deadLetters = [];
|
|
955
|
+
let admittedTotal = 0;
|
|
956
|
+
const timings = { spans: [], intents: 0, sessions: 0 };
|
|
957
|
+
const t0 = Date.now(); let seq = 0;
|
|
958
|
+
const span = (name, kind, depth = 1, extra = {}) => {
|
|
959
|
+
const s0 = Date.now();
|
|
960
|
+
return (end = {}) => timings.spans.push({ id: `intent-offer-${t0}-${seq++}`, track: "cloud", name, kind, depth, t0: s0, t1: Date.now(), ...extra, ...end });
|
|
961
|
+
};
|
|
962
|
+
for (const offer of Array.isArray(offers) ? offers : []) {
|
|
963
|
+
const cid = typeof offer?.session === "string" && offer.session ? offer.session : "direct";
|
|
964
|
+
const rec = sessions.get(cid) || { session: cid, admitted: [], rejected: [], skipped: [] };
|
|
965
|
+
sessions.set(cid, rec);
|
|
966
|
+
const done = span(`intent offer ${cid.slice(0, 14)}`, "cadmit", 1, { client: cid });
|
|
967
|
+
let blob = offer?.blob instanceof Uint8Array ? offer.blob : null;
|
|
968
|
+
try {
|
|
969
|
+
if (!blob && typeof offer?.intentB64 === "string") blob = Uint8Array.from(atob(offer.intentB64), (c) => c.charCodeAt(0));
|
|
970
|
+
if (!blob || blob.byteLength === 0 || blob.byteLength > 256 * 1024) {
|
|
971
|
+
rec.rejected.push({ oid: String(offer?.oid || "").slice(0, 10), error: "intent offer must carry intentB64 <= 256 KiB" });
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
974
|
+
let doc = null;
|
|
975
|
+
try { doc = JSON.parse(dec.decode(blob)); } catch {}
|
|
976
|
+
const intentId = typeof doc?.id === "string" ? doc.id : null;
|
|
977
|
+
const dom = typeof doc?.payload?.domain === "string" ? doc.payload.domain : null;
|
|
978
|
+
const dirId = typeof doc?.payload?.directiveId === "string" ? doc.payload.directiveId : null;
|
|
979
|
+
const oid = String(offer?.oid || intentId || "").slice(0, 10);
|
|
980
|
+
if (!dom || dom === "nomos" || dom === "bootstrap" || refuse.has(dom)) {
|
|
981
|
+
rec.rejected.push({ oid, error: `session lane refuses ${dom ? `'${dom}'` : "non-authored"} intents — law deploys go through POST /v1/workspaces/${ws}/domains with the workspace secret` });
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
if (receiptFence && dirId && Array.isArray(receiptFence[dom]) && receiptFence[dom].includes(dirId)) {
|
|
985
|
+
const error = `'${dom}/${dirId}' is a capability RECEIPT — receipts are executor/owner writes, never open-session writes. Author it via POST /v1/workspaces/${ws}/author (owner Bearer), or — if this parked receipt is sanctioned — the owner unjams it: POST /v1/workspaces/${ws}/dead-letters/retry`;
|
|
986
|
+
rec.rejected.push({ oid, error, deadLettered: true });
|
|
987
|
+
deadLetters.push({ intentId, oid, session: cid, error, blob, domain: dom, directiveId: dirId });
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
if (intentId && main.ids.has(intentId)) {
|
|
991
|
+
rec.skipped.push({ oid, intent: intentId.slice(0, 12), alreadyOnMain: true });
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
const res = applyIntentBytes(eng, ws, blob);
|
|
995
|
+
if (res.ok) {
|
|
996
|
+
if (intentId) main.ids.add(intentId);
|
|
997
|
+
main.oids.add(res.head); main.head = res.head;
|
|
998
|
+
rec.admitted.push({ oid, head: res.head });
|
|
999
|
+
admittedTotal += 1;
|
|
1000
|
+
} else {
|
|
1001
|
+
const error = `${String(res.error || "").slice(0, 300)} — dead-lettered; fix the law (or the payload), then POST /v1/workspaces/${ws}/dead-letters/retry`;
|
|
1002
|
+
rec.rejected.push({ oid, error, deadLettered: true });
|
|
1003
|
+
deadLetters.push({ intentId, oid, session: cid, error, blob, domain: dom, directiveId: dirId });
|
|
1004
|
+
}
|
|
1005
|
+
} catch (e) {
|
|
1006
|
+
rec.rejected.push({ oid: String(offer?.oid || "").slice(0, 10), error: String(e).slice(0, 200) });
|
|
1007
|
+
} finally {
|
|
1008
|
+
done();
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
timings.sessions = sessions.size;
|
|
1012
|
+
timings.intents = admittedTotal;
|
|
1013
|
+
if (admittedTotal) {
|
|
1014
|
+
const sealDone = span("seal offered intents", "cseal", 1, { admitted: admittedTotal });
|
|
1015
|
+
await commitAndPush(eng, ws, ledger);
|
|
1016
|
+
sealDone();
|
|
1017
|
+
}
|
|
1018
|
+
const gitdir = gitdirOf(ws);
|
|
1019
|
+
const head = await git.resolveRef({ fs: eng.fs, gitdir, ref: BRANCH }).catch(() => main.head);
|
|
1020
|
+
log("admit-intent-offers", { ws, sessions: sessions.size, admitted: admittedTotal, rejected: [...sessions.values()].reduce((n, s) => n + s.rejected.length, 0) });
|
|
1021
|
+
return { sessions: [...sessions.values()].map((s) => ({ ...s, ...(s.skipped.length ? {} : { skipped: undefined }) })), main: head, deadLetters, timings };
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1021
1024
|
/**
|
|
1022
1025
|
* §5.5 (slice 4) — the CONTENT HASH of the coordinator's committed subtotal state:
|
|
1023
1026
|
* sha256 over the canonical (row-id-sorted) projection of every
|
|
@@ -1118,31 +1121,92 @@ export async function deepVerifyShard(eng, shardWs, shardLedger, coordinatorRows
|
|
|
1118
1121
|
};
|
|
1119
1122
|
}
|
|
1120
1123
|
|
|
1124
|
+
function stageWorkspaceDir(eng, ws) {
|
|
1125
|
+
const wsContents = new Map();
|
|
1126
|
+
eng.preopen.dir.contents.get("ws").contents.set(ws, new Directory(wsContents));
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1121
1129
|
/** Mount a workspace, restoring its repo from the Artifacts ledger ({remote, headers}). */
|
|
1122
1130
|
export async function mountWorkspace(eng, ws, ledger) {
|
|
1123
1131
|
if (eng.mounted.has(ws)) return eng.mounted.get(ws);
|
|
1124
|
-
|
|
1125
|
-
seedManifests(wsContents, eng.manifestStrings);
|
|
1126
|
-
eng.preopen.dir.contents.get("ws").contents.set(ws, new Directory(wsContents));
|
|
1132
|
+
stageWorkspaceDir(eng, ws);
|
|
1127
1133
|
const gitdir = gitdirOf(ws);
|
|
1128
|
-
let restored = false, remoteMain = null, restoreError = null;
|
|
1134
|
+
let restored = false, remoteMain = null, restoreError = null, checkpoint = null;
|
|
1129
1135
|
try {
|
|
1130
1136
|
const info = await git.getRemoteInfo({ http, url: ledger.remote, headers: ledger.headers });
|
|
1131
1137
|
remoteMain = (info.refs && info.refs.heads && info.refs.heads.main) || null;
|
|
1132
1138
|
if (remoteMain) {
|
|
1133
1139
|
await git.init({ fs: eng.fs, gitdir, bare: true, defaultBranch: "main" });
|
|
1134
1140
|
await git.addRemote({ fs: eng.fs, gitdir, remote: "origin", url: ledger.remote, force: true });
|
|
1135
|
-
|
|
1141
|
+
const ckpt = await discoverCheckpointFromCustody(ledger).catch(() => null);
|
|
1142
|
+
let shallowOk = false;
|
|
1143
|
+
if (ckpt?.cursor && ckpt.frontier) {
|
|
1144
|
+
const steps = [32, 96, 256, 1024];
|
|
1145
|
+
for (let i = 0; i < steps.length; i++) {
|
|
1146
|
+
const depth = steps[i], relative = i > 0;
|
|
1147
|
+
await git.fetch({ fs: eng.fs, http, gitdir, remote: "origin", ref: "main", singleBranch: true, depth, relative, headers: ledger.headers });
|
|
1148
|
+
if (await git.readCommit({ fs: eng.fs, gitdir, oid: ckpt.cursor }).then(() => true, () => false)) { shallowOk = true; break; }
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
if (!shallowOk) await git.fetch({ fs: eng.fs, http, gitdir, remote: "origin", ref: "main", singleBranch: true, headers: ledger.headers });
|
|
1136
1152
|
await git.writeRef({ fs: eng.fs, gitdir, ref: "refs/heads/main", value: remoteMain, force: true });
|
|
1137
1153
|
await git.writeRef({ fs: eng.fs, gitdir, ref: "HEAD", value: "ref: refs/heads/main", force: true, symbolic: true });
|
|
1154
|
+
if (shallowOk && ckpt?.blob) {
|
|
1155
|
+
const r = importCheckpoint(eng, ws, ckpt.blob, ckpt.frontier);
|
|
1156
|
+
checkpoint = { restored: !!(r && r.restored), frontierSeeded: !!(r && r.frontierSeeded), cursor: r && r.cursor ? r.cursor : ckpt.cursor, ref: ckpt.ref };
|
|
1157
|
+
}
|
|
1138
1158
|
restored = true;
|
|
1139
1159
|
}
|
|
1140
1160
|
} catch (e) { restoreError = String((e && e.stack) || e).split("\n").slice(0, 5).join(" | "); }
|
|
1141
|
-
const m = { restored, remoteMain, restoreError };
|
|
1161
|
+
const m = { restored, remoteMain, restoreError, checkpoint };
|
|
1142
1162
|
eng.mounted.set(ws, m);
|
|
1143
1163
|
return m;
|
|
1144
1164
|
}
|
|
1145
1165
|
|
|
1166
|
+
function assertAuthored(res, what) {
|
|
1167
|
+
if (!res || res.ok === false) throw new Error(`${what}: ${String((res && res.error) || "law refused the intent").slice(0, 500)}`);
|
|
1168
|
+
return res;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
/**
|
|
1172
|
+
* Mount a host-ephemeral workspace. It is still a normal WASI GitHolon under
|
|
1173
|
+
* `/work/ws/<name>`; the host behavior is what differs: no custody restore, no
|
|
1174
|
+
* Artifacts push, and the whole chain dies with the resident host.
|
|
1175
|
+
*/
|
|
1176
|
+
export async function mountEphemeralWorkspace(eng, ws, { domainHash = "", packageUsda = "" } = {}) {
|
|
1177
|
+
let m = eng.mounted.get(ws);
|
|
1178
|
+
if (!m) {
|
|
1179
|
+
stageWorkspaceDir(eng, ws);
|
|
1180
|
+
m = { restored: false, remoteMain: null, restoreError: null, ephemeral: true };
|
|
1181
|
+
eng.mounted.set(ws, m);
|
|
1182
|
+
}
|
|
1183
|
+
if (!nomosActive(eng, ws)) {
|
|
1184
|
+
assertAuthored(
|
|
1185
|
+
author(eng, ws, "bootstrap", "installDomain", installPayload(eng.hashes.nomos, eng.nomosPkg, eng.installedBy), "", { deferProjection: false, rngDraws: 8 }),
|
|
1186
|
+
`ephemeral workspace '${ws}' bootstrap`,
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
if (domainHash && packageUsda && qById(eng, ws, `domain-installation:${domainHash}`).length === 0) {
|
|
1190
|
+
assertAuthored(
|
|
1191
|
+
author(eng, ws, "nomos", "installDomain", installPayload(domainHash, packageUsda, eng.installedBy), eng.hashes.nomos, { deferProjection: false, rngDraws: 8 }),
|
|
1192
|
+
`ephemeral workspace '${ws}' installDomain(${domainHash.slice(0, 12)})`,
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
return m;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
/** Author into a host-ephemeral workspace and leave the resulting head in memory only. */
|
|
1199
|
+
export async function authorEphemeral(eng, ws, mountOpts, domain, directiveId, payload, lawHash, opts = {}) {
|
|
1200
|
+
await mountEphemeralWorkspace(eng, ws, mountOpts);
|
|
1201
|
+
return author(eng, ws, domain, directiveId, payload, lawHash, { deferProjection: false, ...opts });
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
/** Query a host-ephemeral workspace, mounting/installing its package first if needed. */
|
|
1205
|
+
export async function queryEphemeral(eng, ws, mountOpts, queryId, paramsJson, principal = "", keys = []) {
|
|
1206
|
+
await mountEphemeralWorkspace(eng, ws, mountOpts);
|
|
1207
|
+
return query(eng, ws, queryId, paramsJson, principal, keys);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1146
1210
|
/** Drop a workspace's in-memory state so the next access re-restores from the ledger. */
|
|
1147
1211
|
export function unmount(eng, ws) {
|
|
1148
1212
|
eng.mounted.delete(ws);
|
|
@@ -1230,8 +1294,8 @@ export function exportCheckpoint(eng, ws) {
|
|
|
1230
1294
|
/** Restore a checkpoint blob into the pool BEFORE the first read on a cold mount (no-op if the pool
|
|
1231
1295
|
* is already warm). The HOST restores ONLY a blob whose stored lawHash matches the workspace's
|
|
1232
1296
|
* CURRENT law (schema parity); a mismatched/absent blob costs at most a cold fold, never a wrong read. */
|
|
1233
|
-
export function importCheckpoint(eng, ws, blobB64) {
|
|
1234
|
-
return JSON.parse(call(eng.ex, "checkpoint_import", { repoArg: repoArgOf(ws), workspace: ws, branch: BRANCH, blob: blobB64 }, eng.STDERR));
|
|
1297
|
+
export function importCheckpoint(eng, ws, blobB64, frontierB64) {
|
|
1298
|
+
return JSON.parse(call(eng.ex, "checkpoint_import", { repoArg: repoArgOf(ws), workspace: ws, branch: BRANCH, blob: blobB64, ...(frontierB64 ? { frontier: frontierB64 } : {}) }, eng.STDERR));
|
|
1235
1299
|
}
|
|
1236
1300
|
|
|
1237
1301
|
// ── CHECKPOINT IN CUSTODY (the host-agnostic cold-mount; compute placement is beneath the contract) ──
|
|
@@ -1265,6 +1329,36 @@ async function gunzip(bytes) {
|
|
|
1265
1329
|
return streamAll(new Response(bytes).body.pipeThrough(ds));
|
|
1266
1330
|
}
|
|
1267
1331
|
|
|
1332
|
+
async function discoverCheckpointFromCustody(ledger) {
|
|
1333
|
+
const freshHeaders = () => ({ ...(ledger.headers || {}) });
|
|
1334
|
+
const refs = ((await git.listServerRefs({ http, url: ledger.remote, headers: freshHeaders(), prefix: "refs/meta/checkpoint/", protocolVersion: 2 })) || [])
|
|
1335
|
+
.filter((r) => r.ref && r.ref.startsWith("refs/meta/checkpoint/") && r.oid);
|
|
1336
|
+
if (!refs.length) return null;
|
|
1337
|
+
const root = new Map();
|
|
1338
|
+
root.set("ckpt", new Directory(new Map()));
|
|
1339
|
+
const preopen = new PreopenDirectory("/work", root);
|
|
1340
|
+
const fs = makeGitFs(preopen, "/work", { File, Directory });
|
|
1341
|
+
const gitdir = "/work/ckpt/nomos.git";
|
|
1342
|
+
await git.init({ fs, gitdir, bare: true, defaultBranch: BRANCH });
|
|
1343
|
+
await git.addRemote({ fs, gitdir, remote: "origin", url: ledger.remote, force: true });
|
|
1344
|
+
const readFile = async (oid, filepath) => { try { return (await git.readBlob({ fs, gitdir, oid, filepath })).blob; } catch { return null; } };
|
|
1345
|
+
for (let i = refs.length - 1; i >= 0 && i >= refs.length - 3; i--) {
|
|
1346
|
+
const { ref, oid } = refs[i];
|
|
1347
|
+
try {
|
|
1348
|
+
await git.fetch({ fs, http, gitdir, remote: "origin", ref, singleBranch: true, depth: 1, headers: freshHeaders() });
|
|
1349
|
+
const gz = await readFile(oid, "checkpoint");
|
|
1350
|
+
if (!gz) continue;
|
|
1351
|
+
let meta = {}; try { const mb = await readFile(oid, "meta"); if (mb) meta = JSON.parse(dec.decode(mb)); } catch {}
|
|
1352
|
+
const codec = meta.codec ?? "gzip";
|
|
1353
|
+
const blob = dec.decode(codec === "gzip" ? await gunzip(gz) : gz);
|
|
1354
|
+
const fgz = await readFile(oid, "frontier");
|
|
1355
|
+
const frontier = fgz ? dec.decode(codec === "gzip" ? await gunzip(fgz) : fgz) : null;
|
|
1356
|
+
return { ref, oid, cursor: meta.cursor || null, blob, frontier };
|
|
1357
|
+
} catch { /* try an older checkpoint */ }
|
|
1358
|
+
}
|
|
1359
|
+
return null;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1268
1362
|
/** Wrap an exported checkpoint into git objects under `refs/meta/checkpoint/<lawHash>` (LOCAL only —
|
|
1269
1363
|
* no network). The ref points at a parentless commit whose tree carries `checkpoint` (the b64 blob)
|
|
1270
1364
|
* and `meta` ({cursor,bytes,lawHash}). Returns {ref, commitOid}. Separated from the push so the
|
|
@@ -1273,25 +1367,41 @@ export async function writeCheckpointObjects(eng, ws, ex, lawHash) {
|
|
|
1273
1367
|
const gitdir = gitdirOf(ws);
|
|
1274
1368
|
const gz = await gzip(enc.encode(ex.blob));
|
|
1275
1369
|
const blobOid = await git.writeBlob({ fs: eng.fs, gitdir, blob: gz });
|
|
1276
|
-
|
|
1277
|
-
|
|
1370
|
+
// THE FOLD FRONTIER (the gate's Workspace state @cursor) rides ALONGSIDE the projection so a cold mount
|
|
1371
|
+
// can AUTHOR on a shallow clone (the projection alone is a read-collapsed view the gate can't fold from).
|
|
1372
|
+
// Era-safe: pre-this-era exports carry no `ex.frontier`, so the `frontier` file is simply absent and a
|
|
1373
|
+
// reader falls back to a full clone (writes need full history). Gzipped like the projection blob.
|
|
1374
|
+
const tree = [
|
|
1278
1375
|
{ mode: "100644", path: "checkpoint", oid: blobOid, type: "blob" },
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1376
|
+
];
|
|
1377
|
+
let fgzLen = 0;
|
|
1378
|
+
if (ex.frontier) {
|
|
1379
|
+
const fgz = await gzip(enc.encode(ex.frontier));
|
|
1380
|
+
fgzLen = fgz.length;
|
|
1381
|
+
tree.push({ mode: "100644", path: "frontier", oid: await git.writeBlob({ fs: eng.fs, gitdir, blob: fgz }), type: "blob" });
|
|
1382
|
+
}
|
|
1383
|
+
const metaOid = await git.writeBlob({ fs: eng.fs, gitdir, blob: enc.encode(JSON.stringify({ cursor: ex.cursor, bytes: ex.bytes ?? 0, lawHash, codec: "gzip", raw: ex.blob.length, stored: gz.length, frontierStored: fgzLen })) });
|
|
1384
|
+
tree.push({ mode: "100644", path: "meta", oid: metaOid, type: "blob" });
|
|
1385
|
+
const tree_ = await git.writeTree({ fs: eng.fs, gitdir, tree });
|
|
1386
|
+
const commitOid = await git.writeCommit({ fs: eng.fs, gitdir, commit: { tree: tree_, parent: [], author: CKPT_AUTHOR, committer: CKPT_AUTHOR, message: `checkpoint ${ws} @${String(ex.cursor).slice(0, 16)}` } });
|
|
1282
1387
|
const ref = CKPT_REF(lawHash);
|
|
1283
1388
|
await git.writeRef({ fs: eng.fs, gitdir, ref, value: commitOid, force: true });
|
|
1284
1389
|
return { ref, commitOid };
|
|
1285
1390
|
}
|
|
1286
1391
|
|
|
1287
|
-
/** Read a checkpoint blob back out of a commit oid
|
|
1288
|
-
* via a prior write or a shallow fetch). Returns
|
|
1392
|
+
/** Read a checkpoint's projection blob (and its fold frontier, if present) back out of a commit oid
|
|
1393
|
+
* (LOCAL only — the objects must already be present, via a prior write or a shallow fetch). Returns
|
|
1394
|
+
* `{ blob, frontier }` — both the b64 strings importCheckpoint accepts; `frontier` is null for a
|
|
1395
|
+
* pre-this-era checkpoint (no `frontier` file). */
|
|
1289
1396
|
export async function readCheckpointBlob(eng, ws, commitOid) {
|
|
1290
1397
|
const gitdir = gitdirOf(ws);
|
|
1291
1398
|
const { blob } = await git.readBlob({ fs: eng.fs, gitdir, oid: commitOid, filepath: "checkpoint" });
|
|
1292
1399
|
let codec = "gzip";
|
|
1293
1400
|
try { codec = JSON.parse(dec.decode((await git.readBlob({ fs: eng.fs, gitdir, oid: commitOid, filepath: "meta" })).blob)).codec ?? "gzip"; } catch {}
|
|
1294
|
-
|
|
1401
|
+
const dezip = async (b) => dec.decode(codec === "gzip" ? await gunzip(b) : b);
|
|
1402
|
+
let frontier = null;
|
|
1403
|
+
try { frontier = await dezip((await git.readBlob({ fs: eng.fs, gitdir, oid: commitOid, filepath: "frontier" })).blob); } catch { /* pre-this-era: no frontier */ }
|
|
1404
|
+
return { blob: await dezip(blob), frontier };
|
|
1295
1405
|
}
|
|
1296
1406
|
|
|
1297
1407
|
/** The checkpoint's schema-parity key: a hash of the workspace's CURRENT law map (every installed
|
|
@@ -1312,13 +1422,18 @@ export async function pushCheckpoint(eng, ws, ledger, lawHash) {
|
|
|
1312
1422
|
try {
|
|
1313
1423
|
lawHash = await lawParity(eng, ws, lawHash);
|
|
1314
1424
|
if (!lawHash) return { pushed: false, reason: "no-law" };
|
|
1425
|
+
const gitdir = gitdirOf(ws);
|
|
1426
|
+
const head = await git.resolveRef({ fs: eng.fs, gitdir, ref: "refs/heads/main" }).catch(() => null);
|
|
1427
|
+
eng._ckptPushed ??= new Map();
|
|
1428
|
+
if (head && eng._ckptPushed.get(ws) === `${lawHash}:${head}`) {
|
|
1429
|
+
return { pushed: false, reason: "unchanged" };
|
|
1430
|
+
}
|
|
1315
1431
|
const ex = exportCheckpoint(eng, ws);
|
|
1316
1432
|
if (!ex || ex.empty || !ex.ok || !ex.blob) return { pushed: false, reason: ex && ex.empty ? "empty" : "no-blob" };
|
|
1317
|
-
eng._ckptPushed ??= new Map();
|
|
1318
1433
|
const tag = `${lawHash}:${ex.cursor}`;
|
|
1319
1434
|
if (eng._ckptPushed.get(ws) === tag) return { pushed: false, reason: "unchanged" };
|
|
1320
1435
|
const { ref } = await writeCheckpointObjects(eng, ws, ex, lawHash);
|
|
1321
|
-
await git.push({ fs: eng.fs, http, gitdir
|
|
1436
|
+
await pushVia(ledger, () => git.push({ fs: eng.fs, http, gitdir, url: ledger.remote, ref, remoteRef: ref, force: true, headers: ledger.headers }));
|
|
1322
1437
|
eng._ckptPushed.set(ws, tag);
|
|
1323
1438
|
return { pushed: true, cursor: ex.cursor, bytes: ex.bytes ?? 0 };
|
|
1324
1439
|
} catch (e) { return { pushed: false, reason: String((e && e.message) || e).slice(0, 160) }; }
|
|
@@ -1329,25 +1444,40 @@ export async function pushCheckpoint(eng, ws, ledger, lawHash) {
|
|
|
1329
1444
|
* absent or the law drifted. Returns {restored, cursor?, reason?}. */
|
|
1330
1445
|
export async function restoreCheckpointFromCustody(eng, ws, ledger, lawHash) {
|
|
1331
1446
|
try {
|
|
1447
|
+
const mounted = eng.mounted.get(ws);
|
|
1448
|
+
if (mounted?.checkpoint?.restored) {
|
|
1449
|
+
return { restored: false, reason: "already-mounted-checkpoint" };
|
|
1450
|
+
}
|
|
1332
1451
|
lawHash = await lawParity(eng, ws, lawHash);
|
|
1333
1452
|
if (!lawHash) return { restored: false, reason: "no-law" };
|
|
1334
1453
|
const ref = CKPT_REF(lawHash);
|
|
1335
|
-
const
|
|
1454
|
+
const freshHeaders = () => ({ ...(ledger.headers || {}) });
|
|
1455
|
+
const serverRefs = await git.listServerRefs({ http, url: ledger.remote, headers: freshHeaders(), prefix: ref, protocolVersion: 2 });
|
|
1336
1456
|
const found = (serverRefs || []).find((r) => r.ref === ref);
|
|
1337
1457
|
if (!found || !found.oid) return { restored: false, reason: "absent" };
|
|
1338
1458
|
const gitdir = gitdirOf(ws);
|
|
1339
|
-
await git.fetch({ fs: eng.fs, http, gitdir, url: ledger.remote, ref, singleBranch: true, depth: 1, headers:
|
|
1340
|
-
const
|
|
1341
|
-
const r = importCheckpoint(eng, ws,
|
|
1342
|
-
|
|
1459
|
+
await git.fetch({ fs: eng.fs, http, gitdir, url: ledger.remote, ref, singleBranch: true, depth: 1, headers: freshHeaders() });
|
|
1460
|
+
const { blob, frontier } = await readCheckpointBlob(eng, ws, found.oid);
|
|
1461
|
+
const r = importCheckpoint(eng, ws, blob, frontier);
|
|
1462
|
+
if (r && r.restored === true && r.cursor) {
|
|
1463
|
+
eng._ckptPushed ??= new Map();
|
|
1464
|
+
eng._ckptPushed.set(ws, `${lawHash}:${r.cursor}`);
|
|
1465
|
+
}
|
|
1466
|
+
return { restored: r && r.restored === true, frontierSeeded: r && r.frontierSeeded === true, cursor: r && r.cursor, reason: r && r.reason };
|
|
1343
1467
|
} catch (e) { return { restored: false, reason: String((e && e.message) || e).slice(0, 160) }; }
|
|
1344
1468
|
}
|
|
1345
1469
|
|
|
1470
|
+
// Serialize a push through the host's per-repo lane when present (the DO threads `ledger.pushLane`); the
|
|
1471
|
+
// container has none and serializes on its own chain. The point: ONE receive-pack to a given Artifacts repo
|
|
1472
|
+
// at a time — a detached checkpoint/shape push can never race the admit's main push (concurrent receive-packs
|
|
1473
|
+
// were corrupting/500ing the custody store). All same-repo pushes below funnel through here.
|
|
1474
|
+
const pushVia = (ledger, fn) => (ledger && ledger.pushLane ? ledger.pushLane(fn) : fn());
|
|
1475
|
+
|
|
1346
1476
|
// DURABILITY-BEFORE-ACK: only acked once the write LANDED in the ledger; a failed push
|
|
1347
1477
|
// unmounts (drops local state) so the next access re-restores from custody.
|
|
1348
1478
|
export async function commitAndPush(eng, ws, ledger) {
|
|
1349
1479
|
try {
|
|
1350
|
-
await git.push({ fs: eng.fs, http, gitdir: gitdirOf(ws), url: ledger.remote, ref: "main", remoteRef: "main", force: false, headers: ledger.headers });
|
|
1480
|
+
await pushVia(ledger, () => git.push({ fs: eng.fs, http, gitdir: gitdirOf(ws), url: ledger.remote, ref: "main", remoteRef: "main", force: false, headers: ledger.headers }));
|
|
1351
1481
|
} catch (e) {
|
|
1352
1482
|
unmount(eng, ws);
|
|
1353
1483
|
throw new Error(`ledger push failed — write NOT durable, rolled back to ledger: ${e}`);
|
|
@@ -1374,9 +1504,9 @@ export async function pushShape(eng, ws, ledger) {
|
|
|
1374
1504
|
eng._lastShapeRef ??= new Map();
|
|
1375
1505
|
if (eng._lastShapeRef.get(ws) === newRef) return;
|
|
1376
1506
|
await git.writeRef({ fs: eng.fs, gitdir, ref: newRef, value: head, force: true });
|
|
1377
|
-
await git.push({ fs: eng.fs, http, gitdir, url: ledger.remote, ref: newRef, remoteRef: newRef, force: true, headers: ledger.headers });
|
|
1507
|
+
await pushVia(ledger, () => git.push({ fs: eng.fs, http, gitdir, url: ledger.remote, ref: newRef, remoteRef: newRef, force: true, headers: ledger.headers }));
|
|
1378
1508
|
const prev = eng._lastShapeRef.get(ws);
|
|
1379
|
-
if (prev && prev !== newRef) { try { await git.push({ fs: eng.fs, http, gitdir, url: ledger.remote, ref: prev, remoteRef: prev, delete: true, headers: ledger.headers }); } catch { /* leftover refs are harmless: the reader takes max(bytes) */ } }
|
|
1509
|
+
if (prev && prev !== newRef) { try { await pushVia(ledger, () => git.push({ fs: eng.fs, http, gitdir, url: ledger.remote, ref: prev, remoteRef: prev, delete: true, headers: ledger.headers })); } catch { /* leftover refs are harmless: the reader takes max(bytes) */ } }
|
|
1380
1510
|
eng._lastShapeRef.set(ws, newRef);
|
|
1381
1511
|
} catch { /* best-effort; placement self-heals */ }
|
|
1382
1512
|
}
|
|
@@ -1385,7 +1515,7 @@ export async function pushShape(eng, ws, ledger) {
|
|
|
1385
1515
|
// Returns {bytes} (the max, in case a restart left a stale ref) or null (no shape yet).
|
|
1386
1516
|
export async function readShape(ledger) {
|
|
1387
1517
|
try {
|
|
1388
|
-
const refs = await git.listServerRefs({ http, url: ledger.remote, headers: ledger.headers, prefix: "refs/meta/shape/", protocolVersion: 2 });
|
|
1518
|
+
const refs = await git.listServerRefs({ http, url: ledger.remote, headers: { ...(ledger.headers || {}) }, prefix: "refs/meta/shape/", protocolVersion: 2 });
|
|
1389
1519
|
let best = null;
|
|
1390
1520
|
for (const r of refs || []) {
|
|
1391
1521
|
const m = /refs\/meta\/shape\/b(\d+)$/.exec(r.ref || "");
|
|
@@ -1482,13 +1612,53 @@ async function mainIntentIds(eng, ws) {
|
|
|
1482
1612
|
* engine has no durable store), obvious attacks drop. Admitted intents merge to main,
|
|
1483
1613
|
* durably push, the branch is consumed.
|
|
1484
1614
|
*/
|
|
1485
|
-
export async function admitAll(eng, ws, ledger, { refuseDomains = [], receiptFence = null, shardRouting = null } = {}) {
|
|
1615
|
+
export async function admitAll(eng, ws, ledger, { refuseDomains = [], receiptFence = null, shardRouting = null, sessionRefs = null, defer = false } = {}) {
|
|
1486
1616
|
const refuse = new Set(refuseDomains);
|
|
1487
1617
|
const routes = shardRouting ? directiveRoutes(eng) : null;
|
|
1488
1618
|
const gitdir = gitdirOf(ws);
|
|
1489
|
-
|
|
1490
|
-
const
|
|
1619
|
+
// FLAME-GRAPH EDGE TIMINGS — coarse per-stage wall-clock the client stitches into the cross-tier trace.
|
|
1620
|
+
const _t0 = Date.now(); let _fetchMs = 0, _sealMs = 0, _intents = 0;
|
|
1621
|
+
const _spans = []; let _traceSeq = 0;
|
|
1622
|
+
const _span = (name, kind, depth = 1, extra = {}) => {
|
|
1623
|
+
const t0 = Date.now();
|
|
1624
|
+
return (end = {}) => {
|
|
1625
|
+
const t1 = Date.now();
|
|
1626
|
+
_spans.push({ id: `admit-${_t0}-${_traceSeq++}`, track: "cloud", name, kind, depth, t0, t1, ...extra, ...end });
|
|
1627
|
+
};
|
|
1628
|
+
};
|
|
1629
|
+
const sessionRefPrefix = "refs/heads/session/";
|
|
1630
|
+
const sessions = {};
|
|
1631
|
+
const indexedRefs = sessionRefs && typeof sessionRefs === "object" ? sessionRefs : null;
|
|
1632
|
+
const _scanDone = _span(indexedRefs ? "scan session index" : "scan session refs", "cscan", 1);
|
|
1633
|
+
if (indexedRefs) {
|
|
1634
|
+
const entries = Array.isArray(indexedRefs)
|
|
1635
|
+
? indexedRefs.map((r) => [String(r?.session || r?.cid || r?.client || ""), String(r?.head || r?.oid || "")])
|
|
1636
|
+
: Object.entries(indexedRefs).map(([cid, head]) => [cid, String(head || "")]);
|
|
1637
|
+
for (const [cid, head] of entries) {
|
|
1638
|
+
if (/^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/.test(cid) && /^[0-9a-f]{40}$/.test(head)) sessions[cid] = head;
|
|
1639
|
+
}
|
|
1640
|
+
} else {
|
|
1641
|
+
const refs = await git.listServerRefs({
|
|
1642
|
+
http,
|
|
1643
|
+
url: ledger.remote,
|
|
1644
|
+
headers: ledger.headers,
|
|
1645
|
+
prefix: sessionRefPrefix,
|
|
1646
|
+
protocolVersion: 2,
|
|
1647
|
+
});
|
|
1648
|
+
for (const r of refs || []) {
|
|
1649
|
+
const ref = typeof r?.ref === "string" ? r.ref : "";
|
|
1650
|
+
const oid = typeof r?.oid === "string" ? r.oid : "";
|
|
1651
|
+
if (!ref.startsWith(sessionRefPrefix) || !oid) continue;
|
|
1652
|
+
sessions[ref.slice(sessionRefPrefix.length)] = oid;
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
_scanDone({ sessions: Object.keys(sessions).length });
|
|
1656
|
+
const _scanMs = Date.now() - _t0;
|
|
1491
1657
|
const results = [], deadLetters = [], placements = [], splits = [], globalsOut = [];
|
|
1658
|
+
// BATCH THE MAIN PUSH: every session folds onto local main in the loop, then ONE commitAndPush seals the
|
|
1659
|
+
// whole cycle (instead of one receive-pack per session). `consumed` are the session branches we processed
|
|
1660
|
+
// and may delete AFTER the batch push lands — a failed batch leaves them for re-admit (idempotent by id).
|
|
1661
|
+
const consumed = []; let totalAdmitted = 0;
|
|
1492
1662
|
// §5.4 (slice 4): on the COORDINATOR of a taxonomy that declares `.global(...)`
|
|
1493
1663
|
// reference data, every admitted intent touching a global aggregate is surfaced
|
|
1494
1664
|
// to the HOST, which replicates the SAME sealed bytes into each shard chain.
|
|
@@ -1518,9 +1688,27 @@ export async function admitAll(eng, ws, ledger, { refuseDomains = [], receiptFen
|
|
|
1518
1688
|
const headBefore = summaryCtx ? await git.resolveRef({ fs: eng.fs, gitdir, ref: BRANCH }).catch(() => null) : null;
|
|
1519
1689
|
for (const [cid, head] of Object.entries(sessions)) {
|
|
1520
1690
|
const ref = `refs/heads/session/${cid}`;
|
|
1691
|
+
const shortCid = cid.length > 14 ? `${cid.slice(0, 14)}...` : cid;
|
|
1692
|
+
const _sessionDone = _span(`session ${shortCid}`, "csession", 1, { client: cid });
|
|
1521
1693
|
try {
|
|
1522
1694
|
await git.addRemote({ fs: eng.fs, gitdir, remote: "origin", url: ledger.remote, force: true });
|
|
1523
|
-
|
|
1695
|
+
const _fetchDone = _span(`fetch ${shortCid}`, "cfetch", 2, { client: cid });
|
|
1696
|
+
const _f = Date.now();
|
|
1697
|
+
try {
|
|
1698
|
+
// THIN-FETCH (the 40s→0.5s fix): a `singleBranch` fetch sets isomorphic-git's have-set to
|
|
1699
|
+
// [the fetched ref] ONLY. The edge holds no local session ref yet ⇒ it offers ZERO haves ⇒ the
|
|
1700
|
+
// server re-ships the ENTIRE history (O(chain): measured ~10MB/40s for a 1-commit delta).
|
|
1701
|
+
// Pre-seed the local session ref to OUR main head so the negotiation offers main as a have and the
|
|
1702
|
+
// server sends only the delta above the common ancestor (~1KB/0.5s). main is append-only, so the
|
|
1703
|
+
// client's fork point is always an ancestor of our head — the thin pack is always sound.
|
|
1704
|
+
const localMain = await git.resolveRef({ fs: eng.fs, gitdir, ref: BRANCH }).catch(() => null);
|
|
1705
|
+
if (localMain) { try { await git.writeRef({ fs: eng.fs, gitdir, ref, value: localMain, force: true }); } catch {} }
|
|
1706
|
+
await git.fetch({ fs: eng.fs, http, gitdir, remote: "origin", ref, singleBranch: true, headers: ledger.headers });
|
|
1707
|
+
} finally {
|
|
1708
|
+
_fetchMs += Date.now() - _f;
|
|
1709
|
+
_fetchDone();
|
|
1710
|
+
}
|
|
1711
|
+
const _walkDone = _span(`walk ${shortCid}`, "cwalk", 2, { client: cid });
|
|
1524
1712
|
const main = await mainIntentIds(eng, ws);
|
|
1525
1713
|
const chain = [];
|
|
1526
1714
|
for (const c of await git.log({ fs: eng.fs, gitdir, ref: head, depth: 100 })) {
|
|
@@ -1528,7 +1716,9 @@ export async function admitAll(eng, ws, ledger, { refuseDomains = [], receiptFen
|
|
|
1528
1716
|
chain.push(c.oid);
|
|
1529
1717
|
}
|
|
1530
1718
|
chain.reverse(); // oldest → newest
|
|
1719
|
+
_walkDone({ intents: chain.length });
|
|
1531
1720
|
const admitted = [], rejected = [], skipped = [];
|
|
1721
|
+
const _gateDone = _span(`gate+fold ${shortCid}`, "cgate", 2, { client: cid, intents: chain.length });
|
|
1532
1722
|
for (const oid of chain) {
|
|
1533
1723
|
try {
|
|
1534
1724
|
const { blob } = await git.readBlob({ fs: eng.fs, gitdir, oid, filepath: "intent.json" });
|
|
@@ -1541,14 +1731,6 @@ export async function admitAll(eng, ws, ledger, { refuseDomains = [], receiptFen
|
|
|
1541
1731
|
rejected.push({ oid: oid.slice(0, 10), error: `session lane refuses ${dom ? `'${dom}'` : "non-authored"} intents — law deploys go through POST /v1/workspaces/${ws}/domains with the workspace secret` });
|
|
1542
1732
|
continue;
|
|
1543
1733
|
}
|
|
1544
|
-
// LAW-FENCE part 2: the domain must be DEPLOYED to this workspace. An
|
|
1545
|
-
// undeployed domain's write is the canonical LEGITIMATE JAM — dead-letter.
|
|
1546
|
-
if (!knownDomains(eng).has(dom)) {
|
|
1547
|
-
const error = `domain '${dom}' is not deployed to this workspace — deploy its law (POST /v1/workspaces/${ws}/domains with the workspace secret), then POST /v1/workspaces/${ws}/dead-letters/retry`;
|
|
1548
|
-
rejected.push({ oid: oid.slice(0, 10), error, deadLettered: true });
|
|
1549
|
-
deadLetters.push({ intentId, oid: oid.slice(0, 10), session: cid, error, blob, domain: dom, directiveId: dirId });
|
|
1550
|
-
continue;
|
|
1551
|
-
}
|
|
1552
1734
|
// THE RECEIPT FENCE (capability_marketplace.md slice 1.5 — the ratified
|
|
1553
1735
|
// authz condition): a capability RECEIPT (complete/fail/block/deadLetter)
|
|
1554
1736
|
// is an EXECUTOR/OWNER write — the open session lane never folds one, so
|
|
@@ -1685,11 +1867,49 @@ export async function admitAll(eng, ws, ledger, { refuseDomains = [], receiptFen
|
|
|
1685
1867
|
rejected.push({ oid: oid.slice(0, 10), error: String(e).slice(0, 200) });
|
|
1686
1868
|
}
|
|
1687
1869
|
}
|
|
1688
|
-
|
|
1689
|
-
|
|
1870
|
+
_gateDone({ admitted: admitted.length, rejected: rejected.length, skipped: skipped.length });
|
|
1871
|
+
// FOLDED onto local main; the receive-pack is BATCHED below (one per cycle, not one per session).
|
|
1872
|
+
// Record the branch as consumed so it's deleted only AFTER the batch push lands.
|
|
1873
|
+
_intents += admitted.length;
|
|
1874
|
+
totalAdmitted += admitted.length;
|
|
1875
|
+
consumed.push({ cid, ref });
|
|
1690
1876
|
results.push({ session: cid, admitted, rejected, ...(skipped.length ? { skipped } : {}) });
|
|
1691
1877
|
} catch (e) {
|
|
1692
1878
|
results.push({ session: cid, error: String(e).slice(0, 200) });
|
|
1879
|
+
} finally {
|
|
1880
|
+
_sessionDone();
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
// THE BATCH SEAL — one receive-pack for the whole admit cycle. A failed push unmounts (engine.unmount drops
|
|
1884
|
+
// the ws fs); we then SKIP the branch deletes so every offered intent re-admits next cycle (idempotent by
|
|
1885
|
+
// id). On success, delete the consumed session branches (best-effort cleanup; a stale branch is a harmless
|
|
1886
|
+
// re-fetch). Single-client workspaces were already one push/cycle; this collapses collaborative bursts.
|
|
1887
|
+
// THE BAILIFF SEAM: when the HOST asked us to `defer` (the open session admit lane on a
|
|
1888
|
+
// non-coordinator workspace), we DO NOT push or delete consumed branches inline — the fold
|
|
1889
|
+
// is already on resident main (truth), and the host backs it up to custody off the critical
|
|
1890
|
+
// path (engine.backupToCustody, debounced + coalesced). This is sound because the SESSION
|
|
1891
|
+
// BRANCHES are themselves durable in custody (the durable inbox) and admit is idempotent by
|
|
1892
|
+
// intent id: a crash before backup just re-derives the SAME sealed main from the same offers.
|
|
1893
|
+
// Coordinator/sharded admits (any §5.2/§5.4/placement/split follow-up) stay durable-before-ack.
|
|
1894
|
+
const hasCoordination = !!(summaryCtx || placements.length || splits.length || globalsOut.length);
|
|
1895
|
+
const deferDurability = defer && !hasCoordination;
|
|
1896
|
+
let _sealed = true;
|
|
1897
|
+
if (totalAdmitted && !deferDurability) {
|
|
1898
|
+
const _sealDone = _span(`seal (batch)`, "cseal", 1, { sessions: consumed.length, admitted: totalAdmitted });
|
|
1899
|
+
const _s = Date.now();
|
|
1900
|
+
try {
|
|
1901
|
+
await commitAndPush(eng, ws, ledger);
|
|
1902
|
+
} catch (e) {
|
|
1903
|
+
_sealed = false;
|
|
1904
|
+
log("admit-batch-push-failed", { ws, sessions: consumed.length, admitted: totalAdmitted, error: String(e).slice(0, 200) });
|
|
1905
|
+
} finally {
|
|
1906
|
+
_sealMs += Date.now() - _s;
|
|
1907
|
+
_sealDone();
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
if (_sealed && !deferDurability) {
|
|
1911
|
+
for (const { ref } of consumed) {
|
|
1912
|
+
try { await pushVia(ledger, () => git.push({ fs: eng.fs, http, gitdir, url: ledger.remote, ref: BRANCH, remoteRef: ref, delete: true, headers: ledger.headers })); } catch {}
|
|
1693
1913
|
}
|
|
1694
1914
|
}
|
|
1695
1915
|
const newMain = await git.resolveRef({ fs: eng.fs, gitdir, ref: BRANCH }).catch(() => null);
|
|
@@ -1697,15 +1917,42 @@ export async function admitAll(eng, ws, ledger, { refuseDomains = [], receiptFen
|
|
|
1697
1917
|
// The coalesced §5.2 delta of this batch (null when no scoped read moved): the HOST
|
|
1698
1918
|
// closes it against the coordinator's recorded frontier and authors the Order.
|
|
1699
1919
|
const summary = summaryCtx ? summaryOfCapture(summaryCtx, headBefore, newMain) : null;
|
|
1920
|
+
const _total = Date.now() - _t0;
|
|
1921
|
+
// EDGE FLAME SPANS: scan refs → fetch session branches → gate+fold each intent → seal to custody.
|
|
1922
|
+
const timings = { totalMs: _total, scanMs: _scanMs, fetchMs: _fetchMs, sealMs: _sealMs, gateFoldMs: Math.max(0, _total - _scanMs - _fetchMs - _sealMs), intents: _intents, sessions: results.length, spans: _spans };
|
|
1700
1923
|
return {
|
|
1701
|
-
sessions: results, main: newMain, deadLetters,
|
|
1924
|
+
sessions: results, main: newMain, deadLetters, timings,
|
|
1702
1925
|
...(summary ? { summary } : {}),
|
|
1703
1926
|
...(placements.length ? { placements } : {}),
|
|
1704
1927
|
...(splits.length ? { splits } : {}),
|
|
1705
1928
|
...(globalsOut.length ? { globals: globalsOut } : {}),
|
|
1929
|
+
// The host's backup lane consumes these: `deferred` ⇒ resident main carries totalAdmitted
|
|
1930
|
+
// un-backed-up commits and `consumed` are the session branches to delete AFTER the push lands.
|
|
1931
|
+
...(deferDurability && totalAdmitted ? { deferred: true, consumed, admitted: totalAdmitted } : {}),
|
|
1706
1932
|
};
|
|
1707
1933
|
}
|
|
1708
1934
|
|
|
1935
|
+
// THE BAILIFF'S BACKUP — push resident main to custody when quiet, NEVER on a write's critical
|
|
1936
|
+
// path and NEVER destructive. Unlike commitAndPush (durability-before-ack: unmounts on failure),
|
|
1937
|
+
// a failed backup leaves the resident holon mounted and the session branches intact, so the host
|
|
1938
|
+
// just retries on the next quiet tick. Append-only main means an off-lock push of an older head
|
|
1939
|
+
// concurrent with a fresh fold is always a sound fast-forward subset (the newer head backs up next).
|
|
1940
|
+
export async function backupToCustody(eng, ws, ledger, { consumed = [] } = {}) {
|
|
1941
|
+
if (!eng.mounted.has(ws)) return { ok: false, error: "not mounted" };
|
|
1942
|
+
const gitdir = gitdirOf(ws);
|
|
1943
|
+
let head = null;
|
|
1944
|
+
try { head = await git.resolveRef({ fs: eng.fs, gitdir, ref: BRANCH }); } catch { return { ok: false, error: "no local main" }; }
|
|
1945
|
+
try {
|
|
1946
|
+
await pushVia(ledger, () => git.push({ fs: eng.fs, http, gitdir, url: ledger.remote, ref: BRANCH, remoteRef: BRANCH, force: false, headers: ledger.headers }));
|
|
1947
|
+
} catch (e) {
|
|
1948
|
+
return { ok: false, error: String(e).slice(0, 200), head };
|
|
1949
|
+
}
|
|
1950
|
+
for (const { ref } of consumed) {
|
|
1951
|
+
try { await pushVia(ledger, () => git.push({ fs: eng.fs, http, gitdir, url: ledger.remote, ref: BRANCH, remoteRef: ref, delete: true, headers: ledger.headers })); } catch {}
|
|
1952
|
+
}
|
|
1953
|
+
return { ok: true, head };
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1709
1956
|
/**
|
|
1710
1957
|
* VERIFY AN UPLOADED LEDGER — "the githolon executes and thus validates it". Fresh-mounts
|
|
1711
1958
|
* the workspace from custody (never a cached mount — the bytes under judgment are the
|
|
@@ -1755,8 +2002,8 @@ export async function archiveRefusedMain(eng, ws, ledger, label) {
|
|
|
1755
2002
|
if (!m.restored) { unmount(eng, ws); return { archived: false, error: "no main to archive" }; }
|
|
1756
2003
|
const gitdir = gitdirOf(ws);
|
|
1757
2004
|
try {
|
|
1758
|
-
await git.push({ fs: eng.fs, http, gitdir, url: ledger.remote, ref: BRANCH, remoteRef: `refs/heads/refused/${label}`, force: true, headers: ledger.headers });
|
|
1759
|
-
await git.push({ fs: eng.fs, http, gitdir, url: ledger.remote, ref: BRANCH, remoteRef: `refs/heads/${BRANCH}`, delete: true, headers: ledger.headers });
|
|
2005
|
+
await pushVia(ledger, () => git.push({ fs: eng.fs, http, gitdir, url: ledger.remote, ref: BRANCH, remoteRef: `refs/heads/refused/${label}`, force: true, headers: ledger.headers }));
|
|
2006
|
+
await pushVia(ledger, () => git.push({ fs: eng.fs, http, gitdir, url: ledger.remote, ref: BRANCH, remoteRef: `refs/heads/${BRANCH}`, delete: true, headers: ledger.headers }));
|
|
1760
2007
|
return { archived: true, ref: `refs/heads/refused/${label}` };
|
|
1761
2008
|
} finally {
|
|
1762
2009
|
unmount(eng, ws);
|
|
@@ -1781,32 +2028,8 @@ export async function deploy(eng, ws, ledger, usda, domainHash, dispositions) {
|
|
|
1781
2028
|
// adjudicates the stable-id diff against. Absent for the common (non-destructive)
|
|
1782
2029
|
// upgrade; the gate's typed refusal names the remedy when one is needed.
|
|
1783
2030
|
const result = author(eng, ws, "nomos", "installDomain", installPayload(domainHash, usda, eng.installedBy, dispositions), eng.hashes.nomos);
|
|
1784
|
-
//
|
|
1785
|
-
//
|
|
1786
|
-
// (`NomosShardIdentity`, law-state in the shard's own chain). The worker is the
|
|
1787
|
-
// bailiff: it records the CUSTODY fact (this workspace's name ⇒ its shard label)
|
|
1788
|
-
// through the chain's own gate right after the law lands — attributed, replayable,
|
|
1789
|
-
// idempotent (an Ensure; re-deploys re-assert the same state). Coordinator ⇒ label ""
|
|
1790
|
-
// (no homing claim — the invariant holds vacuously there).
|
|
1791
|
-
try {
|
|
1792
|
-
eng.directiveRoutesCache = null; // the just-staged overlay may add routes
|
|
1793
|
-
const routed = new Set([...directiveRoutes(eng).keys()].map((k) => k.split(" ")[0]));
|
|
1794
|
-
if (routed.size) {
|
|
1795
|
-
const m = ws.match(/^(.*)--(s\d+)$/);
|
|
1796
|
-
const label = m ? m[2] : "";
|
|
1797
|
-
const coordinator = m ? m[1] : ws;
|
|
1798
|
-
const existing = qById(eng, ws, "nomos-shard-identity:self");
|
|
1799
|
-
const current = existing.length ? existing[0].data : null;
|
|
1800
|
-
if (!current || current.label !== label || current.coordinator !== coordinator) {
|
|
1801
|
-
for (const dom of routed) {
|
|
1802
|
-
try {
|
|
1803
|
-
const declared = author(eng, ws, dom, "nomosDeclareShardIdentity", { label, coordinator, declaredAt: new Date().toISOString() }, domainHash);
|
|
1804
|
-
if (declared && declared.ok !== false) break; // one declaration IS the workspace's identity
|
|
1805
|
-
} catch {} // a routed domain owned by an earlier law: its own deploy declared it
|
|
1806
|
-
}
|
|
1807
|
-
}
|
|
1808
|
-
}
|
|
1809
|
-
} catch {}
|
|
2031
|
+
// Shard identity used to be inferred from host-side taxonomy. That is retired:
|
|
2032
|
+
// if sharding needs declaration, the holon must derive and author it itself.
|
|
1810
2033
|
await commitAndPush(eng, ws, ledger);
|
|
1811
2034
|
return { head: result.head, installation: qById(eng, ws, `domain-installation:${domainHash}`) };
|
|
1812
2035
|
}
|
|
@@ -1823,6 +2046,55 @@ export function stats(eng) {
|
|
|
1823
2046
|
};
|
|
1824
2047
|
}
|
|
1825
2048
|
|
|
2049
|
+
export async function ledgerStats(eng, ws, ledger = null) {
|
|
2050
|
+
const gitdir = gitdirOf(ws);
|
|
2051
|
+
let head = await git.resolveRef({ fs: eng.fs, gitdir, ref: BRANCH }).catch(() => null);
|
|
2052
|
+
if (!head && ledger?.remote) {
|
|
2053
|
+
try {
|
|
2054
|
+
const info = await git.getRemoteInfo({ http, url: ledger.remote, headers: ledger.headers });
|
|
2055
|
+
const remoteMain = (info.refs && info.refs.heads && info.refs.heads[BRANCH]) || null;
|
|
2056
|
+
if (!remoteMain) return { head: null, commits: 0 };
|
|
2057
|
+
await git.init({ fs: eng.fs, gitdir, bare: true, defaultBranch: BRANCH }).catch(() => {});
|
|
2058
|
+
await git.addRemote({ fs: eng.fs, gitdir, remote: "origin", url: ledger.remote, force: true });
|
|
2059
|
+
await git.fetch({ fs: eng.fs, http, gitdir, remote: "origin", ref: BRANCH, singleBranch: true, headers: ledger.headers });
|
|
2060
|
+
await git.writeRef({ fs: eng.fs, gitdir, ref: `refs/heads/${BRANCH}`, value: remoteMain, force: true });
|
|
2061
|
+
await git.writeRef({ fs: eng.fs, gitdir, ref: "HEAD", value: `ref: refs/heads/${BRANCH}`, force: true, symbolic: true });
|
|
2062
|
+
head = await git.resolveRef({ fs: eng.fs, gitdir, ref: BRANCH }).catch(() => null);
|
|
2063
|
+
const mounted = eng.mounted.get(ws);
|
|
2064
|
+
if (mounted) {
|
|
2065
|
+
mounted.restored = true;
|
|
2066
|
+
mounted.remoteMain = remoteMain;
|
|
2067
|
+
mounted.restoreError = null;
|
|
2068
|
+
}
|
|
2069
|
+
} catch {}
|
|
2070
|
+
}
|
|
2071
|
+
if (!head) return { head: null, commits: 0 };
|
|
2072
|
+
const mounted = eng.mounted.get(ws);
|
|
2073
|
+
if (mounted?.ledgerStats?.head === head) return mounted.ledgerStats;
|
|
2074
|
+
const commits = (await git.log({ fs: eng.fs, gitdir, ref: head, depth: 10000000 })).length;
|
|
2075
|
+
const out = { head, commits };
|
|
2076
|
+
if (mounted) mounted.ledgerStats = out;
|
|
2077
|
+
return out;
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
export async function countRemoteCommits(ledger) {
|
|
2081
|
+
const info = await git.getRemoteInfo({ http, url: ledger.remote, headers: ledger.headers });
|
|
2082
|
+
const head = (info.refs && info.refs.heads && info.refs.heads[BRANCH]) || null;
|
|
2083
|
+
if (!head) return { head: null, commits: 0 };
|
|
2084
|
+
const root = new Map();
|
|
2085
|
+
root.set("count", new Directory(new Map()));
|
|
2086
|
+
const preopen = new PreopenDirectory("/work", root);
|
|
2087
|
+
const fs = makeGitFs(preopen, "/work", { File, Directory });
|
|
2088
|
+
const gitdir = "/work/count/nomos.git";
|
|
2089
|
+
await git.init({ fs, gitdir, bare: true, defaultBranch: BRANCH });
|
|
2090
|
+
await git.addRemote({ fs, gitdir, remote: "origin", url: ledger.remote, force: true });
|
|
2091
|
+
await git.fetch({ fs, http, gitdir, remote: "origin", ref: BRANCH, singleBranch: true, headers: ledger.headers });
|
|
2092
|
+
await git.writeRef({ fs, gitdir, ref: `refs/heads/${BRANCH}`, value: head, force: true });
|
|
2093
|
+
await git.writeRef({ fs, gitdir, ref: "HEAD", value: `ref: refs/heads/${BRANCH}`, force: true, symbolic: true });
|
|
2094
|
+
const commits = (await git.log({ fs, gitdir, ref: head, depth: 10000000 })).length;
|
|
2095
|
+
return { head, commits };
|
|
2096
|
+
}
|
|
2097
|
+
|
|
1826
2098
|
/** Per-branch projection observables (cursor, projectCalls, sqliteBytes — see #37). */
|
|
1827
2099
|
export const projectionStats = (eng, ws) => JSON.parse(call(eng.ex, "projection_stats", ws ? { repoArg: repoArgOf(ws), workspace: ws, branch: BRANCH } : {}, eng.STDERR));
|
|
1828
2100
|
|