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/freshness.mjs
CHANGED
|
@@ -1,194 +1,198 @@
|
|
|
1
|
-
// KnoSky ledger-anchored freshness attestation (SAT-444, SAT-474).
|
|
2
|
-
//
|
|
3
|
-
// Closes the clock-skew / key-resurrection gap by basing freshness on the
|
|
4
|
-
// repository's commit count rather than wall-clock time. A `ledger_seq`
|
|
5
|
-
// is a monotone integer — the number of commits reachable from HEAD —
|
|
6
|
-
// which cannot be fabricated by adjusting the system clock and can only
|
|
7
|
-
// decrease when history is rewritten (the ledger-truncation attack caught
|
|
8
|
-
// by the high-water-mark guard in core/ledger.mjs).
|
|
9
|
-
//
|
|
10
|
-
// SAT-474: validateFreshnessWithHwm() routes through the *persisted*
|
|
11
|
-
// checkAndAdvance() guard (core/ledger.mjs) so that the rollback defence
|
|
12
|
-
// survives process restarts. validateFreshness() is retained for callers
|
|
13
|
-
// that manage their own in-memory lastSeq (e.g. protocol-spec validation).
|
|
14
|
-
// Pure Node stdlib, ESM — no third-party dependencies.
|
|
15
|
-
|
|
16
|
-
import { execFileSync } from 'node:child_process';
|
|
17
|
-
import { checkAndAdvance } from './ledger.mjs';
|
|
18
|
-
import { MAX_PLAUSIBLE_LEDGER_SEQ } from './constants.mjs';
|
|
19
|
-
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
// extractLedgerSeq — read the ledger_seq stored in a city envelope
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Extract the `ledger_seq` from a city envelope or any artifact that carries
|
|
26
|
-
* one. Returns null when absent, not a non-negative integer, or implausibly
|
|
27
|
-
* large (see MAX_PLAUSIBLE_LEDGER_SEQ).
|
|
28
|
-
*
|
|
29
|
-
* @param {object} obj City envelope or comparable artifact object.
|
|
30
|
-
* @returns {number|null}
|
|
31
|
-
*/
|
|
32
|
-
export function extractLedgerSeq(obj) {
|
|
33
|
-
if (!obj || typeof obj !== 'object') return null;
|
|
34
|
-
const seq = obj.ledger_seq;
|
|
35
|
-
if (typeof seq !== 'number' || !Number.isInteger(seq) || seq < 0) return null;
|
|
36
|
-
if (seq > MAX_PLAUSIBLE_LEDGER_SEQ) return null;
|
|
37
|
-
return seq;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
// computeLedgerSeq — derive a ledger_seq from a git repo on disk
|
|
42
|
-
// ---------------------------------------------------------------------------
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Compute the ledger_seq for `root` by counting all commits reachable from
|
|
46
|
-
* HEAD (`git rev-list --count HEAD`). Returns 0 when git is unavailable or
|
|
47
|
-
* the directory has no commit history (e.g. a brand-new repo with no commits).
|
|
48
|
-
*
|
|
49
|
-
* @param {string} root Absolute path to the git working tree.
|
|
50
|
-
* @returns {number} Non-negative integer commit count.
|
|
51
|
-
*/
|
|
52
|
-
export function computeLedgerSeq(root) {
|
|
53
|
-
try {
|
|
54
|
-
const out = execFileSync(
|
|
55
|
-
'git',
|
|
56
|
-
['rev-list', '--count', 'HEAD'],
|
|
57
|
-
{ cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] },
|
|
58
|
-
).trim();
|
|
59
|
-
const n = parseInt(out, 10);
|
|
60
|
-
return Number.isFinite(n) && n >= 0 ? n : 0;
|
|
61
|
-
} catch {
|
|
62
|
-
// No git / no commits / non-repo directory → 0 (safe: sequence starts fresh)
|
|
63
|
-
return 0;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// ---------------------------------------------------------------------------
|
|
68
|
-
// checkHighWaterMark — V13 guard against ledger-truncation attack
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* High-water-mark guard (V13).
|
|
73
|
-
*
|
|
74
|
-
* Returns `{ ok: true }` when `newSeq` is strictly greater than `lastSeq`,
|
|
75
|
-
* meaning the city has advanced — its ledger has grown (or this is the first
|
|
76
|
-
* load, indicated by `lastSeq === null`).
|
|
77
|
-
*
|
|
78
|
-
* Returns `{ ok: false, reason }` when `newSeq` is ≤ `lastSeq`, which means
|
|
79
|
-
* the ledger has been truncated or replayed — a potential key-resurrection or
|
|
80
|
-
* rollback attack. Callers MUST treat stale/equal as suspicious.
|
|
81
|
-
*
|
|
82
|
-
* @param {number|null} lastSeq Previously accepted ledger_seq, or null on
|
|
83
|
-
* first load (unconditionally accepted).
|
|
84
|
-
* @param {number|null} newSeq Ledger_seq from the incoming artifact, or
|
|
85
|
-
* null when absent (treated as 0 — conservative).
|
|
86
|
-
* @returns {{ ok: boolean, reason?: string }}
|
|
87
|
-
*/
|
|
88
|
-
export function checkHighWaterMark(lastSeq, newSeq) {
|
|
89
|
-
// Treat missing seq as 0 — conservative: an artifact with no ledger anchoring
|
|
90
|
-
// is as trustworthy as a brand-new repo.
|
|
91
|
-
const effective = (typeof newSeq === 'number' && Number.isInteger(newSeq) && newSeq >= 0)
|
|
92
|
-
? newSeq
|
|
93
|
-
: 0;
|
|
94
|
-
|
|
95
|
-
// First load: no last seq to compare against — unconditionally accept.
|
|
96
|
-
if (lastSeq === null || lastSeq === undefined) {
|
|
97
|
-
return { ok: true };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (typeof lastSeq !== 'number' || !Number.isInteger(lastSeq) || lastSeq < 0) {
|
|
101
|
-
// Corrupt lastSeq — treat it as 0 (reset the watermark)
|
|
102
|
-
return { ok: true };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (effective > lastSeq) {
|
|
106
|
-
return { ok: true };
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// equal: rolled back to same point (replay) or no new commits
|
|
110
|
-
// less: ledger truncation — history was rewritten
|
|
111
|
-
const direction = effective < lastSeq ? 'ledger truncated' : 'ledger not advanced';
|
|
112
|
-
return {
|
|
113
|
-
ok: false,
|
|
114
|
-
reason:
|
|
115
|
-
direction +
|
|
116
|
-
': incoming ledger_seq ' + effective +
|
|
117
|
-
' is not greater than last accepted ' + lastSeq +
|
|
118
|
-
' — possible rollback or key-resurrection attack',
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// ---------------------------------------------------------------------------
|
|
123
|
-
// validateFreshness — full attestation check for an incoming artifact
|
|
124
|
-
// ---------------------------------------------------------------------------
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Validate the freshness of an incoming artifact against an optional stored
|
|
128
|
-
* high-water mark.
|
|
129
|
-
*
|
|
130
|
-
* Combines:
|
|
131
|
-
* (1) structural check: `ledger_seq` is present and a non-negative integer
|
|
132
|
-
* (2) V13 high-water-mark guard: `ledger_seq` must exceed the last accepted
|
|
133
|
-
* value (pass `null` on first load to skip this check)
|
|
134
|
-
*
|
|
135
|
-
* @param {object} artifact City envelope or protocol artifact.
|
|
136
|
-
* @param {number|null} lastSeq Last accepted ledger_seq (null = first load).
|
|
137
|
-
* @returns {{ ok: boolean, ledger_seq: number|null, errors: string[] }}
|
|
138
|
-
*/
|
|
139
|
-
export function validateFreshness(artifact, lastSeq = null) {
|
|
140
|
-
const errors = [];
|
|
141
|
-
const seq = extractLedgerSeq(artifact);
|
|
142
|
-
|
|
143
|
-
if (seq === null) {
|
|
144
|
-
errors.push('ledger_seq is missing or not a non-negative integer');
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const hwm = checkHighWaterMark(lastSeq, seq);
|
|
148
|
-
if (!hwm.ok) {
|
|
149
|
-
errors.push(hwm.reason);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return { ok: errors.length === 0, ledger_seq: seq, errors };
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// ---------------------------------------------------------------------------
|
|
156
|
-
// validateFreshnessWithHwm — persisted-HWM variant (SAT-474)
|
|
157
|
-
// ---------------------------------------------------------------------------
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Validate freshness through the **persisted** high-water-mark guard
|
|
161
|
-
* (`core/ledger.mjs` `checkAndAdvance`).
|
|
162
|
-
*
|
|
163
|
-
* Unlike `validateFreshness`, this function reads and updates the HWM from
|
|
164
|
-
* `hwmPath` on disk, so the rollback defence is durable across process
|
|
165
|
-
* restarts. Callers that previously maintained an in-memory `lastSeq` and
|
|
166
|
-
* passed it to `validateFreshness` should migrate to this function and a
|
|
167
|
-
* stable `hwmPath` in their data directory.
|
|
168
|
-
*
|
|
169
|
-
* Semantics of the persisted guard (from `checkAndAdvance`):
|
|
170
|
-
* - seq STRICTLY LESS than HWM → rejected (anti-truncation guard)
|
|
171
|
-
* - seq EQUAL to HWM → accepted (idempotent replay)
|
|
172
|
-
* - seq GREATER than HWM → accepted and HWM advanced
|
|
173
|
-
*
|
|
174
|
-
* @param {object} artifact
|
|
175
|
-
* @param {string} hwmPath
|
|
176
|
-
* @
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
1
|
+
// KnoSky ledger-anchored freshness attestation (SAT-444, SAT-474).
|
|
2
|
+
//
|
|
3
|
+
// Closes the clock-skew / key-resurrection gap by basing freshness on the
|
|
4
|
+
// repository's commit count rather than wall-clock time. A `ledger_seq`
|
|
5
|
+
// is a monotone integer — the number of commits reachable from HEAD —
|
|
6
|
+
// which cannot be fabricated by adjusting the system clock and can only
|
|
7
|
+
// decrease when history is rewritten (the ledger-truncation attack caught
|
|
8
|
+
// by the high-water-mark guard in core/ledger.mjs).
|
|
9
|
+
//
|
|
10
|
+
// SAT-474: validateFreshnessWithHwm() routes through the *persisted*
|
|
11
|
+
// checkAndAdvance() guard (core/ledger.mjs) so that the rollback defence
|
|
12
|
+
// survives process restarts. validateFreshness() is retained for callers
|
|
13
|
+
// that manage their own in-memory lastSeq (e.g. protocol-spec validation).
|
|
14
|
+
// Pure Node stdlib, ESM — no third-party dependencies.
|
|
15
|
+
|
|
16
|
+
import { execFileSync } from 'node:child_process';
|
|
17
|
+
import { checkAndAdvance } from './ledger.mjs';
|
|
18
|
+
import { MAX_PLAUSIBLE_LEDGER_SEQ } from './constants.mjs';
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// extractLedgerSeq — read the ledger_seq stored in a city envelope
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Extract the `ledger_seq` from a city envelope or any artifact that carries
|
|
26
|
+
* one. Returns null when absent, not a non-negative integer, or implausibly
|
|
27
|
+
* large (see MAX_PLAUSIBLE_LEDGER_SEQ).
|
|
28
|
+
*
|
|
29
|
+
* @param {object} obj City envelope or comparable artifact object.
|
|
30
|
+
* @returns {number|null}
|
|
31
|
+
*/
|
|
32
|
+
export function extractLedgerSeq(obj) {
|
|
33
|
+
if (!obj || typeof obj !== 'object') return null;
|
|
34
|
+
const seq = obj.ledger_seq;
|
|
35
|
+
if (typeof seq !== 'number' || !Number.isInteger(seq) || seq < 0) return null;
|
|
36
|
+
if (seq > MAX_PLAUSIBLE_LEDGER_SEQ) return null;
|
|
37
|
+
return seq;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// computeLedgerSeq — derive a ledger_seq from a git repo on disk
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Compute the ledger_seq for `root` by counting all commits reachable from
|
|
46
|
+
* HEAD (`git rev-list --count HEAD`). Returns 0 when git is unavailable or
|
|
47
|
+
* the directory has no commit history (e.g. a brand-new repo with no commits).
|
|
48
|
+
*
|
|
49
|
+
* @param {string} root Absolute path to the git working tree.
|
|
50
|
+
* @returns {number} Non-negative integer commit count.
|
|
51
|
+
*/
|
|
52
|
+
export function computeLedgerSeq(root) {
|
|
53
|
+
try {
|
|
54
|
+
const out = execFileSync(
|
|
55
|
+
'git',
|
|
56
|
+
['rev-list', '--count', 'HEAD'],
|
|
57
|
+
{ cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] },
|
|
58
|
+
).trim();
|
|
59
|
+
const n = parseInt(out, 10);
|
|
60
|
+
return Number.isFinite(n) && n >= 0 ? n : 0;
|
|
61
|
+
} catch {
|
|
62
|
+
// No git / no commits / non-repo directory → 0 (safe: sequence starts fresh)
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// checkHighWaterMark — V13 guard against ledger-truncation attack
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* High-water-mark guard (V13).
|
|
73
|
+
*
|
|
74
|
+
* Returns `{ ok: true }` when `newSeq` is strictly greater than `lastSeq`,
|
|
75
|
+
* meaning the city has advanced — its ledger has grown (or this is the first
|
|
76
|
+
* load, indicated by `lastSeq === null`).
|
|
77
|
+
*
|
|
78
|
+
* Returns `{ ok: false, reason }` when `newSeq` is ≤ `lastSeq`, which means
|
|
79
|
+
* the ledger has been truncated or replayed — a potential key-resurrection or
|
|
80
|
+
* rollback attack. Callers MUST treat stale/equal as suspicious.
|
|
81
|
+
*
|
|
82
|
+
* @param {number|null} lastSeq Previously accepted ledger_seq, or null on
|
|
83
|
+
* first load (unconditionally accepted).
|
|
84
|
+
* @param {number|null} newSeq Ledger_seq from the incoming artifact, or
|
|
85
|
+
* null when absent (treated as 0 — conservative).
|
|
86
|
+
* @returns {{ ok: boolean, reason?: string }}
|
|
87
|
+
*/
|
|
88
|
+
export function checkHighWaterMark(lastSeq, newSeq) {
|
|
89
|
+
// Treat missing seq as 0 — conservative: an artifact with no ledger anchoring
|
|
90
|
+
// is as trustworthy as a brand-new repo.
|
|
91
|
+
const effective = (typeof newSeq === 'number' && Number.isInteger(newSeq) && newSeq >= 0)
|
|
92
|
+
? newSeq
|
|
93
|
+
: 0;
|
|
94
|
+
|
|
95
|
+
// First load: no last seq to compare against — unconditionally accept.
|
|
96
|
+
if (lastSeq === null || lastSeq === undefined) {
|
|
97
|
+
return { ok: true };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (typeof lastSeq !== 'number' || !Number.isInteger(lastSeq) || lastSeq < 0) {
|
|
101
|
+
// Corrupt lastSeq — treat it as 0 (reset the watermark)
|
|
102
|
+
return { ok: true };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (effective > lastSeq) {
|
|
106
|
+
return { ok: true };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// equal: rolled back to same point (replay) or no new commits
|
|
110
|
+
// less: ledger truncation — history was rewritten
|
|
111
|
+
const direction = effective < lastSeq ? 'ledger truncated' : 'ledger not advanced';
|
|
112
|
+
return {
|
|
113
|
+
ok: false,
|
|
114
|
+
reason:
|
|
115
|
+
direction +
|
|
116
|
+
': incoming ledger_seq ' + effective +
|
|
117
|
+
' is not greater than last accepted ' + lastSeq +
|
|
118
|
+
' — possible rollback or key-resurrection attack',
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// validateFreshness — full attestation check for an incoming artifact
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Validate the freshness of an incoming artifact against an optional stored
|
|
128
|
+
* high-water mark.
|
|
129
|
+
*
|
|
130
|
+
* Combines:
|
|
131
|
+
* (1) structural check: `ledger_seq` is present and a non-negative integer
|
|
132
|
+
* (2) V13 high-water-mark guard: `ledger_seq` must exceed the last accepted
|
|
133
|
+
* value (pass `null` on first load to skip this check)
|
|
134
|
+
*
|
|
135
|
+
* @param {object} artifact City envelope or protocol artifact.
|
|
136
|
+
* @param {number|null} lastSeq Last accepted ledger_seq (null = first load).
|
|
137
|
+
* @returns {{ ok: boolean, ledger_seq: number|null, errors: string[] }}
|
|
138
|
+
*/
|
|
139
|
+
export function validateFreshness(artifact, lastSeq = null) {
|
|
140
|
+
const errors = [];
|
|
141
|
+
const seq = extractLedgerSeq(artifact);
|
|
142
|
+
|
|
143
|
+
if (seq === null) {
|
|
144
|
+
errors.push('ledger_seq is missing or not a non-negative integer');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const hwm = checkHighWaterMark(lastSeq, seq);
|
|
148
|
+
if (!hwm.ok) {
|
|
149
|
+
errors.push(hwm.reason);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { ok: errors.length === 0, ledger_seq: seq, errors };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// validateFreshnessWithHwm — persisted-HWM variant (SAT-474)
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Validate freshness through the **persisted** high-water-mark guard
|
|
161
|
+
* (`core/ledger.mjs` `checkAndAdvance`).
|
|
162
|
+
*
|
|
163
|
+
* Unlike `validateFreshness`, this function reads and updates the HWM from
|
|
164
|
+
* `hwmPath` on disk, so the rollback defence is durable across process
|
|
165
|
+
* restarts. Callers that previously maintained an in-memory `lastSeq` and
|
|
166
|
+
* passed it to `validateFreshness` should migrate to this function and a
|
|
167
|
+
* stable `hwmPath` in their data directory.
|
|
168
|
+
*
|
|
169
|
+
* Semantics of the persisted guard (from `checkAndAdvance`):
|
|
170
|
+
* - seq STRICTLY LESS than HWM → rejected (anti-truncation guard)
|
|
171
|
+
* - seq EQUAL to HWM → accepted (idempotent replay)
|
|
172
|
+
* - seq GREATER than HWM → accepted and HWM advanced
|
|
173
|
+
*
|
|
174
|
+
* @param {object} artifact City envelope or protocol artifact.
|
|
175
|
+
* @param {string} hwmPath Path to the independently-persisted HWM file.
|
|
176
|
+
* @param {string} [checkpointPath] Optional path to the append-only JSONL
|
|
177
|
+
* checkpoint file (SAT-561 / F0.2b). When supplied, every accepted write
|
|
178
|
+
* is also appended to the secondary checkpoint (best-effort; failure never
|
|
179
|
+
* blocks the primary HWM write).
|
|
180
|
+
* @returns {{ ok: boolean, ledger_seq: number|null, errors: string[] }}
|
|
181
|
+
*/
|
|
182
|
+
export function validateFreshnessWithHwm(artifact, hwmPath, checkpointPath) {
|
|
183
|
+
const errors = [];
|
|
184
|
+
const seq = extractLedgerSeq(artifact);
|
|
185
|
+
|
|
186
|
+
if (seq === null) {
|
|
187
|
+
errors.push('ledger_seq is missing or not a non-negative integer');
|
|
188
|
+
// Cannot call checkAndAdvance with a null seq — return early.
|
|
189
|
+
return { ok: false, ledger_seq: null, errors };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const result = checkAndAdvance(seq, hwmPath, checkpointPath);
|
|
193
|
+
if (!result.ok) {
|
|
194
|
+
errors.push(result.error);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return { ok: errors.length === 0, ledger_seq: seq, errors };
|
|
198
|
+
}
|