@holmes-lab/holmes-kit 0.19.6 → 0.20.1
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 +143 -0
- package/README.md +9 -2
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/approve-status.d.ts +24 -0
- package/dist/holmes/cli/approve-status.js +166 -0
- package/dist/holmes/cli/approve-watch.d.ts +6 -0
- package/dist/holmes/cli/approve-watch.js +6 -0
- package/dist/holmes/cli/approve.d.ts +27 -0
- package/dist/holmes/cli/approve.js +68 -7
- package/dist/holmes/cli/cli-execution.d.ts +2 -0
- package/dist/holmes/cli/cli-execution.js +24 -0
- package/dist/holmes/cli/doctor-check.d.ts +15 -0
- package/dist/holmes/cli/doctor-check.js +2 -0
- package/dist/holmes/cli/doctor.d.ts +2 -8
- package/dist/holmes/cli/doctor.js +12 -3
- package/dist/holmes/cli/gitignore-merge.d.ts +0 -5
- package/dist/holmes/cli/gitignore-merge.js +6 -2
- package/dist/holmes/cli/index.js +176 -139
- package/dist/holmes/cli/npx-cache-check.d.ts +16 -0
- package/dist/holmes/cli/npx-cache-check.js +130 -0
- package/dist/holmes/cli/playbook-skills.d.ts +2 -22
- package/dist/holmes/cli/playbook-skills.js +10 -54
- package/dist/holmes/cli/probe-process.d.ts +17 -2
- package/dist/holmes/cli/probe-process.js +44 -11
- package/dist/holmes/governance/approval-grants.d.ts +22 -0
- package/dist/holmes/governance/approval-grants.js +55 -1
- package/dist/holmes/governance/approval-queue.d.ts +43 -4
- package/dist/holmes/governance/approval-queue.js +91 -13
- package/dist/holmes/governance/display-id-aliases.d.ts +29 -0
- package/dist/holmes/governance/display-id-aliases.js +86 -0
- package/dist/holmes/governance/ledger-store.d.ts +42 -14
- package/dist/holmes/governance/ledger-store.js +49 -3
- package/dist/holmes/governance/ledger-timeline.d.ts +3 -1
- package/dist/holmes/governance/ledger-timeline.js +11 -1
- package/dist/holmes/governance/provenance-chain.d.ts +2 -0
- package/dist/holmes/governance/provenance-chain.js +13 -2
- package/dist/holmes/governance/provenance-ledger.d.ts +28 -0
- package/dist/holmes/governance/provenance-ledger.js +69 -16
- package/dist/holmes/governance/session-context.d.ts +3 -0
- package/dist/holmes/governance/session-context.js +26 -8
- package/dist/holmes/guardrail/risk-classifier.js +2 -2
- package/dist/holmes/guardrail/write-target.js +2 -1
- package/dist/holmes/hooks/pre-tool-use.js +12 -1
- package/dist/holmes/hooks/stop.js +14 -3
- package/dist/holmes/mcp/handlers/adoption.d.ts +118 -0
- package/dist/holmes/mcp/handlers/adoption.js +186 -0
- package/dist/holmes/mcp/handlers/entity-integration.d.ts +100 -0
- package/dist/holmes/mcp/handlers/entity-integration.js +324 -0
- package/dist/holmes/mcp/handlers/entity-renumber.d.ts +137 -0
- package/dist/holmes/mcp/handlers/entity-renumber.js +228 -0
- package/dist/holmes/mcp/handlers/entity-store.d.ts +78 -0
- package/dist/holmes/mcp/handlers/entity-store.js +212 -0
- package/dist/holmes/mcp/handlers/graph-operations.d.ts +90 -0
- package/dist/holmes/mcp/handlers/graph-operations.js +260 -0
- package/dist/holmes/mcp/handlers/maintenance-analysis.d.ts +34 -0
- package/dist/holmes/mcp/handlers/maintenance-analysis.js +288 -0
- package/dist/holmes/mcp/handlers/maintenance-evidence.d.ts +40 -0
- package/dist/holmes/mcp/handlers/maintenance-evidence.js +104 -0
- package/dist/holmes/mcp/handlers/operator-inspection.d.ts +167 -0
- package/dist/holmes/mcp/handlers/operator-inspection.js +284 -0
- package/dist/holmes/mcp/handlers/phase-queries.d.ts +34 -0
- package/dist/holmes/mcp/handlers/phase-queries.js +86 -0
- package/dist/holmes/mcp/handlers/review-evidence.d.ts +47 -0
- package/dist/holmes/mcp/handlers/review-evidence.js +370 -0
- package/dist/holmes/mcp/handlers/review-queries.d.ts +68 -0
- package/dist/holmes/mcp/handlers/review-queries.js +79 -0
- package/dist/holmes/mcp/handlers/risk-assessment.d.ts +25 -0
- package/dist/holmes/mcp/handlers/risk-assessment.js +181 -0
- package/dist/holmes/mcp/handlers/slice-orchestration.d.ts +72 -0
- package/dist/holmes/mcp/handlers/slice-orchestration.js +373 -0
- package/dist/holmes/mcp/handlers/spec-approval.d.ts +105 -0
- package/dist/holmes/mcp/handlers/spec-approval.js +504 -0
- package/dist/holmes/mcp/handlers/spec-authoring.d.ts +57 -0
- package/dist/holmes/mcp/handlers/spec-authoring.js +326 -0
- package/dist/holmes/mcp/handlers/spec-lifecycle.d.ts +122 -0
- package/dist/holmes/mcp/handlers/spec-lifecycle.js +339 -0
- package/dist/holmes/mcp/handlers/spec-queries.d.ts +30 -0
- package/dist/holmes/mcp/handlers/spec-queries.js +74 -0
- package/dist/holmes/mcp/handlers/taint-screening.d.ts +48 -0
- package/dist/holmes/mcp/handlers/taint-screening.js +183 -0
- package/dist/holmes/mcp/handlers/test-execution.d.ts +63 -0
- package/dist/holmes/mcp/handlers/test-execution.js +210 -0
- package/dist/holmes/mcp/handlers/workspace-identity.d.ts +30 -0
- package/dist/holmes/mcp/handlers/workspace-identity.js +68 -0
- package/dist/holmes/mcp/handlers/workspace-queries.d.ts +77 -0
- package/dist/holmes/mcp/handlers/workspace-queries.js +193 -0
- package/dist/holmes/mcp/handlers.d.ts +661 -424
- package/dist/holmes/mcp/handlers.js +270 -3251
- package/dist/holmes/mcp/server-specs-dir.d.ts +2 -0
- package/dist/holmes/mcp/server-specs-dir.js +71 -0
- package/dist/holmes/mcp/server.js +12 -2
- package/dist/holmes/mcp/tool-schemas.js +43 -2
- package/dist/holmes/project/execution-context.d.ts +17 -0
- package/dist/holmes/project/execution-context.js +121 -0
- package/dist/holmes/project/installer-markers.d.ts +34 -0
- package/dist/holmes/project/installer-markers.js +65 -0
- package/dist/holmes/project/root.d.ts +12 -1
- package/dist/holmes/project/root.js +22 -3
- package/dist/holmes/project/workspace-identity.d.ts +29 -0
- package/dist/holmes/project/workspace-identity.js +181 -0
- package/dist/holmes/review/test-outcomes.d.ts +6 -1
- package/dist/holmes/review/test-outcomes.js +23 -5
- package/dist/holmes/review/test-runner.d.ts +18 -0
- package/dist/holmes/review/test-runner.js +136 -5
- package/dist/holmes/rtm/graph-store.d.ts +50 -1
- package/dist/holmes/rtm/graph-store.js +244 -3
- package/dist/holmes/rtm/incremental.d.ts +1 -0
- package/dist/holmes/rtm/incremental.js +12 -3
- package/dist/holmes/rtm/localize.js +7 -0
- package/dist/holmes/rtm/rtm-graph.d.ts +9 -0
- package/dist/holmes/rtm/rtm-graph.js +13 -0
- package/dist/holmes/rtm/test-scope.js +4 -1
- package/dist/holmes/semantic/vector-cache.d.ts +16 -1
- package/dist/holmes/semantic/vector-cache.js +155 -19
- package/dist/holmes/spec/approval-status.d.ts +10 -0
- package/dist/holmes/spec/approval-status.js +7 -3
- package/dist/holmes/spec/entity-git-snapshot.d.ts +50 -0
- package/dist/holmes/spec/entity-git-snapshot.js +276 -0
- package/dist/holmes/spec/entity-integration-discharge.d.ts +73 -0
- package/dist/holmes/spec/entity-integration-discharge.js +233 -0
- package/dist/holmes/spec/entity-integration-transaction.d.ts +179 -0
- package/dist/holmes/spec/entity-integration-transaction.js +1221 -0
- package/dist/holmes/spec/entity-integration.d.ts +216 -0
- package/dist/holmes/spec/entity-integration.js +760 -0
- package/dist/holmes/spec/entity-renumber-transaction.d.ts +82 -0
- package/dist/holmes/spec/entity-renumber-transaction.js +711 -0
- package/dist/holmes/spec/entity-renumber.d.ts +32 -0
- package/dist/holmes/spec/entity-renumber.js +156 -0
- package/dist/holmes/spec/entity-store.d.ts +135 -0
- package/dist/holmes/spec/entity-store.js +1053 -0
- package/dist/holmes/spec/entity-transaction.d.ts +105 -0
- package/dist/holmes/spec/entity-transaction.js +741 -0
- package/dist/holmes/spec/renumber.d.ts +58 -0
- package/dist/holmes/spec/renumber.js +200 -1
- package/dist/holmes/spec/spec-store.d.ts +3 -2
- package/dist/holmes/spec/spec-store.js +23 -2
- package/dist/holmes/spec/transition-policy.d.ts +36 -0
- package/dist/holmes/spec/transition-policy.js +135 -0
- package/package.json +5 -2
|
@@ -41,9 +41,12 @@ exports.approvalRequestId = approvalRequestId;
|
|
|
41
41
|
exports.foldQueue = foldQueue;
|
|
42
42
|
exports.enqueueApprovalRequest = enqueueApprovalRequest;
|
|
43
43
|
exports.enqueueApprovalRequestDetailed = enqueueApprovalRequestDetailed;
|
|
44
|
+
exports.appendQueueEvent = appendQueueEvent;
|
|
44
45
|
exports.readQueue = readQueue;
|
|
45
46
|
exports.queueHint = queueHint;
|
|
46
|
-
// @implements A-SPEC-244
|
|
47
|
+
// @implements A-SPEC-244, A-SPEC-626
|
|
48
|
+
const execution_context_1 = require("../project/execution-context");
|
|
49
|
+
const root_1 = require("../project/root");
|
|
47
50
|
const node_crypto_1 = require("node:crypto");
|
|
48
51
|
const npx_bin_1 = require("../project/npx-bin");
|
|
49
52
|
const fs = __importStar(require("node:fs"));
|
|
@@ -75,8 +78,12 @@ exports.QUEUE_RELPATH = path.join('.ax', 'approvals', 'queue.jsonl');
|
|
|
75
78
|
// single-shot gate refusals nobody ever meant to decide, burying the real inbox (spec-approve 9,
|
|
76
79
|
// config-write 1) and accumulating raw shell commands in a tracked file. Only these kinds seek a
|
|
77
80
|
// decision; everything else routes to the local per-replica refusal log below. Wired RED-first.
|
|
81
|
+
// @implements A-SPEC-651.4 — `review-resolve` is a decision too: resolving an open critical needs a
|
|
82
|
+
// human, review_record's refusal points that human at `holmes-kit approve`, and without this entry the
|
|
83
|
+
// request went to the refusal log so the inbox stayed empty (measured 2026-09-14 on Windows: two
|
|
84
|
+
// expired session dialogs, then `approve --list` answered "No requests awaiting approval").
|
|
78
85
|
exports.DECISION_KINDS = new Set([
|
|
79
|
-
'spec-approve', 'config-write', 'autonomy-grant', 'spec-reappraisal',
|
|
86
|
+
'spec-approve', 'config-write', 'autonomy-grant', 'spec-reappraisal', 'review-resolve',
|
|
80
87
|
]);
|
|
81
88
|
const REFUSALS_FILE_RE = /^refusals\.([^.]+)\.jsonl$/;
|
|
82
89
|
function refusalsFilename(replica) {
|
|
@@ -145,6 +152,7 @@ function foldQueue(lines, opts) {
|
|
|
145
152
|
const pending = new Map();
|
|
146
153
|
const decisions = {};
|
|
147
154
|
const holds = {};
|
|
155
|
+
const subjects = {};
|
|
148
156
|
let malformedLines = 0;
|
|
149
157
|
for (const raw of Array.isArray(lines) ? lines : []) {
|
|
150
158
|
const line = String(raw ?? '').trim();
|
|
@@ -163,6 +171,16 @@ function foldQueue(lines, opts) {
|
|
|
163
171
|
malformedLines++;
|
|
164
172
|
continue;
|
|
165
173
|
}
|
|
174
|
+
let execution;
|
|
175
|
+
if ('execution' in e) {
|
|
176
|
+
try {
|
|
177
|
+
execution = (0, execution_context_1.validateExecution)(e.execution);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
malformedLines++;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
166
184
|
const id = typeof e.id === 'string' ? e.id : undefined;
|
|
167
185
|
switch (e.event) {
|
|
168
186
|
case 'requested': {
|
|
@@ -170,6 +188,10 @@ function foldQueue(lines, opts) {
|
|
|
170
188
|
malformedLines++;
|
|
171
189
|
break;
|
|
172
190
|
}
|
|
191
|
+
// @implements A-SPEC-650 — remembered for every kind, before the inbox filter: a decided shell
|
|
192
|
+
// refusal still has a subject to show on the status surface.
|
|
193
|
+
if (!subjects[id])
|
|
194
|
+
subjects[id] = { kind: String(e.kind ?? ''), target: String(e.target ?? '') };
|
|
173
195
|
// @implements A-SPEC-576.1 — the READER applies the writer's predicate.
|
|
174
196
|
// REQ-563 routed gate refusals away from the inbox and deliberately did NOT rewrite the
|
|
175
197
|
// 2,192 lines already written; append-only is the rule this ledger is worth something for.
|
|
@@ -198,6 +220,10 @@ function foldQueue(lines, opts) {
|
|
|
198
220
|
// @implements A-SPEC-562.2 — events written before attribution existed have no field;
|
|
199
221
|
// `unknown` names that honestly instead of pretending they came from this machine.
|
|
200
222
|
replica: typeof e.replica === 'string' && e.replica !== '' ? e.replica : 'unknown',
|
|
223
|
+
// @implements A-SPEC-649 — the details ride the first filing; a repeat never rewrites them.
|
|
224
|
+
...(execution ? { execution } : {}),
|
|
225
|
+
...(typeof e.subject === 'string' && e.subject !== '' ? { subject: e.subject } : {}),
|
|
226
|
+
...(typeof e.risk === 'string' && e.risk !== '' ? { risk: e.risk } : {}),
|
|
201
227
|
});
|
|
202
228
|
}
|
|
203
229
|
break;
|
|
@@ -212,9 +238,28 @@ function foldQueue(lines, opts) {
|
|
|
212
238
|
...(typeof e.reason === 'string' ? { reason: e.reason } : {}),
|
|
213
239
|
ts: typeof e.ts === 'string' ? e.ts : '',
|
|
214
240
|
...(typeof e.covered === 'number' ? { covered: e.covered } : {}),
|
|
241
|
+
...(execution ? { execution } : {}),
|
|
242
|
+
// @implements A-SPEC-650 — when a grant expires and who decided, for the outcome rows.
|
|
243
|
+
...(typeof e.expires === 'string' ? { expires: e.expires } : {}),
|
|
244
|
+
...(typeof e.actor === 'string' ? { actor: e.actor } : {}),
|
|
215
245
|
};
|
|
216
246
|
}
|
|
217
247
|
break;
|
|
248
|
+
// @implements A-SPEC-649 — the grant's later life, in the same ledger the screen reads.
|
|
249
|
+
// `consumed`: the act the grant authorized succeeded (written by consumeGrantFile) — the
|
|
250
|
+
// decision keeps its `granted` shape and gains the consumption time. `revoked`: the operator
|
|
251
|
+
// withdrew a live grant — a decision like any other (pending cleared, the hold answered).
|
|
252
|
+
case 'consumed':
|
|
253
|
+
if (id)
|
|
254
|
+
decisions[id] = { ...(decisions[id] ?? { event: 'granted', ts: typeof e.ts === 'string' ? e.ts : '' }), consumedTs: typeof e.ts === 'string' ? e.ts : '' };
|
|
255
|
+
break;
|
|
256
|
+
case 'revoked':
|
|
257
|
+
if (id) {
|
|
258
|
+
pending.delete(id);
|
|
259
|
+
delete holds[id];
|
|
260
|
+
decisions[id] = { event: 'revoked', ...(typeof e.reason === 'string' ? { reason: e.reason } : {}), ts: typeof e.ts === 'string' ? e.ts : '', ...(execution ? { execution } : {}) };
|
|
261
|
+
}
|
|
262
|
+
break;
|
|
218
263
|
case 'held': {
|
|
219
264
|
const entry = id ? pending.get(id) : undefined;
|
|
220
265
|
if (entry) {
|
|
@@ -225,7 +270,7 @@ function foldQueue(lines, opts) {
|
|
|
225
270
|
// @implements A-SPEC-563.2 — record the hold by id too: a refusal-log request has no pending
|
|
226
271
|
// entry to hang the question on, and the question must still reach the retry.
|
|
227
272
|
if (id)
|
|
228
|
-
holds[id] = { ...(typeof e.question === 'string' ? { question: e.question } : {}), ts: typeof e.ts === 'string' ? e.ts : '' };
|
|
273
|
+
holds[id] = { ...(typeof e.question === 'string' ? { question: e.question } : {}), ts: typeof e.ts === 'string' ? e.ts : '', ...(execution ? { execution } : {}) };
|
|
229
274
|
break;
|
|
230
275
|
}
|
|
231
276
|
default:
|
|
@@ -236,8 +281,9 @@ function foldQueue(lines, opts) {
|
|
|
236
281
|
const all = [...pending.values()];
|
|
237
282
|
const ttl = typeof opts?.ttlMs === 'number' && typeof opts?.now === 'number'
|
|
238
283
|
? { ttlMs: opts.ttlMs, now: opts.now } : null;
|
|
284
|
+
const withSubjects = Object.keys(subjects).length > 0 ? { subjects } : {};
|
|
239
285
|
if (ttl === null)
|
|
240
|
-
return { pending: all, expired: [], malformedLines, decisions, holds };
|
|
286
|
+
return { pending: all, expired: [], malformedLines, decisions, holds, ...withSubjects };
|
|
241
287
|
// @implements A-SPEC-507.1 — strict excess only, and an unparseable lastTs stays ACTIVE: a
|
|
242
288
|
// clockless entry must never be silently hidden by a clock it does not carry.
|
|
243
289
|
const expired = [];
|
|
@@ -246,16 +292,8 @@ function foldQueue(lines, opts) {
|
|
|
246
292
|
const last = Date.parse(p.lastTs);
|
|
247
293
|
(Number.isFinite(last) && last + ttl.ttlMs < ttl.now ? expired : active).push(p);
|
|
248
294
|
}
|
|
249
|
-
return { pending: active, expired, malformedLines, decisions, holds };
|
|
295
|
+
return { pending: active, expired, malformedLines, decisions, holds, ...withSubjects };
|
|
250
296
|
}
|
|
251
|
-
/**
|
|
252
|
-
* Append a request event. Fire-and-forget.
|
|
253
|
-
*
|
|
254
|
-
* @implements A-SPEC-244
|
|
255
|
-
* Every failure is swallowed into `false`: this runs INSIDE gate verdict paths, and the moment a
|
|
256
|
-
* dead queue could change a verdict, killing the queue becomes a way to manipulate the gate. The
|
|
257
|
-
* caller uses the boolean only to decide whether to print the review hint.
|
|
258
|
-
*/
|
|
259
297
|
function enqueueApprovalRequest(root, req) {
|
|
260
298
|
return enqueueApprovalRequestDetailed(root, req).written;
|
|
261
299
|
}
|
|
@@ -298,6 +336,15 @@ function enqueueApprovalRequestDetailed(root, req) {
|
|
|
298
336
|
catch {
|
|
299
337
|
replica = undefined;
|
|
300
338
|
}
|
|
339
|
+
// @implements A-SPEC-649 — the requesting execution, when this write happens inside a handler run
|
|
340
|
+
// (absent for hooks, the CLI and a foreign root — never guessed).
|
|
341
|
+
let execution;
|
|
342
|
+
try {
|
|
343
|
+
execution = (0, execution_context_1.executionForRoot)(root);
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
execution = undefined;
|
|
347
|
+
}
|
|
301
348
|
const event = {
|
|
302
349
|
event: 'requested',
|
|
303
350
|
id: approvalRequestId(req.kind, req.target),
|
|
@@ -306,6 +353,9 @@ function enqueueApprovalRequestDetailed(root, req) {
|
|
|
306
353
|
why: req.why,
|
|
307
354
|
ts: new Date().toISOString(),
|
|
308
355
|
...(replica !== undefined && replica !== '' ? { replica } : {}),
|
|
356
|
+
...(execution ? { execution } : {}),
|
|
357
|
+
...(typeof req.subject === 'string' && req.subject !== '' ? { subject: req.subject } : {}),
|
|
358
|
+
...(typeof req.risk === 'string' && req.risk !== '' ? { risk: req.risk } : {}),
|
|
309
359
|
// @implements A-SPEC-564.2 — only the refusal log carries the feedback cost; inbox kinds have
|
|
310
360
|
// no deny text to measure.
|
|
311
361
|
...(!exports.DECISION_KINDS.has(req.kind) && typeof req.reasonBytes === 'number' && Number.isFinite(req.reasonBytes)
|
|
@@ -362,6 +412,34 @@ function isPlainFile(file) {
|
|
|
362
412
|
return true;
|
|
363
413
|
}
|
|
364
414
|
}
|
|
415
|
+
/**
|
|
416
|
+
* @implements A-SPEC-649
|
|
417
|
+
* Append a lifecycle event (`consumed`, `revoked`) to the tracked queue: `.ax` must exist, the
|
|
418
|
+
* queue must be a plain file, the timestamp is this write's, and the active execution (if any) is
|
|
419
|
+
* attached — the same discipline as the CLI's decision writer. Fire-and-forget: `false` on failure.
|
|
420
|
+
*/
|
|
421
|
+
function appendQueueEvent(root, event) {
|
|
422
|
+
try {
|
|
423
|
+
let execution;
|
|
424
|
+
try {
|
|
425
|
+
execution = (0, execution_context_1.executionForRoot)((0, root_1.resolveProjectRoot)(root).root);
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
execution = undefined;
|
|
429
|
+
}
|
|
430
|
+
const file = path.join(root, exports.QUEUE_RELPATH);
|
|
431
|
+
if (!fs.existsSync(path.join(root, '.ax')))
|
|
432
|
+
return false;
|
|
433
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
434
|
+
if (!isPlainFile(file))
|
|
435
|
+
return false;
|
|
436
|
+
fs.appendFileSync(file, JSON.stringify({ ...event, ts: new Date().toISOString(), ...(execution ? { execution } : {}) }) + '\n');
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
365
443
|
/** Read and fold the queue on disk. A missing file is an empty queue, not an error. */
|
|
366
444
|
function readQueue(root, opts) {
|
|
367
445
|
const file = path.join(root, exports.QUEUE_RELPATH);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display-id history, read from the ledger's rename records.
|
|
3
|
+
*
|
|
4
|
+
* The entity model keeps identity immutable and the display id mutable, and every official rename is
|
|
5
|
+
* already in the ledger: `entity-renumbered` (inputs `entity:<uuid>`, `old-id:<X>`, `new-id:<Y>`), the
|
|
6
|
+
* legacy `spec-renumbered` (`X->Y`), and `entity-integrated` (same `old-id:`/`new-id:` form for the
|
|
7
|
+
* mapping steps an integration renumbers). Measured 2026-09-14: evidence consumers keyed by display id
|
|
8
|
+
* lost a renumbered document's red-first outcomes and timeline entirely. This module is the one place
|
|
9
|
+
* that turns those records into aliases; it reads only recorded renames and infers nothing.
|
|
10
|
+
*/
|
|
11
|
+
export interface DisplayIdRename {
|
|
12
|
+
oldId: string;
|
|
13
|
+
newId: string;
|
|
14
|
+
ts: string;
|
|
15
|
+
}
|
|
16
|
+
type RenameEvent = {
|
|
17
|
+
kind: string;
|
|
18
|
+
ts: string;
|
|
19
|
+
inputs?: string[];
|
|
20
|
+
};
|
|
21
|
+
/** Every recorded rename as an ordered pair, in ledger order (ts, then input order). */
|
|
22
|
+
export declare function displayIdRenames(events: ReadonlyArray<RenameEvent>): DisplayIdRename[];
|
|
23
|
+
/** The display ids an entity has carried, as the connected component of `id` over recorded renames. */
|
|
24
|
+
export declare function aliasesOf(events: ReadonlyArray<RenameEvent>, id: string): string[];
|
|
25
|
+
/** The display id the entity carries now: recorded renames applied forward from `id`, in ledger order. */
|
|
26
|
+
export declare function currentDisplayId(events: ReadonlyArray<RenameEvent>, id: string): string;
|
|
27
|
+
/** One pass over the renames, then memoized answers — for consumers that canonicalize many ids. */
|
|
28
|
+
export declare function displayIdCanonicalizer(events: ReadonlyArray<RenameEvent>): (id: string) => string;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.displayIdRenames = displayIdRenames;
|
|
4
|
+
exports.aliasesOf = aliasesOf;
|
|
5
|
+
exports.currentDisplayId = currentDisplayId;
|
|
6
|
+
exports.displayIdCanonicalizer = displayIdCanonicalizer;
|
|
7
|
+
const RENAME_KINDS = new Set(['entity-renumbered', 'entity-integrated']);
|
|
8
|
+
const ARROW = /^(\S+)->(\S+)$/;
|
|
9
|
+
/** Every recorded rename as an ordered pair, in ledger order (ts, then input order). */
|
|
10
|
+
function displayIdRenames(events) {
|
|
11
|
+
const indexed = events.map((e, i) => ({ e, i }));
|
|
12
|
+
indexed.sort((a, b) => (a.e.ts < b.e.ts ? -1 : a.e.ts > b.e.ts ? 1 : a.i - b.i));
|
|
13
|
+
const out = [];
|
|
14
|
+
for (const { e } of indexed) {
|
|
15
|
+
const inputs = e.inputs ?? [];
|
|
16
|
+
if (RENAME_KINDS.has(e.kind)) {
|
|
17
|
+
// An `old-id:` is paired with the next `new-id:` that follows it; an unpaired one is ignored.
|
|
18
|
+
let pending = null;
|
|
19
|
+
for (const input of inputs) {
|
|
20
|
+
if (input.startsWith('old-id:')) {
|
|
21
|
+
pending = input.slice('old-id:'.length) || null;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (input.startsWith('new-id:')) {
|
|
25
|
+
const newId = input.slice('new-id:'.length);
|
|
26
|
+
if (pending && newId && newId !== pending)
|
|
27
|
+
out.push({ oldId: pending, newId, ts: e.ts });
|
|
28
|
+
pending = null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else if (e.kind === 'spec-renumbered') {
|
|
33
|
+
for (const input of inputs) {
|
|
34
|
+
const m = ARROW.exec(input);
|
|
35
|
+
if (m && m[1] !== m[2])
|
|
36
|
+
out.push({ oldId: m[1], newId: m[2], ts: e.ts });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
/** The display ids an entity has carried, as the connected component of `id` over recorded renames. */
|
|
43
|
+
function aliasesOf(events, id) {
|
|
44
|
+
const adjacent = new Map();
|
|
45
|
+
const link = (a, b) => {
|
|
46
|
+
(adjacent.get(a) ?? adjacent.set(a, new Set()).get(a)).add(b);
|
|
47
|
+
(adjacent.get(b) ?? adjacent.set(b, new Set()).get(b)).add(a);
|
|
48
|
+
};
|
|
49
|
+
for (const r of displayIdRenames(events))
|
|
50
|
+
link(r.oldId, r.newId);
|
|
51
|
+
const seen = new Set([id]);
|
|
52
|
+
const queue = [id];
|
|
53
|
+
while (queue.length) {
|
|
54
|
+
const cur = queue.shift();
|
|
55
|
+
for (const next of adjacent.get(cur) ?? [])
|
|
56
|
+
if (!seen.has(next)) {
|
|
57
|
+
seen.add(next);
|
|
58
|
+
queue.push(next);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return [...seen].sort();
|
|
62
|
+
}
|
|
63
|
+
/** The display id the entity carries now: recorded renames applied forward from `id`, in ledger order. */
|
|
64
|
+
function currentDisplayId(events, id) {
|
|
65
|
+
let cur = id;
|
|
66
|
+
for (const r of displayIdRenames(events))
|
|
67
|
+
if (r.oldId === cur)
|
|
68
|
+
cur = r.newId;
|
|
69
|
+
return cur;
|
|
70
|
+
}
|
|
71
|
+
/** One pass over the renames, then memoized answers — for consumers that canonicalize many ids. */
|
|
72
|
+
function displayIdCanonicalizer(events) {
|
|
73
|
+
const renames = displayIdRenames(events);
|
|
74
|
+
const memo = new Map();
|
|
75
|
+
return (id) => {
|
|
76
|
+
const hit = memo.get(id);
|
|
77
|
+
if (hit !== undefined)
|
|
78
|
+
return hit;
|
|
79
|
+
let cur = id;
|
|
80
|
+
for (const r of renames)
|
|
81
|
+
if (r.oldId === cur)
|
|
82
|
+
cur = r.newId;
|
|
83
|
+
memo.set(id, cur);
|
|
84
|
+
return cur;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ProvenanceEvent } from './provenance-chain';
|
|
2
|
-
import { LedgerEvent } from './provenance-ledger';
|
|
2
|
+
import { LedgerEvent, DoubleSpend } from './provenance-ledger';
|
|
3
3
|
/**
|
|
4
4
|
* The ledger as a PORT — ADR-001's deferred half, applied to the one store whose shape was derived
|
|
5
5
|
* from real use rather than guessed at.
|
|
@@ -24,6 +24,14 @@ import { LedgerEvent } from './provenance-ledger';
|
|
|
24
24
|
* ADR-001 named speculative port design as the cost of deciding early; this is where that cost is
|
|
25
25
|
* refused.
|
|
26
26
|
*/
|
|
27
|
+
/** A-SPEC-642 — one verification failure: a chain break (`kind: 'chain'`) or an unreconciled double-spend. */
|
|
28
|
+
export interface LedgerBroken {
|
|
29
|
+
replicaId: string | null;
|
|
30
|
+
detail?: string;
|
|
31
|
+
kind?: 'chain' | 'double-spend';
|
|
32
|
+
nonce?: string;
|
|
33
|
+
events?: DoubleSpend['events'];
|
|
34
|
+
}
|
|
27
35
|
export interface LedgerStore {
|
|
28
36
|
/** This writer's identity — the one used for events this store appends. */
|
|
29
37
|
replicaId(): string;
|
|
@@ -34,19 +42,43 @@ export interface LedgerStore {
|
|
|
34
42
|
}[];
|
|
35
43
|
/** Every event across every chain, deterministically ordered. */
|
|
36
44
|
loadAll(): LedgerEvent[];
|
|
37
|
-
/** Per-chain verification; one broken chain fails the whole store and is named. */
|
|
45
|
+
/** Per-chain verification; one broken chain fails the whole store and is named. A-SPEC-642: cross-replica double-spends ride along as `kind: 'double-spend'` entries. */
|
|
38
46
|
verifyAll(): {
|
|
39
47
|
ok: boolean;
|
|
40
|
-
broken:
|
|
41
|
-
replicaId: string | null;
|
|
42
|
-
detail?: string;
|
|
43
|
-
}[];
|
|
48
|
+
broken: LedgerBroken[];
|
|
44
49
|
};
|
|
45
50
|
/** Append to this writer's chain. Never modifies or removes an existing event. */
|
|
46
51
|
append(body: Omit<ProvenanceEvent, 'prevHash' | 'hash' | 'seq'>): ProvenanceEvent;
|
|
47
52
|
/** Spent on ANY chain — see the interface note. */
|
|
48
53
|
isNonceConsumed(nonce: string): boolean;
|
|
54
|
+
/** A-SPEC-642 — single-use nonces consumed more than once across chains and not yet reconciled. */
|
|
55
|
+
doubleSpends(): DoubleSpend[];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* @implements A-SPEC-643
|
|
59
|
+
* The chain file THIS process writes: `provenance.<replica>.jsonl`, resolved exactly as every other
|
|
60
|
+
* writer resolves it (the run's execution replica when registered, else the stored or hostname-derived
|
|
61
|
+
* id). Measured 2026-09-14: the shell gate, `risk_check` and `review_record` handed
|
|
62
|
+
* `consumeNonceExclusively` the legacy `provenance.jsonl`, so two clones spending the same approval
|
|
63
|
+
* offline both appended to one file and the origin's merge died with CONFLICT (add/add) before any
|
|
64
|
+
* double-spend could be seen. Consumptions belong on the writer's chain like every other event.
|
|
65
|
+
*/
|
|
66
|
+
export declare function writerChainFile(ledgerDir: string): string;
|
|
67
|
+
/** A-SPEC-642 — thrown by approval resolution while the merged ledger holds an unreconciled double-spend. */
|
|
68
|
+
export declare class LedgerConflictRefusal extends Error {
|
|
69
|
+
readonly holmesRefusal = true;
|
|
70
|
+
readonly code = "ledger-conflict";
|
|
71
|
+
constructor(message: string);
|
|
49
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* @implements A-SPEC-642
|
|
75
|
+
* The one sentence every authority-spending act refuses with while the merged ledger holds an
|
|
76
|
+
* unreconciled cross-replica double-spend, or null. Names the nonce, the replicas and the act that
|
|
77
|
+
* clears it, because a refusal that cannot be acted on is an obstacle rather than a gate. An absent
|
|
78
|
+
* ledger directory is a clean ledger. Not caught: an unreadable chain file throws here exactly as it
|
|
79
|
+
* throws in `isNonceConsumed`, and the callers' fail-closed handling applies.
|
|
80
|
+
*/
|
|
81
|
+
export declare function ledgerConflictReason(dir: string): string | null;
|
|
50
82
|
/**
|
|
51
83
|
* Local adapter — DELEGATES to `ProvenanceLedger` rather than re-implementing it.
|
|
52
84
|
*
|
|
@@ -77,13 +109,11 @@ export declare class FileLedgerStore implements LedgerStore {
|
|
|
77
109
|
loadAll(): LedgerEvent[];
|
|
78
110
|
verifyAll(): {
|
|
79
111
|
ok: boolean;
|
|
80
|
-
broken:
|
|
81
|
-
replicaId: string | null;
|
|
82
|
-
detail?: string;
|
|
83
|
-
}[];
|
|
112
|
+
broken: LedgerBroken[];
|
|
84
113
|
};
|
|
85
114
|
append(body: Omit<ProvenanceEvent, 'prevHash' | 'hash' | 'seq'>): ProvenanceEvent;
|
|
86
115
|
isNonceConsumed(nonce: string): boolean;
|
|
116
|
+
doubleSpends(): DoubleSpend[];
|
|
87
117
|
}
|
|
88
118
|
/**
|
|
89
119
|
* In-memory adapter — the instrument that makes the contract testable.
|
|
@@ -104,13 +134,11 @@ export declare class MemoryLedgerStore implements LedgerStore {
|
|
|
104
134
|
loadAll(): LedgerEvent[];
|
|
105
135
|
verifyAll(): {
|
|
106
136
|
ok: boolean;
|
|
107
|
-
broken:
|
|
108
|
-
replicaId: string | null;
|
|
109
|
-
detail?: string;
|
|
110
|
-
}[];
|
|
137
|
+
broken: LedgerBroken[];
|
|
111
138
|
};
|
|
112
139
|
append(body: Omit<ProvenanceEvent, 'prevHash' | 'hash' | 'seq'>): ProvenanceEvent;
|
|
113
140
|
isNonceConsumed(nonce: string): boolean;
|
|
141
|
+
doubleSpends(): DoubleSpend[];
|
|
114
142
|
}
|
|
115
143
|
/**
|
|
116
144
|
* The cross-replica nonce check, bound to one ledger file, ready to hand to
|
|
@@ -33,12 +33,53 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.MemoryLedgerStore = exports.FileLedgerStore = void 0;
|
|
36
|
+
exports.MemoryLedgerStore = exports.FileLedgerStore = exports.LedgerConflictRefusal = void 0;
|
|
37
|
+
exports.writerChainFile = writerChainFile;
|
|
38
|
+
exports.ledgerConflictReason = ledgerConflictReason;
|
|
37
39
|
exports.nonceConsumedIn = nonceConsumedIn;
|
|
38
|
-
// @implements A-SPEC-150
|
|
40
|
+
// @implements A-SPEC-150, A-SPEC-624
|
|
41
|
+
// @implements A-SPEC-643
|
|
42
|
+
// @implements A-SPEC-642
|
|
39
43
|
const provenance_chain_1 = require("./provenance-chain");
|
|
40
44
|
const provenance_ledger_1 = require("./provenance-ledger");
|
|
41
45
|
const path = __importStar(require("node:path"));
|
|
46
|
+
/**
|
|
47
|
+
* @implements A-SPEC-643
|
|
48
|
+
* The chain file THIS process writes: `provenance.<replica>.jsonl`, resolved exactly as every other
|
|
49
|
+
* writer resolves it (the run's execution replica when registered, else the stored or hostname-derived
|
|
50
|
+
* id). Measured 2026-09-14: the shell gate, `risk_check` and `review_record` handed
|
|
51
|
+
* `consumeNonceExclusively` the legacy `provenance.jsonl`, so two clones spending the same approval
|
|
52
|
+
* offline both appended to one file and the origin's merge died with CONFLICT (add/add) before any
|
|
53
|
+
* double-spend could be seen. Consumptions belong on the writer's chain like every other event.
|
|
54
|
+
*/
|
|
55
|
+
function writerChainFile(ledgerDir) {
|
|
56
|
+
return provenance_ledger_1.ProvenanceLedger.at(ledgerDir).currentFile();
|
|
57
|
+
}
|
|
58
|
+
/** A-SPEC-642 — thrown by approval resolution while the merged ledger holds an unreconciled double-spend. */
|
|
59
|
+
class LedgerConflictRefusal extends Error {
|
|
60
|
+
holmesRefusal = true;
|
|
61
|
+
code = 'ledger-conflict';
|
|
62
|
+
constructor(message) { super(message); this.name = 'LedgerConflictRefusal'; }
|
|
63
|
+
}
|
|
64
|
+
exports.LedgerConflictRefusal = LedgerConflictRefusal;
|
|
65
|
+
/**
|
|
66
|
+
* @implements A-SPEC-642
|
|
67
|
+
* The one sentence every authority-spending act refuses with while the merged ledger holds an
|
|
68
|
+
* unreconciled cross-replica double-spend, or null. Names the nonce, the replicas and the act that
|
|
69
|
+
* clears it, because a refusal that cannot be acted on is an obstacle rather than a gate. An absent
|
|
70
|
+
* ledger directory is a clean ledger. Not caught: an unreadable chain file throws here exactly as it
|
|
71
|
+
* throws in `isNonceConsumed`, and the callers' fail-closed handling applies.
|
|
72
|
+
*/
|
|
73
|
+
function ledgerConflictReason(dir) {
|
|
74
|
+
const pending = new FileLedgerStore(dir).doubleSpends();
|
|
75
|
+
if (pending.length === 0)
|
|
76
|
+
return null;
|
|
77
|
+
const d = pending[0];
|
|
78
|
+
const replicas = [...new Set(d.events.map((e) => e.replicaId ?? 'legacy'))].join(', ');
|
|
79
|
+
return `the merged ledger holds an unreconciled cross-replica double-spend: single-use approval ${d.nonce} was consumed on replicas ${replicas}`
|
|
80
|
+
+ ` — approvals, applies and recoveries are refused until an operator reconciles it (ledger_reconcile { nonce: "${d.nonce}" })`
|
|
81
|
+
+ (pending.length > 1 ? `; ${pending.length - 1} more pending` : '');
|
|
82
|
+
}
|
|
42
83
|
/**
|
|
43
84
|
* Local adapter — DELEGATES to `ProvenanceLedger` rather than re-implementing it.
|
|
44
85
|
*
|
|
@@ -66,6 +107,9 @@ class FileLedgerStore {
|
|
|
66
107
|
this.inner = provenance_ledger_1.ProvenanceLedger.at(dir, givenId);
|
|
67
108
|
}
|
|
68
109
|
replicaId() {
|
|
110
|
+
const execution = this.inner.executionContext();
|
|
111
|
+
if (execution)
|
|
112
|
+
return execution.replicaId;
|
|
69
113
|
if (this.cachedId === undefined)
|
|
70
114
|
this.cachedId = this.givenId ?? this.inner.replicaIdOf();
|
|
71
115
|
return this.cachedId;
|
|
@@ -77,12 +121,13 @@ class FileLedgerStore {
|
|
|
77
121
|
loadAll() { return this.inner.loadAll(); }
|
|
78
122
|
verifyAll() {
|
|
79
123
|
const r = this.inner.verifyAll();
|
|
80
|
-
return { ok: r.ok, broken: r.broken.map((b) => ({ replicaId: b.replicaId, detail: b.detail })) };
|
|
124
|
+
return { ok: r.ok, broken: r.broken.map((b) => ({ replicaId: b.replicaId, detail: b.detail, ...(b.kind ? { kind: b.kind } : {}), ...(b.nonce ? { nonce: b.nonce } : {}), ...(b.events ? { events: b.events } : {}) })) };
|
|
81
125
|
}
|
|
82
126
|
append(body) {
|
|
83
127
|
return this.inner.append(body);
|
|
84
128
|
}
|
|
85
129
|
isNonceConsumed(nonce) { return this.inner.isNonceConsumed(nonce); }
|
|
130
|
+
doubleSpends() { return this.inner.doubleSpends(); }
|
|
86
131
|
}
|
|
87
132
|
exports.FileLedgerStore = FileLedgerStore;
|
|
88
133
|
/**
|
|
@@ -137,6 +182,7 @@ class MemoryLedgerStore {
|
|
|
137
182
|
return this.loadAll().some((e) => e.kind === 'nonce-consumed'
|
|
138
183
|
&& ((e.inputs ?? []).includes(nonce) || (e.inputs ?? []).includes(fp)));
|
|
139
184
|
}
|
|
185
|
+
doubleSpends() { return (0, provenance_ledger_1.unreconciledDoubleSpends)(this.loadAll()); }
|
|
140
186
|
}
|
|
141
187
|
exports.MemoryLedgerStore = MemoryLedgerStore;
|
|
142
188
|
// @implements A-SPEC-574.3
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { ExecutionContext } from '../project/execution-context';
|
|
1
2
|
import { ProvenanceEvent } from './provenance-chain';
|
|
2
3
|
/** One governance event, projected to what a timeline shows — chain-integrity fields dropped. */
|
|
3
4
|
export interface TimelineEntry {
|
|
5
|
+
execution?: ExecutionContext;
|
|
4
6
|
ts: string;
|
|
5
7
|
kind: string;
|
|
6
8
|
actor: string;
|
|
@@ -9,7 +11,7 @@ export interface TimelineEntry {
|
|
|
9
11
|
}
|
|
10
12
|
/** The subset of a ledger event this projection reads (structurally typed so callers can pass
|
|
11
13
|
* FileLedgerStore.loadAll() results directly). */
|
|
12
|
-
type TimelineSource = Pick<ProvenanceEvent, 'ts' | 'kind' | 'actor' | 'summary'> & Partial<Pick<ProvenanceEvent, 'inputs' | 'seq'>>;
|
|
14
|
+
type TimelineSource = Pick<ProvenanceEvent, 'ts' | 'kind' | 'actor' | 'summary'> & Partial<Pick<ProvenanceEvent, 'inputs' | 'seq' | 'execution'>>;
|
|
13
15
|
/**
|
|
14
16
|
* @implements A-SPEC-538.3
|
|
15
17
|
* Pure: order ledger events into a timeline, optionally narrowed to one spec. Ascending by `ts`
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.timelineFrom = timelineFrom;
|
|
4
|
+
// @implements A-SPEC-538.3, A-SPEC-624
|
|
5
|
+
// @implements A-SPEC-645
|
|
6
|
+
const execution_context_1 = require("../project/execution-context");
|
|
7
|
+
const display_id_aliases_1 = require("./display-id-aliases");
|
|
4
8
|
/**
|
|
5
9
|
* @implements A-SPEC-538.3
|
|
6
10
|
* Pure: order ledger events into a timeline, optionally narrowed to one spec. Ascending by `ts`
|
|
@@ -9,7 +13,12 @@ exports.timelineFrom = timelineFrom;
|
|
|
9
13
|
* (hash/prevHash/seq/replicaId) are plumbing, not timeline content, and are projected away.
|
|
10
14
|
*/
|
|
11
15
|
function timelineFrom(events, id) {
|
|
12
|
-
|
|
16
|
+
// @implements A-SPEC-645 — an id selects the events of every display id the entity has carried,
|
|
17
|
+
// resolved from the same events (renames are ledger records); an id with no history is itself.
|
|
18
|
+
const ids = id ? new Set((0, display_id_aliases_1.aliasesOf)(events, id)) : undefined;
|
|
19
|
+
// A rename record names the ids as `old-id:`/`new-id:` inputs; those reference the entity too.
|
|
20
|
+
const refersTo = (input) => input.startsWith('old-id:') ? input.slice(7) : input.startsWith('new-id:') ? input.slice(7) : input;
|
|
21
|
+
const selected = ids ? events.filter((e) => (e.inputs ?? []).some((input) => ids.has(refersTo(input)))) : events.slice();
|
|
13
22
|
selected.sort((a, b) => (a.ts < b.ts ? -1 : a.ts > b.ts ? 1 : (a.seq ?? 0) - (b.seq ?? 0)));
|
|
14
23
|
return selected.map((e) => ({
|
|
15
24
|
ts: e.ts,
|
|
@@ -17,5 +26,6 @@ function timelineFrom(events, id) {
|
|
|
17
26
|
actor: e.actor,
|
|
18
27
|
summary: e.summary,
|
|
19
28
|
inputs: e.inputs ?? [],
|
|
29
|
+
...('execution' in e ? { execution: (0, execution_context_1.validateExecution)(e.execution) } : {}),
|
|
20
30
|
}));
|
|
21
31
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ExecutionContext } from '../project/execution-context';
|
|
1
2
|
import { LockOptions } from './ledger-lock';
|
|
2
3
|
/**
|
|
3
4
|
* N3 — Decision PROVENANCE chain (target-architecture §7-N N3): "누가·언제·왜"의 완전 인과 재구성.
|
|
@@ -18,6 +19,7 @@ import { LockOptions } from './ledger-lock';
|
|
|
18
19
|
* returning [] would erase the audit trail this exists to guarantee — same policy as DecisionLedger).
|
|
19
20
|
*/
|
|
20
21
|
export interface ProvenanceEvent {
|
|
22
|
+
execution?: ExecutionContext;
|
|
21
23
|
seq: number;
|
|
22
24
|
ts: string;
|
|
23
25
|
actor: string;
|
|
@@ -50,7 +50,8 @@ exports.keyedSelfConsistent = keyedSelfConsistent;
|
|
|
50
50
|
exports.bodyCanon = bodyCanon;
|
|
51
51
|
exports.parseLedgerLines = parseLedgerLines;
|
|
52
52
|
exports.consumeNonceExclusively = consumeNonceExclusively;
|
|
53
|
-
// @implements A-SPEC-125.2
|
|
53
|
+
// @implements A-SPEC-125.2, A-SPEC-624
|
|
54
|
+
const execution_context_1 = require("../project/execution-context");
|
|
54
55
|
const crypto = __importStar(require("node:crypto"));
|
|
55
56
|
const fs = __importStar(require("node:fs"));
|
|
56
57
|
const path = __importStar(require("node:path"));
|
|
@@ -61,6 +62,7 @@ function canonicalize(b) {
|
|
|
61
62
|
return JSON.stringify({
|
|
62
63
|
seq: b.seq, ts: b.ts, actor: b.actor, kind: b.kind, summary: b.summary,
|
|
63
64
|
inputs: b.inputs ?? [], rationale: b.rationale ?? '', authorization: b.authorization ?? '',
|
|
65
|
+
...('execution' in b ? { execution: (0, execution_context_1.validateExecution)(b.execution) } : {}),
|
|
64
66
|
});
|
|
65
67
|
}
|
|
66
68
|
/**
|
|
@@ -83,7 +85,7 @@ function chainNext(chain, body, key) {
|
|
|
83
85
|
const full = { seq: chain.length, ...body };
|
|
84
86
|
return { ...full, prevHash, hash: computeHash(prevHash, full, key) };
|
|
85
87
|
}
|
|
86
|
-
const KNOWN_FIELDS = new Set(['seq', 'ts', 'actor', 'kind', 'summary', 'inputs', 'rationale', 'authorization', 'prevHash', 'hash']);
|
|
88
|
+
const KNOWN_FIELDS = new Set(['seq', 'ts', 'actor', 'kind', 'summary', 'inputs', 'rationale', 'authorization', 'prevHash', 'hash', 'execution']);
|
|
87
89
|
/**
|
|
88
90
|
* Verify the whole chain: recomputed hashes, prevHash linkage, contiguous seq, AND no unknown fields.
|
|
89
91
|
* The unknown-field check closes the canonicalization-gap attack (adversarial review #2): canonicalize
|
|
@@ -102,6 +104,14 @@ function verifyChain(chain, key) {
|
|
|
102
104
|
let keyedSeen = false;
|
|
103
105
|
for (let i = 0; i < chain.length; i++) {
|
|
104
106
|
const e = chain[i];
|
|
107
|
+
if ('execution' in e) {
|
|
108
|
+
try {
|
|
109
|
+
(0, execution_context_1.validateExecution)(e.execution);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return { ok: false, brokenAt: i, detail: 'invalid execution context' };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
105
115
|
const unknown = Object.keys(e).filter((k) => !KNOWN_FIELDS.has(k));
|
|
106
116
|
if (unknown.length > 0)
|
|
107
117
|
return { ok: false, brokenAt: i, detail: `unknown field(s) outside the hashed schema: ${unknown.join(', ')} (smuggled data)` };
|
|
@@ -276,6 +286,7 @@ function bodyCanon(b) {
|
|
|
276
286
|
return JSON.stringify({
|
|
277
287
|
ts: b.ts, actor: b.actor, kind: b.kind, summary: b.summary,
|
|
278
288
|
inputs: b.inputs ?? [], rationale: b.rationale ?? '', authorization: b.authorization ?? '',
|
|
289
|
+
...('execution' in b ? { execution: (0, execution_context_1.validateExecution)(b.execution) } : {}),
|
|
279
290
|
});
|
|
280
291
|
}
|
|
281
292
|
/**
|