progmune-runtime 2.0.2 → 2.1.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/.mcp.json +11 -0
- package/.progmune_allowlist +50 -0
- package/.test_report/test_report.md +87 -0
- package/Dockerfile +2 -10
- package/FAQ.md +167 -0
- package/demo-project/auth.ts +55 -0
- package/demo-project/tsconfig.json +8 -0
- package/dist/ab-stats.js +11 -0
- package/dist/acl-breakdown.js +13 -0
- package/dist/all-sessions.js +11 -0
- package/dist/antibody-stats.js +11 -0
- package/dist/audit.js +218 -0
- package/dist/benchmark-count.js +7 -0
- package/dist/benchmark-full.js +17 -0
- package/dist/benchmark-pass-rate.js +54 -0
- package/dist/benchmark-report.js +67 -0
- package/dist/benchmark-save.js +62 -0
- package/dist/benchmark-status.js +15 -0
- package/dist/branch-ledger.js +365 -0
- package/dist/branch-tree-count.js +14 -0
- package/dist/check.js +506 -0
- package/dist/common-fixpath.js +12 -0
- package/dist/constraint-types.js +12 -0
- package/dist/deterministic-replay.js +277 -0
- package/dist/emitter.js +112 -12
- package/dist/exec-metrics.js +11 -0
- package/dist/execute.js +242 -0
- package/dist/extract-ir.js +550 -5
- package/dist/failure-collector.js +143 -0
- package/dist/failure-corpus.js +481 -35
- package/dist/failure-report.js +11 -0
- package/dist/failures.js +11 -0
- package/dist/fast-path-hits.js +13 -0
- package/dist/feedback.js +6 -3
- package/dist/file-lock.js +82 -0
- package/dist/find-session.js +10 -0
- package/dist/fingerprint-list.js +15 -0
- package/dist/gen-history-log.js +13 -0
- package/dist/generate.js +2 -1
- package/dist/generate_500.js +4 -2
- package/dist/genome.js +11 -0
- package/dist/heatmap-data.js +11 -0
- package/dist/heatmap.js +11 -0
- package/dist/immune-reporter.js +137 -0
- package/dist/learned.js +11 -0
- package/dist/ledger-registry.js +241 -0
- package/dist/llm.js +43 -2
- package/dist/load-benchmarks.js +47 -0
- package/dist/main.js +2 -1
- package/dist/mcp-server.mjs +446 -34
- package/dist/memory-layer.js +51 -13
- package/dist/metrics.js +11 -0
- package/dist/obs-web.js +561 -0
- package/dist/p0_ssg_demo.js +255 -40
- package/dist/planner.js +933 -65
- package/dist/protocol-registry.js +105 -0
- package/dist/recent-session.js +12 -0
- package/dist/repair-proposal.js +353 -0
- package/dist/report.js +32 -0
- package/dist/runtime-invariants.js +161 -0
- package/dist/runtime-types.js +117 -0
- package/dist/search-planner.js +38 -9
- package/dist/semantic-snapshot.js +155 -0
- package/dist/semantic-trace.js +1497 -0
- package/dist/semantic-validator.js +3 -2
- package/dist/semantic_guard_test.js +2 -1
- package/dist/sessions.js +11 -0
- package/dist/ssg-validator.js +658 -20
- package/dist/svl-distribution.js +11 -0
- package/dist/terminal-status.js +11 -0
- package/dist/test_failure_corpus.js +3 -1
- package/dist/token-savings.js +11 -0
- package/dist/total-repairs.js +12 -0
- package/dist/unresolved-count.js +12 -0
- package/dist/valid-fingerprints.js +13 -0
- package/dist/validator.js +116 -60
- package/dist/verify-fps.js +11 -0
- package/dist/verify-ledgers.js +11 -0
- package/docs/whitepaper-style.css +77 -0
- package/docs/whitepaper-v2.1.md +609 -0
- package/docs/whitepaper-v2.2.md +1064 -0
- package/docs/whitepaper-v2.2.pdf +0 -0
- package/fly.toml +1 -1
- package/package.json +13 -4
- package/protocols.json +136 -0
- package/public/dashboard.html +119 -0
- package/readme.md +829 -0
- package/server/hub.js +84 -12
- package/test/replay-golden/sess_1780063202050_mgeld.json +9 -0
- package/test/replay-golden/sess_1780064032560_gocld.json +354 -0
- package/test/replay-golden/sess_1780064413331_s2709.json +606 -0
- package/test/replay-golden/sess_1780064792710_y3avo.json +614 -0
- package/test/replay-golden.ts +84 -0
- package/test_benchmark.js +165 -0
- package/test_comprehensive.mjs +638 -0
- package/test_concurrency.js +129 -0
- package/test_ir_robustness.js +85 -0
- package/test_semantic_contracts.js +269 -0
- package/test_ssg_stress.js +156 -0
- package/test_svl3.js +58 -0
- package/tsconfig.json +1 -1
- package/.env.example +0 -3
- package/.progmune_memory/fingerprints.json +0 -7
- package/.progmune_memory/opt_in.json +0 -4
- package/README.md +0 -118
- package/dist/mcp-server.js +0 -55
- package/immune_hub_data/2026-05-14.json +0 -50
package/dist/ssg-validator.js
CHANGED
|
@@ -1,32 +1,670 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.StateMachineValidator = void 0;
|
|
36
|
+
exports.StateMachineValidator = exports.InvariantViolationError = void 0;
|
|
37
|
+
exports.rebuildState = rebuildState;
|
|
38
|
+
exports.applyTransitionDelta = applyTransitionDelta;
|
|
39
|
+
exports.findFixPathStatic = findFixPathStatic;
|
|
40
|
+
exports.validateTransition = validateTransition;
|
|
41
|
+
exports.checkLedgerConsistency = checkLedgerConsistency;
|
|
42
|
+
exports.hashRules = hashRules;
|
|
43
|
+
exports.hashLedger = hashLedger;
|
|
44
|
+
exports.diffLedgers = diffLedgers;
|
|
45
|
+
exports.findProducer = findProducer;
|
|
46
|
+
exports.findConsumer = findConsumer;
|
|
47
|
+
exports.findViolations = findViolations;
|
|
48
|
+
exports.findTransition = findTransition;
|
|
49
|
+
exports.listAllStates = listAllStates;
|
|
50
|
+
exports.explainRejection = explainRejection;
|
|
51
|
+
exports.rejectionToJSON = rejectionToJSON;
|
|
52
|
+
exports.parseProtocolsFromJSON = parseProtocolsFromJSON;
|
|
53
|
+
const crypto = __importStar(require("crypto"));
|
|
54
|
+
const DEFAULT_NAMESPACE = "_global";
|
|
55
|
+
class InvariantViolationError extends Error {
|
|
56
|
+
constructor(message, detail) {
|
|
57
|
+
super(message);
|
|
58
|
+
this.name = "InvariantViolationError";
|
|
59
|
+
this.detail = detail;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
exports.InvariantViolationError = InvariantViolationError;
|
|
63
|
+
// ═══════════════════════════════════════════════════════════════
|
|
64
|
+
// Phase 3: Pure Functions — Semantic Ledger Kernel
|
|
65
|
+
// ═══════════════════════════════════════════════════════════════
|
|
66
|
+
// ── Internal helpers ──
|
|
67
|
+
function toSnapshot(stateMap) {
|
|
68
|
+
const snap = {};
|
|
69
|
+
for (const [ns, states] of stateMap) {
|
|
70
|
+
snap[ns] = [...states].sort();
|
|
71
|
+
}
|
|
72
|
+
return snap;
|
|
73
|
+
}
|
|
74
|
+
function fromSnapshot(snap) {
|
|
75
|
+
const map = new Map();
|
|
76
|
+
for (const [ns, states] of Object.entries(snap)) {
|
|
77
|
+
map.set(ns, new Set(states));
|
|
78
|
+
}
|
|
79
|
+
return map;
|
|
80
|
+
}
|
|
81
|
+
function deepEqualSnapshots(a, b) {
|
|
82
|
+
const keysA = Object.keys(a).sort();
|
|
83
|
+
const keysB = Object.keys(b).sort();
|
|
84
|
+
if (keysA.length !== keysB.length)
|
|
85
|
+
return false;
|
|
86
|
+
for (let i = 0; i < keysA.length; i++) {
|
|
87
|
+
if (keysA[i] !== keysB[i])
|
|
88
|
+
return false;
|
|
89
|
+
const statesA = a[keysA[i]];
|
|
90
|
+
const statesB = b[keysA[i]];
|
|
91
|
+
if (statesA.length !== statesB.length)
|
|
92
|
+
return false;
|
|
93
|
+
for (let j = 0; j < statesA.length; j++) {
|
|
94
|
+
if (statesA[j] !== statesB[j])
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
// ── rebuildState: pure fold over ledger → per-namespace state snapshot ──
|
|
101
|
+
function rebuildState(ledger, namespaceInitialStates = new Map([["_global", "INIT"]])) {
|
|
102
|
+
const stateMap = fromSnapshot({});
|
|
103
|
+
for (const [ns, initState] of namespaceInitialStates) {
|
|
104
|
+
stateMap.set(ns, new Set([initState]));
|
|
105
|
+
}
|
|
106
|
+
if (!stateMap.has("_global"))
|
|
107
|
+
stateMap.set("_global", new Set(["INIT"]));
|
|
108
|
+
for (const t of ledger) {
|
|
109
|
+
if (!t.valid)
|
|
110
|
+
continue;
|
|
111
|
+
applyTransitionDelta(stateMap, t);
|
|
112
|
+
}
|
|
113
|
+
return toSnapshot(stateMap);
|
|
114
|
+
}
|
|
115
|
+
// ── applyTransitionDelta: incremental primitive for O(n) loops ──
|
|
116
|
+
function applyTransitionDelta(stateMap, transition) {
|
|
117
|
+
const ns = transition.namespace || DEFAULT_NAMESPACE;
|
|
118
|
+
const nsStates = stateMap.get(ns) || new Set();
|
|
119
|
+
for (const s of transition.invalidated)
|
|
120
|
+
nsStates.delete(s);
|
|
121
|
+
for (const s of transition.acquired)
|
|
122
|
+
nsStates.add(s);
|
|
123
|
+
stateMap.set(ns, nsStates);
|
|
124
|
+
}
|
|
125
|
+
// ── computeDelta: derive acquired/invalidated from before/after for one namespace ──
|
|
126
|
+
function computeDelta(beforeSnap, afterSnap, namespace) {
|
|
127
|
+
const before = beforeSnap[namespace] || [];
|
|
128
|
+
const after = afterSnap[namespace] || [];
|
|
129
|
+
const acquired = after.filter(s => !before.includes(s));
|
|
130
|
+
const invalidated = before.filter(s => !after.includes(s));
|
|
131
|
+
return { acquired, invalidated };
|
|
132
|
+
}
|
|
133
|
+
// ── findFixPathStatic: BFS state graph search (extracted from class) ──
|
|
134
|
+
function findFixPathStatic(rules, namespace, current, targetPreStates) {
|
|
135
|
+
const nsFuncs = [];
|
|
136
|
+
for (const [fn, rule] of rules) {
|
|
137
|
+
if ((rule.namespace || DEFAULT_NAMESPACE) === namespace) {
|
|
138
|
+
nsFuncs.push({ name: fn, rule });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// BFS
|
|
142
|
+
const startKey = [...new Set(current)].sort().join(",");
|
|
143
|
+
const visited = new Set();
|
|
144
|
+
const queue = [
|
|
145
|
+
{ states: new Set(current), path: [] }
|
|
146
|
+
];
|
|
147
|
+
visited.add(startKey);
|
|
148
|
+
while (queue.length > 0) {
|
|
149
|
+
const { states, path } = queue.shift();
|
|
150
|
+
if (targetPreStates.every(s => states.has(s)))
|
|
151
|
+
return path;
|
|
152
|
+
for (const { name, rule } of nsFuncs) {
|
|
153
|
+
if (!rule.pre_states.every(p => states.has(p)))
|
|
154
|
+
continue;
|
|
155
|
+
const nextStates = new Set(states);
|
|
156
|
+
if (rule.invalidate)
|
|
157
|
+
rule.invalidate.forEach(s => nextStates.delete(s));
|
|
158
|
+
rule.post_states.forEach(s => nextStates.add(s));
|
|
159
|
+
const nextKey = [...nextStates].sort().join(",");
|
|
160
|
+
if (visited.has(nextKey))
|
|
161
|
+
continue;
|
|
162
|
+
visited.add(nextKey);
|
|
163
|
+
if (visited.size > 1000)
|
|
164
|
+
break;
|
|
165
|
+
queue.push({ states: nextStates, path: [...path, name] });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// Fallback: single-hop greedy
|
|
169
|
+
const path = [];
|
|
170
|
+
const currentSet = new Set(current);
|
|
171
|
+
for (const target of targetPreStates) {
|
|
172
|
+
if (currentSet.has(target))
|
|
173
|
+
continue;
|
|
174
|
+
for (const { name, rule } of nsFuncs) {
|
|
175
|
+
if (rule.post_states.includes(target)) {
|
|
176
|
+
path.push(name);
|
|
177
|
+
if (rule.invalidate)
|
|
178
|
+
rule.invalidate.forEach(s => currentSet.delete(s));
|
|
179
|
+
rule.post_states.forEach(s => currentSet.add(s));
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return path;
|
|
185
|
+
}
|
|
186
|
+
// ── validateTransition: pure function, stateless ──
|
|
187
|
+
function validateTransition(ctx, candidateFunctionName, actionIndex, rules, namespaceInitialStates, ruleHash) {
|
|
188
|
+
const currentState = ctx.currentState;
|
|
189
|
+
const rule = rules.get(candidateFunctionName);
|
|
190
|
+
if (!rule) {
|
|
191
|
+
const transition = {
|
|
192
|
+
actionIndex,
|
|
193
|
+
function: candidateFunctionName,
|
|
194
|
+
namespace: DEFAULT_NAMESPACE,
|
|
195
|
+
acquired: [],
|
|
196
|
+
invalidated: [],
|
|
197
|
+
statesBefore: currentState,
|
|
198
|
+
statesAfter: currentState,
|
|
199
|
+
valid: true,
|
|
200
|
+
ruleHash,
|
|
201
|
+
};
|
|
202
|
+
// Invariant-1: delta consistency for no-rule transition (trivially satisfied)
|
|
203
|
+
return { valid: true, transition };
|
|
204
|
+
}
|
|
205
|
+
const ns = rule.namespace || DEFAULT_NAMESPACE;
|
|
206
|
+
const nsStates = new Set(currentState[ns] || []);
|
|
207
|
+
// Check pre-states
|
|
208
|
+
if (!rule.pre_states.every((s) => nsStates.has(s))) {
|
|
209
|
+
const fixPath = findFixPathStatic(rules, ns, [...nsStates], rule.pre_states);
|
|
210
|
+
const rejection = {
|
|
211
|
+
blocked: candidateFunctionName,
|
|
212
|
+
currentState: [...nsStates],
|
|
213
|
+
requiredState: rule.pre_states,
|
|
214
|
+
missingFunctions: fixPath,
|
|
215
|
+
fixPath,
|
|
216
|
+
namespace: ns,
|
|
217
|
+
};
|
|
218
|
+
const transition = {
|
|
219
|
+
actionIndex,
|
|
220
|
+
function: candidateFunctionName,
|
|
221
|
+
namespace: ns,
|
|
222
|
+
acquired: [],
|
|
223
|
+
invalidated: [],
|
|
224
|
+
statesBefore: currentState,
|
|
225
|
+
statesAfter: currentState, // no state change on rejection
|
|
226
|
+
valid: false,
|
|
227
|
+
ruleHash,
|
|
228
|
+
};
|
|
229
|
+
return { valid: false, transition, rejection };
|
|
230
|
+
}
|
|
231
|
+
// Valid — compute state change
|
|
232
|
+
const beforeNs = [...nsStates];
|
|
233
|
+
if (rule.invalidate)
|
|
234
|
+
rule.invalidate.forEach((s) => nsStates.delete(s));
|
|
235
|
+
rule.post_states.forEach((s) => nsStates.add(s));
|
|
236
|
+
// Build statesAfter: copy currentState, replace this namespace
|
|
237
|
+
const statesAfter = {};
|
|
238
|
+
for (const nsKey of Object.keys(currentState)) {
|
|
239
|
+
statesAfter[nsKey] = nsKey === ns
|
|
240
|
+
? [...nsStates].sort()
|
|
241
|
+
: [...(currentState[nsKey] || [])];
|
|
242
|
+
}
|
|
243
|
+
if (!statesAfter[ns]) {
|
|
244
|
+
statesAfter[ns] = [...nsStates].sort();
|
|
245
|
+
}
|
|
246
|
+
const afterNs = statesAfter[ns] || [];
|
|
247
|
+
const acquired = afterNs.filter((s) => !beforeNs.includes(s));
|
|
248
|
+
const invalidated = beforeNs.filter((s) => !afterNs.includes(s));
|
|
249
|
+
const transition = {
|
|
250
|
+
actionIndex,
|
|
251
|
+
function: candidateFunctionName,
|
|
252
|
+
namespace: ns,
|
|
253
|
+
acquired,
|
|
254
|
+
invalidated,
|
|
255
|
+
statesBefore: currentState,
|
|
256
|
+
statesAfter,
|
|
257
|
+
valid: true,
|
|
258
|
+
ruleHash,
|
|
259
|
+
};
|
|
260
|
+
// Invariant-1: delta consistency
|
|
261
|
+
const deltaMap = fromSnapshot(currentState);
|
|
262
|
+
applyTransitionDelta(deltaMap, transition);
|
|
263
|
+
const computedAfter = toSnapshot(deltaMap);
|
|
264
|
+
if (!deepEqualSnapshots(computedAfter, statesAfter)) {
|
|
265
|
+
const detail = `[Invariant-1] Delta consistency violation in validateTransition for "${candidateFunctionName}":\n` +
|
|
266
|
+
` acquired: [${acquired.join(", ")}] invalidated: [${invalidated.join(", ")}]\n` +
|
|
267
|
+
` expected after: ${JSON.stringify(computedAfter)}\n` +
|
|
268
|
+
` actual after: ${JSON.stringify(statesAfter)}`;
|
|
269
|
+
// P0 Strict Mode: fail fast on kernel corruption
|
|
270
|
+
if (process.env.PROGMUNE_STRICT !== "false") {
|
|
271
|
+
throw new InvariantViolationError(detail, {
|
|
272
|
+
invariant: "delta-consistency",
|
|
273
|
+
namespace: ns,
|
|
274
|
+
function: candidateFunctionName,
|
|
275
|
+
expected: computedAfter,
|
|
276
|
+
actual: statesAfter,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
console.error(detail);
|
|
280
|
+
}
|
|
281
|
+
return { valid: true, transition };
|
|
282
|
+
}
|
|
283
|
+
// ── checkLedgerConsistency: Invariant-0 + Invariant-1 over full ledger ──
|
|
284
|
+
function checkLedgerConsistency(ledger, namespaceInitialStates = new Map([["_global", "INIT"]])) {
|
|
285
|
+
const violations = [];
|
|
286
|
+
const running = fromSnapshot({});
|
|
287
|
+
// Pre-scan: collect all namespaces referenced in any transition
|
|
288
|
+
const allNamespaces = new Set(namespaceInitialStates.keys());
|
|
289
|
+
for (const t of ledger) {
|
|
290
|
+
for (const ns of Object.keys(t.statesBefore))
|
|
291
|
+
allNamespaces.add(ns);
|
|
292
|
+
for (const ns of Object.keys(t.statesAfter))
|
|
293
|
+
allNamespaces.add(ns);
|
|
294
|
+
allNamespaces.add(t.namespace);
|
|
295
|
+
}
|
|
296
|
+
// Initialize running state: init from namespaceInitialStates, empty for others
|
|
297
|
+
for (const ns of allNamespaces) {
|
|
298
|
+
if (namespaceInitialStates.has(ns)) {
|
|
299
|
+
running.set(ns, new Set([namespaceInitialStates.get(ns)]));
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
running.set(ns, new Set());
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// Normalize a snapshot: ensure all known namespaces are present
|
|
306
|
+
function normalizeSnap(snap) {
|
|
307
|
+
const out = {};
|
|
308
|
+
for (const ns of allNamespaces) {
|
|
309
|
+
out[ns] = [...(snap[ns] || [])].sort();
|
|
310
|
+
}
|
|
311
|
+
return out;
|
|
312
|
+
}
|
|
313
|
+
for (let i = 0; i < ledger.length; i++) {
|
|
314
|
+
const t = ledger[i];
|
|
315
|
+
// Invariant-0: statesBefore must equal rebuildState(ledger[0..i-1])
|
|
316
|
+
const expectedBefore = normalizeSnap(toSnapshot(running));
|
|
317
|
+
const actualBefore = normalizeSnap(t.statesBefore);
|
|
318
|
+
if (!deepEqualSnapshots(expectedBefore, actualBefore)) {
|
|
319
|
+
violations.push({
|
|
320
|
+
index: i,
|
|
321
|
+
invariant: "before-consistency",
|
|
322
|
+
expected: expectedBefore,
|
|
323
|
+
actual: actualBefore,
|
|
324
|
+
detail: `Transition[${i}] "${t.function}": statesBefore does not match rebuilt state from previous ledger`,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
// Invariant-1: statesAfter must equal applyDelta(statesBefore, acquired, invalidated)
|
|
328
|
+
if (t.valid) {
|
|
329
|
+
const beforeMap = fromSnapshot(t.statesBefore);
|
|
330
|
+
applyTransitionDelta(beforeMap, t);
|
|
331
|
+
const expectedAfter = normalizeSnap(toSnapshot(beforeMap));
|
|
332
|
+
const actualAfter = normalizeSnap(t.statesAfter);
|
|
333
|
+
if (!deepEqualSnapshots(expectedAfter, actualAfter)) {
|
|
334
|
+
violations.push({
|
|
335
|
+
index: i,
|
|
336
|
+
invariant: "delta-consistency",
|
|
337
|
+
expected: expectedAfter,
|
|
338
|
+
actual: actualAfter,
|
|
339
|
+
detail: `Transition[${i}] "${t.function}": statesAfter does not match applyDelta(statesBefore, acquired, invalidated)`,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// Advance running state
|
|
344
|
+
if (t.valid) {
|
|
345
|
+
applyTransitionDelta(running, t);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return { consistent: violations.length === 0, violations };
|
|
349
|
+
}
|
|
350
|
+
// ── hashRules: stable hash of rule set for constraint snapshot (P1) ──
|
|
351
|
+
function hashRules(rules) {
|
|
352
|
+
const sorted = [...rules.entries()]
|
|
353
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
354
|
+
.map(([name, rule]) => ({
|
|
355
|
+
function: name,
|
|
356
|
+
pre_states: [...rule.pre_states].sort(),
|
|
357
|
+
post_states: [...rule.post_states].sort(),
|
|
358
|
+
invalidate: rule.invalidate ? [...rule.invalidate].sort() : undefined,
|
|
359
|
+
namespace: rule.namespace || DEFAULT_NAMESPACE,
|
|
360
|
+
}));
|
|
361
|
+
return crypto.createHash("sha256").update(JSON.stringify(sorted)).digest("hex").slice(0, 16);
|
|
362
|
+
}
|
|
363
|
+
/** Compute a deterministic SHA256 hash of an entire ledger (P1: Tamper-evident integrity). */
|
|
364
|
+
function hashLedger(ledger) {
|
|
365
|
+
const canonical = ledger.map(t => ({
|
|
366
|
+
actionIndex: t.actionIndex,
|
|
367
|
+
function: t.function,
|
|
368
|
+
namespace: t.namespace,
|
|
369
|
+
acquired: [...t.acquired].sort(),
|
|
370
|
+
invalidated: [...t.invalidated].sort(),
|
|
371
|
+
statesBefore: Object.fromEntries(Object.entries(t.statesBefore).map(([k, v]) => [k, [...v].sort()]).sort()),
|
|
372
|
+
statesAfter: Object.fromEntries(Object.entries(t.statesAfter).map(([k, v]) => [k, [...v].sort()]).sort()),
|
|
373
|
+
valid: t.valid,
|
|
374
|
+
ruleHash: t.ruleHash || "",
|
|
375
|
+
}));
|
|
376
|
+
return crypto.createHash("sha256").update(JSON.stringify(canonical)).digest("hex").slice(0, 16);
|
|
377
|
+
}
|
|
378
|
+
/** Compare two ledgers and identify structural differences. */
|
|
379
|
+
function diffLedgers(ledgerA, ledgerB) {
|
|
380
|
+
const hash = (t) => crypto.createHash("sha256").update(JSON.stringify({
|
|
381
|
+
i: t.actionIndex, f: t.function, n: t.namespace,
|
|
382
|
+
a: [...t.acquired].sort(), x: [...t.invalidated].sort(),
|
|
383
|
+
v: t.valid, r: t.ruleHash || "",
|
|
384
|
+
})).digest("hex").slice(0, 12);
|
|
385
|
+
const mapA = new Map();
|
|
386
|
+
const mapB = new Map();
|
|
387
|
+
for (const t of ledgerA)
|
|
388
|
+
mapA.set(t.actionIndex, { t, h: hash(t) });
|
|
389
|
+
for (const t of ledgerB)
|
|
390
|
+
mapB.set(t.actionIndex, { t, h: hash(t) });
|
|
391
|
+
const allIndices = new Set([...mapA.keys(), ...mapB.keys()]);
|
|
392
|
+
const unchanged = [];
|
|
393
|
+
const onlyInA = [];
|
|
394
|
+
const onlyInB = [];
|
|
395
|
+
const changed = [];
|
|
396
|
+
for (const idx of [...allIndices].sort((a, b) => a - b)) {
|
|
397
|
+
const a = mapA.get(idx);
|
|
398
|
+
const b = mapB.get(idx);
|
|
399
|
+
if (a && b) {
|
|
400
|
+
if (a.h === b.h) {
|
|
401
|
+
unchanged.push(idx);
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
changed.push({ index: idx, function: a.t.function, hashA: a.h, hashB: b.h });
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
else if (a && !b) {
|
|
408
|
+
onlyInA.push({ index: idx, function: a.t.function, hashA: a.h });
|
|
409
|
+
}
|
|
410
|
+
else if (!a && b) {
|
|
411
|
+
onlyInB.push({ index: idx, function: b.t.function, hashB: b.h });
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
unchanged: unchanged.length,
|
|
416
|
+
onlyInA,
|
|
417
|
+
onlyInB,
|
|
418
|
+
changed,
|
|
419
|
+
identical: onlyInA.length === 0 && onlyInB.length === 0 && changed.length === 0,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
/** Find all transitions that acquire (produce) a given state in the ledger. */
|
|
423
|
+
function findProducer(state, ledger) {
|
|
424
|
+
return ledger
|
|
425
|
+
.map((t, i) => ({ transition: t, index: i, namespace: t.namespace }))
|
|
426
|
+
.filter(r => r.transition.acquired.includes(state));
|
|
427
|
+
}
|
|
428
|
+
/** Find all transitions that have a given state in their pre_states (consume it). */
|
|
429
|
+
function findConsumer(state, ledger) {
|
|
430
|
+
return ledger
|
|
431
|
+
.map((t, i) => ({ transition: t, index: i, namespace: t.namespace }))
|
|
432
|
+
.filter(r => (r.transition.statesBefore[r.namespace] || []).includes(state));
|
|
433
|
+
}
|
|
434
|
+
/** Find all invalid transitions in a ledger. */
|
|
435
|
+
function findViolations(ledger) {
|
|
436
|
+
return ledger
|
|
437
|
+
.map((t, i) => ({ transition: t, index: i, namespace: t.namespace }))
|
|
438
|
+
.filter(r => !r.transition.valid);
|
|
439
|
+
}
|
|
440
|
+
/** Find a transition by its action index. */
|
|
441
|
+
function findTransition(actionIndex, ledger) {
|
|
442
|
+
const idx = ledger.findIndex(t => t.actionIndex === actionIndex);
|
|
443
|
+
if (idx === -1)
|
|
444
|
+
return null;
|
|
445
|
+
return { transition: ledger[idx], index: idx, namespace: ledger[idx].namespace };
|
|
446
|
+
}
|
|
447
|
+
/** List all unique states present across all namespaces in a ledger. */
|
|
448
|
+
function listAllStates(ledger) {
|
|
449
|
+
const seen = new Set();
|
|
450
|
+
const result = [];
|
|
451
|
+
for (const t of ledger) {
|
|
452
|
+
const ns = t.namespace;
|
|
453
|
+
for (const s of t.acquired) {
|
|
454
|
+
const key = `${ns}:${s}`;
|
|
455
|
+
if (!seen.has(key)) {
|
|
456
|
+
seen.add(key);
|
|
457
|
+
result.push({ namespace: ns, state: s });
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
for (const s of t.invalidated) {
|
|
461
|
+
const key = `${ns}:${s}`;
|
|
462
|
+
if (!seen.has(key)) {
|
|
463
|
+
seen.add(key);
|
|
464
|
+
result.push({ namespace: ns, state: s });
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
for (const states of Object.values(t.statesBefore)) {
|
|
468
|
+
for (const s of states) {
|
|
469
|
+
const key = `${ns}:${s}`;
|
|
470
|
+
if (!seen.has(key)) {
|
|
471
|
+
seen.add(key);
|
|
472
|
+
result.push({ namespace: ns, state: s });
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return result;
|
|
478
|
+
}
|
|
479
|
+
// ═══════════════════════════════════════════════════════════════
|
|
480
|
+
// StateMachineValidator — backward-compatible class wrapper
|
|
481
|
+
// Delegates to pure functions internally (Strangler Pattern)
|
|
482
|
+
// ═══════════════════════════════════════════════════════════════
|
|
4
483
|
class StateMachineValidator {
|
|
5
|
-
constructor(rules, initialState = 'INIT') {
|
|
6
|
-
|
|
484
|
+
constructor(rules, initialState = 'INIT', namespaceInitialStates) {
|
|
485
|
+
/** Internal ledger — the truth source. State is derived from this. */
|
|
486
|
+
this.ledger = [];
|
|
7
487
|
this.rules = new Map();
|
|
8
|
-
rules.forEach(r =>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (!
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
488
|
+
rules.forEach(r => {
|
|
489
|
+
this.rules.set(r.function, r.protocol);
|
|
490
|
+
});
|
|
491
|
+
this.nsInitialStates = new Map(namespaceInitialStates);
|
|
492
|
+
if (!this.nsInitialStates.has("_global")) {
|
|
493
|
+
this.nsInitialStates.set("_global", initialState);
|
|
494
|
+
}
|
|
495
|
+
// Pre-compute rule hash for constraint snapshot determinism
|
|
496
|
+
this._ruleHash = hashRules(this.rules);
|
|
497
|
+
// Initialize ledger and context from namespace initial states
|
|
498
|
+
this.ctx = {
|
|
499
|
+
ledger: [],
|
|
500
|
+
currentState: rebuildState([], this.nsInitialStates),
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
/** @deprecated Use validateTransition(ctx, ...) for stateless validation */
|
|
504
|
+
setNamespaceInitialState(namespace, state) {
|
|
505
|
+
const ns = namespace || DEFAULT_NAMESPACE;
|
|
506
|
+
this.nsInitialStates.set(ns, state);
|
|
507
|
+
// Rebuild context to reflect new initial state
|
|
508
|
+
this.ctx = {
|
|
509
|
+
ledger: this.ledger,
|
|
510
|
+
currentState: rebuildState(this.ledger, this.nsInitialStates),
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Validate a function call against current protocol state.
|
|
515
|
+
* Internally delegates to the pure validateTransition() function.
|
|
516
|
+
* @deprecated Prefer validateTransition() directly for stateless validation.
|
|
517
|
+
*/
|
|
518
|
+
apply(functionName, actionIndex) {
|
|
519
|
+
const idx = actionIndex ?? this.ledger.length;
|
|
520
|
+
const { valid, transition, rejection } = validateTransition(this.ctx, functionName, idx, this.rules, this.nsInitialStates, this._ruleHash);
|
|
521
|
+
// Append to ledger (truth source)
|
|
522
|
+
this.ledger.push(transition);
|
|
523
|
+
// Update context incrementally (O(1) per step)
|
|
524
|
+
if (transition.valid) {
|
|
525
|
+
const stateMap = fromSnapshot(this.ctx.currentState);
|
|
526
|
+
applyTransitionDelta(stateMap, transition);
|
|
527
|
+
this.ctx = {
|
|
528
|
+
ledger: this.ledger,
|
|
529
|
+
currentState: toSnapshot(stateMap),
|
|
20
530
|
};
|
|
21
531
|
}
|
|
22
|
-
|
|
23
|
-
|
|
532
|
+
else {
|
|
533
|
+
this.ctx = {
|
|
534
|
+
ledger: this.ledger,
|
|
535
|
+
currentState: this.ctx.currentState, // unchanged on rejection
|
|
536
|
+
};
|
|
24
537
|
}
|
|
25
|
-
|
|
26
|
-
|
|
538
|
+
return {
|
|
539
|
+
valid,
|
|
540
|
+
statesBefore: transition.statesBefore,
|
|
541
|
+
statesAfter: transition.statesAfter,
|
|
542
|
+
acquired: transition.acquired,
|
|
543
|
+
invalidated: transition.invalidated,
|
|
544
|
+
namespace: transition.namespace,
|
|
545
|
+
rejection,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
applyWithTransition(functionName, actionIndex) {
|
|
549
|
+
const result = this.apply(functionName, actionIndex);
|
|
550
|
+
const transition = this.ledger[this.ledger.length - 1];
|
|
551
|
+
return { result, transition };
|
|
27
552
|
}
|
|
28
553
|
getCurrentStates() {
|
|
29
|
-
|
|
554
|
+
const effective = new Set();
|
|
555
|
+
for (const states of Object.values(this.ctx.currentState)) {
|
|
556
|
+
for (const s of states)
|
|
557
|
+
effective.add(s);
|
|
558
|
+
}
|
|
559
|
+
return [...effective];
|
|
560
|
+
}
|
|
561
|
+
snapshotNamespaceStates() {
|
|
562
|
+
return rebuildState(this.ledger, this.nsInitialStates);
|
|
563
|
+
}
|
|
564
|
+
getNamespaceStates(namespace) {
|
|
565
|
+
const ns = namespace || DEFAULT_NAMESPACE;
|
|
566
|
+
return [...(this.ctx.currentState[ns] || [])];
|
|
567
|
+
}
|
|
568
|
+
/** Returns trace reconstructed from the ledger (backward-compat) */
|
|
569
|
+
getTrace() {
|
|
570
|
+
// Replay the ledger to build trace nodes
|
|
571
|
+
const trace = [];
|
|
572
|
+
const runningState = fromSnapshot({});
|
|
573
|
+
for (const [ns, initState] of this.nsInitialStates) {
|
|
574
|
+
runningState.set(ns, new Set([initState]));
|
|
575
|
+
}
|
|
576
|
+
if (!runningState.has("_global"))
|
|
577
|
+
runningState.set("_global", new Set(["INIT"]));
|
|
578
|
+
for (const t of this.ledger) {
|
|
579
|
+
const node = {
|
|
580
|
+
function: t.function,
|
|
581
|
+
valid: t.valid,
|
|
582
|
+
statesBefore: t.statesBefore,
|
|
583
|
+
statesAfter: t.statesAfter,
|
|
584
|
+
acquired: t.acquired,
|
|
585
|
+
invalidated: t.invalidated,
|
|
586
|
+
namespace: t.namespace,
|
|
587
|
+
};
|
|
588
|
+
trace.push(node);
|
|
589
|
+
if (t.valid) {
|
|
590
|
+
applyTransitionDelta(runningState, t);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return trace;
|
|
594
|
+
}
|
|
595
|
+
/** Returns the internal ledger (Phase 3 API) */
|
|
596
|
+
getLedger() {
|
|
597
|
+
return [...this.ledger];
|
|
598
|
+
}
|
|
599
|
+
/** Returns the current ValidationContext (Phase 3 API) */
|
|
600
|
+
getContext() {
|
|
601
|
+
return { ...this.ctx, ledger: [...this.ctx.ledger] };
|
|
602
|
+
}
|
|
603
|
+
/** Returns the pre-computed rule hash (Phase 3 API) */
|
|
604
|
+
getRuleHash() {
|
|
605
|
+
return this._ruleHash;
|
|
606
|
+
}
|
|
607
|
+
// ── Static utilities (delegate to standalone functions) ──
|
|
608
|
+
static explainRejection(rejection) {
|
|
609
|
+
return explainRejection(rejection);
|
|
610
|
+
}
|
|
611
|
+
static rejectionToJSON(rejection) {
|
|
612
|
+
return rejectionToJSON(rejection);
|
|
30
613
|
}
|
|
31
614
|
}
|
|
32
615
|
exports.StateMachineValidator = StateMachineValidator;
|
|
616
|
+
// ═══════════════════════════════════════════════════════════════
|
|
617
|
+
// Standalone presentation utilities (formerly static methods)
|
|
618
|
+
// ═══════════════════════════════════════════════════════════════
|
|
619
|
+
/** Format an SSG rejection as a human-readable multi-line string. */
|
|
620
|
+
function explainRejection(rejection) {
|
|
621
|
+
const nsLabel = rejection.namespace && rejection.namespace !== DEFAULT_NAMESPACE
|
|
622
|
+
? ` [namespace: ${rejection.namespace}]` : '';
|
|
623
|
+
const lines = [
|
|
624
|
+
`🚫 SSG 协议拦截: ${rejection.blocked}${nsLabel}`,
|
|
625
|
+
``,
|
|
626
|
+
` 当前状态: ${rejection.currentState.join(', ') || '(无)'}`,
|
|
627
|
+
` 所需状态: ${rejection.requiredState.join(', ')}`,
|
|
628
|
+
``,
|
|
629
|
+
];
|
|
630
|
+
if (rejection.missingFunctions.length > 0) {
|
|
631
|
+
lines.push(` 缺失步骤: ${rejection.missingFunctions.join(' → ')}`);
|
|
632
|
+
}
|
|
633
|
+
if (rejection.fixPath.length > 0) {
|
|
634
|
+
lines.push(` 修复路径: ${rejection.fixPath.join(' → ')}`);
|
|
635
|
+
}
|
|
636
|
+
return lines.join('\n');
|
|
637
|
+
}
|
|
638
|
+
/** Format an SSG rejection as a structured JSON object. */
|
|
639
|
+
function rejectionToJSON(rejection) {
|
|
640
|
+
return {
|
|
641
|
+
protocol_violation: {
|
|
642
|
+
blocked_function: rejection.blocked,
|
|
643
|
+
namespace: rejection.namespace,
|
|
644
|
+
current_state: rejection.currentState,
|
|
645
|
+
required_pre_states: rejection.requiredState,
|
|
646
|
+
},
|
|
647
|
+
diagnosis: {
|
|
648
|
+
missing_functions: rejection.missingFunctions,
|
|
649
|
+
fix_path: rejection.fixPath,
|
|
650
|
+
},
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
// ═══════════════════════════════════════════════════════════════
|
|
654
|
+
// parseProtocolsFromJSON (unchanged)
|
|
655
|
+
// ═══════════════════════════════════════════════════════════════
|
|
656
|
+
function parseProtocolsFromJSON(protocolDef) {
|
|
657
|
+
const protocols = [];
|
|
658
|
+
for (const [funcName, rule] of Object.entries(protocolDef.rules)) {
|
|
659
|
+
protocols.push({
|
|
660
|
+
function: funcName,
|
|
661
|
+
protocol: {
|
|
662
|
+
pre_states: rule.pre_states,
|
|
663
|
+
post_states: rule.post_states,
|
|
664
|
+
invalidate: rule.invalidate,
|
|
665
|
+
namespace: rule.namespace,
|
|
666
|
+
},
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
return protocols;
|
|
670
|
+
}
|