knosky 0.6.3 → 0.7.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/CHANGELOG.md +149 -93
- package/CREDITS.md +14 -14
- package/LICENSE.md +76 -76
- package/LIMITATIONS.md +33 -23
- package/PRIVACY.md +30 -30
- package/README.md +170 -117
- package/SECURITY.md +78 -46
- package/action/post-comment.mjs +94 -89
- package/action.yml +62 -62
- package/bin/knosky.mjs +279 -105
- package/core/CONTRACT.md +70 -70
- package/core/append-only-checkpoint.mjs +215 -0
- package/core/audit-writer.mjs +317 -0
- package/core/benchmark-results.mjs +225 -225
- package/core/bundle.mjs +178 -178
- package/core/churn.mjs +23 -23
- package/core/ci.mjs +268 -268
- package/core/comparison.mjs +189 -189
- package/core/config.mjs +189 -189
- package/core/constants.mjs +13 -13
- package/core/contract.mjs +123 -123
- package/core/cross-repo.mjs +111 -111
- package/core/decision-codes.mjs +92 -0
- package/core/destination.mjs +161 -161
- package/core/district-classification.mjs +111 -0
- package/core/doctor-scorecard.mjs +369 -0
- package/core/domain-store.mjs +347 -0
- package/core/edges.mjs +43 -43
- package/core/escalate.mjs +68 -68
- package/core/freshness.mjs +198 -194
- package/core/fs-indexer.mjs +218 -218
- package/core/key-store.mjs +348 -348
- package/core/layout.mjs +46 -46
- package/core/ledger.mjs +176 -141
- package/core/local-ipc-identity.mjs +500 -0
- package/core/lod.mjs +155 -155
- package/core/mode-b.mjs +410 -0
- package/core/multi-model-benchmark.mjs +405 -405
- package/core/net-lockdown.mjs +421 -0
- package/core/onboarding.mjs +223 -223
- package/core/operator-auth.mjs +317 -0
- package/core/overlays.mjs +45 -45
- package/core/policy-lattice.mjs +142 -0
- package/core/pr-comment.mjs +198 -198
- package/core/protocol-spec.mjs +460 -460
- package/core/provenance.mjs +320 -0
- package/core/retrieve.mjs +63 -63
- package/core/route.mjs +304 -304
- package/core/schema.mjs +275 -275
- package/core/signing-tiers.mjs +1265 -0
- package/core/swarm-bench.mjs +106 -0
- package/core/swarm-coordinator.mjs +867 -0
- package/core/trust-root-rekey.mjs +410 -0
- package/mcp/server.mjs +264 -108
- package/package.json +56 -46
- package/renderer/art/kenney/buildingTiles_sheet.xml +130 -130
- package/renderer/art/kenney/cityDetails_sheet.xml +12 -12
- package/renderer/art/kenney/landscapeTiles_sheet.xml +129 -129
- package/renderer/art/kenney/sheet_allCars.xml +545 -545
- package/renderer/build-rich.mjs +43 -43
- package/renderer/city.template.html +808 -808
- package/ssot/decision-codes.json +133 -0
- package/ssot/ladder-l0-l3.md +232 -0
- package/ssot/tool-menu.json +130 -0
package/core/layout.mjs
CHANGED
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
// Deterministic N-category city layout (Phase 2b). Implements locked D3:
|
|
2
|
-
// stable order (category manifest order), each district a square sized to hold its nodes,
|
|
3
|
-
// districts shelf-packed into a roughly square city. Recompute is deterministic (same data -> same layout).
|
|
4
|
-
export function layoutCity(city, opts = {}) {
|
|
5
|
-
const PAD = opts.pad ?? 1; // building-cells of padding inside a district
|
|
6
|
-
const GAP = opts.gap ?? 2; // building-cells between districts
|
|
7
|
-
|
|
8
|
-
const cats = (city.categories || []).slice().sort((a, b) => a.order - b.order);
|
|
9
|
-
const byCat = new Map(cats.map(c => [c.id, []]));
|
|
10
|
-
for (const n of city.nodes || []) {
|
|
11
|
-
if (!byCat.has(n.category)) byCat.set(n.category, []); // tolerate uncategorized
|
|
12
|
-
byCat.get(n.category).push(n);
|
|
13
|
-
}
|
|
14
|
-
// ensure any extra categories (not in manifest) still get a district, appended in id order
|
|
15
|
-
const extra = [...byCat.keys()].filter(id => !cats.find(c => c.id === id)).sort();
|
|
16
|
-
const order = [...cats.map(c => ({ id: c.id, label: c.label, color: c.color })),
|
|
17
|
-
...extra.map(id => ({ id, label: id, color: '#888' }))];
|
|
18
|
-
|
|
19
|
-
const districts = order.map((c, i) => {
|
|
20
|
-
const nodes = byCat.get(c.id) || [];
|
|
21
|
-
const side = Math.max(1, Math.ceil(Math.sqrt(nodes.length || 1)));
|
|
22
|
-
return { id: c.id, label: c.label, color: c.color, idx: i, nodes, side, w: side + PAD * 2, h: side + PAD * 2 };
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
const totalArea = districts.reduce((a, d) => a + d.w * d.h, 0);
|
|
26
|
-
const rowTarget = Math.max(Math.max(...districts.map(d => d.w)), Math.ceil(Math.sqrt(totalArea)));
|
|
27
|
-
|
|
28
|
-
const placed = [];
|
|
29
|
-
let x = 0, y = 0, rowH = 0;
|
|
30
|
-
for (const d of districts) {
|
|
31
|
-
if (x > 0 && x + d.w > rowTarget) { x = 0; y += rowH + GAP; rowH = 0; }
|
|
32
|
-
d.x = x; d.y = y; rowH = Math.max(rowH, d.h);
|
|
33
|
-
d.nodes.forEach((n, i) => {
|
|
34
|
-
placed.push({
|
|
35
|
-
id: n.id, title: n.title, category: n.category, kind: n.kind,
|
|
36
|
-
gx: d.x + PAD + (i % d.side),
|
|
37
|
-
gy: d.y + PAD + Math.floor(i / d.side),
|
|
38
|
-
ref: n.provenance ? n.provenance.ref : null,
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
x += d.w + GAP;
|
|
42
|
-
}
|
|
43
|
-
const gridW = Math.max(1, ...placed.map(p => p.gx + 1), ...districts.map(d => d.x + d.w));
|
|
44
|
-
const gridH = Math.max(1, ...placed.map(p => p.gy + 1), ...districts.map(d => d.y + d.h));
|
|
45
|
-
return { districts, nodes: placed, gridW, gridH };
|
|
46
|
-
}
|
|
1
|
+
// Deterministic N-category city layout (Phase 2b). Implements locked D3:
|
|
2
|
+
// stable order (category manifest order), each district a square sized to hold its nodes,
|
|
3
|
+
// districts shelf-packed into a roughly square city. Recompute is deterministic (same data -> same layout).
|
|
4
|
+
export function layoutCity(city, opts = {}) {
|
|
5
|
+
const PAD = opts.pad ?? 1; // building-cells of padding inside a district
|
|
6
|
+
const GAP = opts.gap ?? 2; // building-cells between districts
|
|
7
|
+
|
|
8
|
+
const cats = (city.categories || []).slice().sort((a, b) => a.order - b.order);
|
|
9
|
+
const byCat = new Map(cats.map(c => [c.id, []]));
|
|
10
|
+
for (const n of city.nodes || []) {
|
|
11
|
+
if (!byCat.has(n.category)) byCat.set(n.category, []); // tolerate uncategorized
|
|
12
|
+
byCat.get(n.category).push(n);
|
|
13
|
+
}
|
|
14
|
+
// ensure any extra categories (not in manifest) still get a district, appended in id order
|
|
15
|
+
const extra = [...byCat.keys()].filter(id => !cats.find(c => c.id === id)).sort();
|
|
16
|
+
const order = [...cats.map(c => ({ id: c.id, label: c.label, color: c.color })),
|
|
17
|
+
...extra.map(id => ({ id, label: id, color: '#888' }))];
|
|
18
|
+
|
|
19
|
+
const districts = order.map((c, i) => {
|
|
20
|
+
const nodes = byCat.get(c.id) || [];
|
|
21
|
+
const side = Math.max(1, Math.ceil(Math.sqrt(nodes.length || 1)));
|
|
22
|
+
return { id: c.id, label: c.label, color: c.color, idx: i, nodes, side, w: side + PAD * 2, h: side + PAD * 2 };
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const totalArea = districts.reduce((a, d) => a + d.w * d.h, 0);
|
|
26
|
+
const rowTarget = Math.max(Math.max(...districts.map(d => d.w)), Math.ceil(Math.sqrt(totalArea)));
|
|
27
|
+
|
|
28
|
+
const placed = [];
|
|
29
|
+
let x = 0, y = 0, rowH = 0;
|
|
30
|
+
for (const d of districts) {
|
|
31
|
+
if (x > 0 && x + d.w > rowTarget) { x = 0; y += rowH + GAP; rowH = 0; }
|
|
32
|
+
d.x = x; d.y = y; rowH = Math.max(rowH, d.h);
|
|
33
|
+
d.nodes.forEach((n, i) => {
|
|
34
|
+
placed.push({
|
|
35
|
+
id: n.id, title: n.title, category: n.category, kind: n.kind,
|
|
36
|
+
gx: d.x + PAD + (i % d.side),
|
|
37
|
+
gy: d.y + PAD + Math.floor(i / d.side),
|
|
38
|
+
ref: n.provenance ? n.provenance.ref : null,
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
x += d.w + GAP;
|
|
42
|
+
}
|
|
43
|
+
const gridW = Math.max(1, ...placed.map(p => p.gx + 1), ...districts.map(d => d.x + d.w));
|
|
44
|
+
const gridH = Math.max(1, ...placed.map(p => p.gy + 1), ...districts.map(d => d.y + d.h));
|
|
45
|
+
return { districts, nodes: placed, gridW, gridH };
|
|
46
|
+
}
|
package/core/ledger.mjs
CHANGED
|
@@ -1,141 +1,176 @@
|
|
|
1
|
-
// KnoSky ledger high-water-mark guard (SAT-443 / V13).
|
|
2
|
-
//
|
|
3
|
-
// Independently persists the highest-seen ledger sequence number in a
|
|
4
|
-
// dedicated file (separate from the ledger itself) so that a truncation
|
|
5
|
-
// attack that replaces the whole ledger cannot roll back the sequence.
|
|
6
|
-
//
|
|
7
|
-
// Anti-truncation rule (D-164 TUF-evolution): a ledger state whose sequence
|
|
8
|
-
// number is STRICTLY LOWER than the persisted high-water mark is refused.
|
|
9
|
-
//
|
|
10
|
-
// The HWM file is a minimal JSON document: { "ledger_hwm": <integer> }
|
|
11
|
-
// It is intentionally kept narrow — no ledger content lives here.
|
|
12
|
-
//
|
|
13
|
-
// Pure Node stdlib, ESM — no third-party dependencies.
|
|
14
|
-
|
|
15
|
-
import { readFileSync, writeFileSync, renameSync, mkdirSync, openSync, fsyncSync, closeSync } from 'node:fs';
|
|
16
|
-
import { dirname } from 'node:path';
|
|
17
|
-
import { MAX_PLAUSIBLE_LEDGER_SEQ } from './constants.mjs';
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* @
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* @param {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
const dirFd = openSync(dir, 'r');
|
|
86
|
-
try {
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
return {
|
|
131
|
-
ok: false,
|
|
132
|
-
seq,
|
|
133
|
-
hwm,
|
|
134
|
-
error: `
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
1
|
+
// KnoSky ledger high-water-mark guard (SAT-443 / V13).
|
|
2
|
+
//
|
|
3
|
+
// Independently persists the highest-seen ledger sequence number in a
|
|
4
|
+
// dedicated file (separate from the ledger itself) so that a truncation
|
|
5
|
+
// attack that replaces the whole ledger cannot roll back the sequence.
|
|
6
|
+
//
|
|
7
|
+
// Anti-truncation rule (D-164 TUF-evolution): a ledger state whose sequence
|
|
8
|
+
// number is STRICTLY LOWER than the persisted high-water mark is refused.
|
|
9
|
+
//
|
|
10
|
+
// The HWM file is a minimal JSON document: { "ledger_hwm": <integer> }
|
|
11
|
+
// It is intentionally kept narrow — no ledger content lives here.
|
|
12
|
+
//
|
|
13
|
+
// Pure Node stdlib, ESM — no third-party dependencies.
|
|
14
|
+
|
|
15
|
+
import { readFileSync, writeFileSync, renameSync, mkdirSync, openSync, fsyncSync, closeSync } from 'node:fs';
|
|
16
|
+
import { dirname } from 'node:path';
|
|
17
|
+
import { MAX_PLAUSIBLE_LEDGER_SEQ } from './constants.mjs';
|
|
18
|
+
import { openCheckpoint, appendCheckpointEntry } from './append-only-checkpoint.mjs';
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// HWM file I/O
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Read the persisted high-water mark from `hwmPath`.
|
|
26
|
+
* Returns 0 when the file does not exist (first run — any sequence is valid).
|
|
27
|
+
* Throws on I/O errors other than ENOENT, and on malformed content (fail-closed).
|
|
28
|
+
*
|
|
29
|
+
* @param {string} hwmPath Path to the independently-persisted HWM file.
|
|
30
|
+
* @returns {number} Stored sequence number (non-negative integer).
|
|
31
|
+
*/
|
|
32
|
+
export function readHwm(hwmPath) {
|
|
33
|
+
let text;
|
|
34
|
+
try {
|
|
35
|
+
text = readFileSync(hwmPath, 'utf8');
|
|
36
|
+
} catch (err) {
|
|
37
|
+
if (err.code === 'ENOENT') return 0;
|
|
38
|
+
throw err; // permission error — fail-closed
|
|
39
|
+
}
|
|
40
|
+
let parsed;
|
|
41
|
+
try {
|
|
42
|
+
parsed = JSON.parse(text);
|
|
43
|
+
} catch {
|
|
44
|
+
throw new Error(`ledger HWM file is not valid JSON: ${hwmPath}`);
|
|
45
|
+
}
|
|
46
|
+
if (!Number.isInteger(parsed.ledger_hwm) || parsed.ledger_hwm < 0) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`ledger HWM file has invalid ledger_hwm: ${JSON.stringify(parsed.ledger_hwm)} in ${hwmPath}`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return parsed.ledger_hwm;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Persist `seq` as the current high-water mark.
|
|
56
|
+
* Creates parent directories if they do not already exist.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} hwmPath Path to write.
|
|
59
|
+
* @param {number} seq New HWM value (non-negative integer).
|
|
60
|
+
*/
|
|
61
|
+
export function writeHwm(hwmPath, seq) {
|
|
62
|
+
if (!Number.isInteger(seq) || seq < 0) {
|
|
63
|
+
throw new TypeError(
|
|
64
|
+
`ledger_hwm must be a non-negative integer, got: ${JSON.stringify(seq)}`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const dir = dirname(hwmPath);
|
|
68
|
+
mkdirSync(dir, { recursive: true });
|
|
69
|
+
// Atomic write: sibling temp + rename (SAT-474).
|
|
70
|
+
const tmp = hwmPath + '.tmp';
|
|
71
|
+
writeFileSync(tmp, JSON.stringify({ ledger_hwm: seq }) + '\n', 'utf8');
|
|
72
|
+
// Best-effort fsync: Windows TEMP often returns EPERM; full write + rename still atomic.
|
|
73
|
+
try {
|
|
74
|
+
const tmpFd = openSync(tmp, 'r');
|
|
75
|
+
try {
|
|
76
|
+
fsyncSync(tmpFd);
|
|
77
|
+
} finally {
|
|
78
|
+
closeSync(tmpFd);
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
/* EPERM / unsupported */
|
|
82
|
+
}
|
|
83
|
+
renameSync(tmp, hwmPath);
|
|
84
|
+
try {
|
|
85
|
+
const dirFd = openSync(dir, 'r');
|
|
86
|
+
try {
|
|
87
|
+
fsyncSync(dirFd);
|
|
88
|
+
} finally {
|
|
89
|
+
closeSync(dirFd);
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
/* best-effort directory fsync (often unsupported on Windows) */
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// checkAndAdvance — the core anti-truncation guard
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Enforce the high-water-mark invariant for incoming ledger state.
|
|
102
|
+
*
|
|
103
|
+
* Reads the persisted HWM from `hwmPath`, then applies the anti-truncation
|
|
104
|
+
* rule:
|
|
105
|
+
* - `seq` STRICTLY LESS than HWM → refuse. The HWM file is NOT updated.
|
|
106
|
+
* - `seq` EQUAL to HWM → accept (idempotent replay). HWM written.
|
|
107
|
+
* - `seq` GREATER than HWM → accept and advance. HWM written.
|
|
108
|
+
*
|
|
109
|
+
* Returning `{ ok: false }` means the caller must treat the incoming ledger
|
|
110
|
+
* state as invalid and stop processing — it must not be applied or surfaced
|
|
111
|
+
* as authoritative.
|
|
112
|
+
*
|
|
113
|
+
* When `checkpointPath` is supplied, every accepted write is also appended to
|
|
114
|
+
* the append-only secondary checkpoint file (SAT-561 / F0.2b). The checkpoint
|
|
115
|
+
* write is best-effort: any error is swallowed so that a checkpoint failure
|
|
116
|
+
* never blocks the primary HWM write. `openCheckpoint` is called once per
|
|
117
|
+
* path on first use (idempotent; no-op on subsequent calls).
|
|
118
|
+
*
|
|
119
|
+
* @param {number} seq Claimed sequence number from the incoming ledger state.
|
|
120
|
+
* @param {string} hwmPath Path to the independently-persisted HWM file.
|
|
121
|
+
* @param {string} [checkpointPath] Optional path to the append-only JSONL checkpoint file.
|
|
122
|
+
* @returns {{ ok: boolean, seq: number, hwm: number, checkpoint_ok: boolean | null, checkpoint_error: string | null, error?: string }}
|
|
123
|
+
*/
|
|
124
|
+
// Second layer of defense: enforced here too (not only in
|
|
125
|
+
// core/freshness.mjs's extractLedgerSeq) so checkAndAdvance is safe even if
|
|
126
|
+
// called directly. Imports the single source of truth from constants.mjs
|
|
127
|
+
// (a dependency-free module) so the two layers cannot silently diverge.
|
|
128
|
+
export function checkAndAdvance(seq, hwmPath, checkpointPath) {
|
|
129
|
+
if (!Number.isInteger(seq) || seq < 0 || seq > MAX_PLAUSIBLE_LEDGER_SEQ) {
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
seq,
|
|
133
|
+
hwm: -1,
|
|
134
|
+
error: `seq must be a non-negative integer no greater than ${MAX_PLAUSIBLE_LEDGER_SEQ}, got: ${JSON.stringify(seq)}`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const hwm = readHwm(hwmPath);
|
|
139
|
+
|
|
140
|
+
if (seq < hwm) {
|
|
141
|
+
// Anti-truncation guard: incoming sequence is below the highest-seen mark.
|
|
142
|
+
// Do NOT update the file — the old (higher) mark must remain authoritative.
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
seq,
|
|
146
|
+
hwm,
|
|
147
|
+
error: `ledger sequence ${seq} is below the high-water mark ${hwm} — refusing (anti-truncation guard)`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// seq >= hwm: advance (or re-confirm) the mark.
|
|
152
|
+
writeHwm(hwmPath, seq);
|
|
153
|
+
|
|
154
|
+
// SAT-561 (F0.2b): secondary append-only checkpoint write — best-effort,
|
|
155
|
+
// never blocks or fails the primary HWM write.
|
|
156
|
+
let checkpoint_ok = null;
|
|
157
|
+
let checkpoint_error = null;
|
|
158
|
+
if (checkpointPath !== undefined && checkpointPath !== null) {
|
|
159
|
+
try {
|
|
160
|
+
openCheckpoint(checkpointPath);
|
|
161
|
+
appendCheckpointEntry(checkpointPath, {
|
|
162
|
+
seq,
|
|
163
|
+
event: 'ledger_state',
|
|
164
|
+
ts: new Date().toISOString(),
|
|
165
|
+
hwm_previously: hwm,
|
|
166
|
+
});
|
|
167
|
+
checkpoint_ok = true;
|
|
168
|
+
} catch (err) {
|
|
169
|
+
// best-effort — checkpoint failure must not affect the primary write
|
|
170
|
+
checkpoint_ok = false;
|
|
171
|
+
checkpoint_error = err.message || null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { ok: true, seq, hwm, checkpoint_ok, checkpoint_error };
|
|
176
|
+
}
|