@holmes-lab/holmes-kit 0.19.5 → 0.20.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 +135 -0
- package/README.md +17 -1
- 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/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/playbook-skills.d.ts +2 -22
- package/dist/holmes/cli/playbook-skills.js +10 -54
- 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 +86 -12
- 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 +55 -0
- package/dist/holmes/mcp/handlers/slice-orchestration.js +324 -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 +653 -424
- package/dist/holmes/mcp/handlers.js +267 -3250
- package/dist/holmes/mcp/server.js +2 -1
- 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 +120 -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 +3 -1
- package/dist/holmes/project/root.js +4 -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 +132 -4
- 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/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 +207 -0
- package/dist/holmes/spec/entity-integration.js +747 -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 +1051 -0
- package/dist/holmes/spec/entity-transaction.d.ts +89 -0
- package/dist/holmes/spec/entity-transaction.js +701 -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 +1 -1
|
@@ -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
|
/**
|
|
@@ -25,6 +25,28 @@ export interface LedgerEvent extends ProvenanceEvent {
|
|
|
25
25
|
/** Which chain this came from; `null` for the pre-split legacy file. */
|
|
26
26
|
replicaId: string | null;
|
|
27
27
|
}
|
|
28
|
+
/** A-SPEC-642 — one single-use nonce consumed by several events (any replicas), not yet reconciled. */
|
|
29
|
+
export interface DoubleSpend {
|
|
30
|
+
nonce: string;
|
|
31
|
+
events: {
|
|
32
|
+
replicaId: string | null;
|
|
33
|
+
seq: number;
|
|
34
|
+
hash: string;
|
|
35
|
+
ts: string;
|
|
36
|
+
}[];
|
|
37
|
+
}
|
|
38
|
+
/** A-SPEC-642 — the event kind an operator's reconciliation appends. */
|
|
39
|
+
export declare const DOUBLE_SPEND_RECONCILED = "nonce-double-spend-reconciled";
|
|
40
|
+
/**
|
|
41
|
+
* @implements A-SPEC-642
|
|
42
|
+
* Single-use nonces consumed by two or more `nonce-consumed` events (on any replicas) that no
|
|
43
|
+
* `nonce-double-spend-reconciled` event has covered. Pure and hash-free on purpose: the gate and
|
|
44
|
+
* the approval channel ask this on every guarded act, at the cost `isNonceConsumed` already pays;
|
|
45
|
+
* chain integrity stays with `verifyAll`. A reconciliation covers a group only when it names the
|
|
46
|
+
* nonce input AND `event:<hash>` for every member — a later consumption of the same nonce is a new
|
|
47
|
+
* event the old reconciliation never saw, so the group is open again.
|
|
48
|
+
*/
|
|
49
|
+
export declare function unreconciledDoubleSpends(events: LedgerEvent[]): DoubleSpend[];
|
|
28
50
|
export interface LedgerVerifyResult {
|
|
29
51
|
ok: boolean;
|
|
30
52
|
/** Per-chain failures, naming the replica so a report is actionable rather than merely alarming. */
|
|
@@ -33,6 +55,9 @@ export interface LedgerVerifyResult {
|
|
|
33
55
|
file: string;
|
|
34
56
|
detail?: string;
|
|
35
57
|
brokenAt?: number;
|
|
58
|
+
kind?: 'chain' | 'double-spend';
|
|
59
|
+
nonce?: string;
|
|
60
|
+
events?: DoubleSpend['events'];
|
|
36
61
|
}[];
|
|
37
62
|
}
|
|
38
63
|
/**
|
|
@@ -72,6 +97,7 @@ export declare class ProvenanceLedger {
|
|
|
72
97
|
* `.ax/state/`, which is the REQ-148 footprint defect returning through a new door.
|
|
73
98
|
*/
|
|
74
99
|
replicaIdOf(): string;
|
|
100
|
+
executionContext(): import("../project/execution-context").ExecutionContext | undefined;
|
|
75
101
|
/** Every chain file present, legacy first. `replicaId` is null for the legacy chain. */
|
|
76
102
|
chains(): {
|
|
77
103
|
replicaId: string | null;
|
|
@@ -89,6 +115,8 @@ export declare class ProvenanceLedger {
|
|
|
89
115
|
* ordering exists so output is stable and readable, not so it can be read as "this happened first".
|
|
90
116
|
*/
|
|
91
117
|
loadAll(): LedgerEvent[];
|
|
118
|
+
/** @implements A-SPEC-642 — unreconciled cross-replica double-spends, structured. */
|
|
119
|
+
doubleSpends(): DoubleSpend[];
|
|
92
120
|
/** Each chain verified on its own; one bad chain fails the whole ledger, and is named. */
|
|
93
121
|
verifyAll(): LedgerVerifyResult;
|
|
94
122
|
/**
|
|
@@ -33,14 +33,50 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.ProvenanceLedger = void 0;
|
|
36
|
+
exports.ProvenanceLedger = exports.DOUBLE_SPEND_RECONCILED = void 0;
|
|
37
|
+
exports.unreconciledDoubleSpends = unreconciledDoubleSpends;
|
|
37
38
|
exports.isLedgerFilename = isLedgerFilename;
|
|
38
|
-
// @implements A-SPEC-206
|
|
39
|
+
// @implements A-SPEC-206, A-SPEC-624
|
|
40
|
+
// @implements A-SPEC-642
|
|
41
|
+
const execution_context_1 = require("../project/execution-context");
|
|
39
42
|
// @implements A-SPEC-148
|
|
40
43
|
const fs = __importStar(require("node:fs"));
|
|
41
44
|
const path = __importStar(require("node:path"));
|
|
42
45
|
const provenance_chain_1 = require("./provenance-chain");
|
|
43
46
|
const replica_id_1 = require("./replica-id");
|
|
47
|
+
/** A-SPEC-642 — the event kind an operator's reconciliation appends. */
|
|
48
|
+
exports.DOUBLE_SPEND_RECONCILED = 'nonce-double-spend-reconciled';
|
|
49
|
+
/**
|
|
50
|
+
* @implements A-SPEC-642
|
|
51
|
+
* Single-use nonces consumed by two or more `nonce-consumed` events (on any replicas) that no
|
|
52
|
+
* `nonce-double-spend-reconciled` event has covered. Pure and hash-free on purpose: the gate and
|
|
53
|
+
* the approval channel ask this on every guarded act, at the cost `isNonceConsumed` already pays;
|
|
54
|
+
* chain integrity stays with `verifyAll`. A reconciliation covers a group only when it names the
|
|
55
|
+
* nonce input AND `event:<hash>` for every member — a later consumption of the same nonce is a new
|
|
56
|
+
* event the old reconciliation never saw, so the group is open again.
|
|
57
|
+
*/
|
|
58
|
+
function unreconciledDoubleSpends(events) {
|
|
59
|
+
const groups = new Map();
|
|
60
|
+
for (const e of events) {
|
|
61
|
+
if (e.kind !== 'nonce-consumed')
|
|
62
|
+
continue;
|
|
63
|
+
for (const input of e.inputs ?? []) {
|
|
64
|
+
const list = groups.get(input) ?? [];
|
|
65
|
+
list.push({ replicaId: e.replicaId, seq: e.seq, hash: e.hash, ts: e.ts });
|
|
66
|
+
groups.set(input, list);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const reconciliations = events.filter((e) => e.kind === exports.DOUBLE_SPEND_RECONCILED).map((e) => new Set(e.inputs ?? []));
|
|
70
|
+
const out = [];
|
|
71
|
+
for (const [nonce, list] of groups) {
|
|
72
|
+
if (list.length < 2)
|
|
73
|
+
continue;
|
|
74
|
+
const covered = reconciliations.some((inputs) => inputs.has(nonce) && list.every((ev) => inputs.has('event:' + ev.hash)));
|
|
75
|
+
if (!covered)
|
|
76
|
+
out.push({ nonce, events: list });
|
|
77
|
+
}
|
|
78
|
+
return out.sort((a, b) => (a.nonce < b.nonce ? -1 : a.nonce > b.nonce ? 1 : 0));
|
|
79
|
+
}
|
|
44
80
|
const LEGACY = 'provenance.jsonl';
|
|
45
81
|
const REPLICA_FILE = /^provenance\.([^.]+)\.jsonl$/;
|
|
46
82
|
/**
|
|
@@ -85,6 +121,9 @@ class ProvenanceLedger {
|
|
|
85
121
|
}
|
|
86
122
|
cachedId;
|
|
87
123
|
get replicaId() {
|
|
124
|
+
const execution = this.executionContext();
|
|
125
|
+
if (execution)
|
|
126
|
+
return execution.replicaId;
|
|
88
127
|
if (this.cachedId === undefined)
|
|
89
128
|
this.cachedId = this.replicaIdFn();
|
|
90
129
|
return this.cachedId;
|
|
@@ -107,8 +146,12 @@ class ProvenanceLedger {
|
|
|
107
146
|
* `.ax/state/`, which is the REQ-148 footprint defect returning through a new door.
|
|
108
147
|
*/
|
|
109
148
|
replicaIdOf() {
|
|
149
|
+
const execution = this.executionContext();
|
|
150
|
+
if (execution)
|
|
151
|
+
return execution.replicaId;
|
|
110
152
|
return this.cachedId ?? (0, replica_id_1.resolveReplicaId)(rootForLedgerDir(this.dir), process.env, { persist: false });
|
|
111
153
|
}
|
|
154
|
+
executionContext() { return (0, execution_context_1.executionForRoot)(rootForLedgerDir(this.dir)); }
|
|
112
155
|
/** Every chain file present, legacy first. `replicaId` is null for the legacy chain. */
|
|
113
156
|
chains() {
|
|
114
157
|
let names;
|
|
@@ -149,6 +192,7 @@ class ProvenanceLedger {
|
|
|
149
192
|
return events.map((e) => ({
|
|
150
193
|
...e,
|
|
151
194
|
inputs: e.inputs ? [...e.inputs] : undefined,
|
|
195
|
+
...(e.execution ? { execution: { ...e.execution } } : {}),
|
|
152
196
|
}));
|
|
153
197
|
}
|
|
154
198
|
/**
|
|
@@ -175,13 +219,20 @@ class ProvenanceLedger {
|
|
|
175
219
|
this.indexCache = { key, events: out };
|
|
176
220
|
return this.cloneEvents(out);
|
|
177
221
|
}
|
|
222
|
+
/** @implements A-SPEC-642 — unreconciled cross-replica double-spends, structured. */
|
|
223
|
+
doubleSpends() { return unreconciledDoubleSpends(this.loadAll()); }
|
|
178
224
|
/** Each chain verified on its own; one bad chain fails the whole ledger, and is named. */
|
|
179
225
|
verifyAll() {
|
|
180
226
|
const broken = [];
|
|
181
227
|
for (const c of this.chains()) {
|
|
182
228
|
const r = new provenance_chain_1.ProvenanceChain(c.file).verify();
|
|
183
229
|
if (!r.ok)
|
|
184
|
-
broken.push({ replicaId: c.replicaId, file: c.file, detail: r.detail, brokenAt: r.brokenAt });
|
|
230
|
+
broken.push({ replicaId: c.replicaId, file: c.file, kind: 'chain', detail: r.detail, brokenAt: r.brokenAt });
|
|
231
|
+
if (r.ok) {
|
|
232
|
+
const mismatch = new provenance_chain_1.ProvenanceChain(c.file).load().find(e => e.execution && e.execution.replicaId !== c.replicaId);
|
|
233
|
+
if (mismatch)
|
|
234
|
+
broken.push({ replicaId: c.replicaId, file: c.file, kind: 'chain', detail: 'execution replica differs from chain partition', brokenAt: mismatch.seq });
|
|
235
|
+
}
|
|
185
236
|
}
|
|
186
237
|
// @implements A-SPEC-458
|
|
187
238
|
// Cross-replica double-spend. consumeNonceExclusively's lock is per-file, so two machines can
|
|
@@ -191,18 +242,14 @@ class ProvenanceLedger {
|
|
|
191
242
|
// records is always a violation (REQ-141): there is no legitimate re-spend. Counted by the raw
|
|
192
243
|
// input value, the same rule isNonceConsumed matches on — one consume path writes one form, so
|
|
193
244
|
// a real double-spend records the same value twice.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (n > 1) {
|
|
203
|
-
broken.push({ replicaId: null, file: '(cross-replica)',
|
|
204
|
-
detail: `single-use nonce ${nonce.slice(0, 16)}… spent ${n} times across replicas — REQ-141 double-spend` });
|
|
205
|
-
}
|
|
245
|
+
// @implements A-SPEC-642 — structured, and RECONCILABLE: an operator's `nonce-double-spend-reconciled`
|
|
246
|
+
// event naming the nonce and every consuming event's hash retires the group; a later consumption
|
|
247
|
+
// reopens it. Measured 2026-09-14: the prose-only entry left the Stop hook blocking every turn
|
|
248
|
+
// end "at seq undefined" with no act that could ever clear it.
|
|
249
|
+
for (const d of unreconciledDoubleSpends(this.loadAll())) {
|
|
250
|
+
const replicas = [...new Set(d.events.map((e) => e.replicaId ?? 'legacy'))].join(', ');
|
|
251
|
+
broken.push({ replicaId: null, file: '(cross-replica)', kind: 'double-spend', nonce: d.nonce, events: d.events,
|
|
252
|
+
detail: `single-use nonce ${d.nonce} spent ${d.events.length} times across replicas (${replicas}) — REQ-141 double-spend; reconcile with ledger_reconcile` });
|
|
206
253
|
}
|
|
207
254
|
return { ok: broken.length === 0, broken };
|
|
208
255
|
}
|
|
@@ -213,8 +260,14 @@ class ProvenanceLedger {
|
|
|
213
260
|
* which is the defect this class exists to remove.
|
|
214
261
|
*/
|
|
215
262
|
append(body) {
|
|
263
|
+
const execution = this.executionContext();
|
|
264
|
+
if ('execution' in body) {
|
|
265
|
+
const supplied = (0, execution_context_1.validateExecution)(body.execution);
|
|
266
|
+
if (execution && JSON.stringify(supplied) !== JSON.stringify(execution))
|
|
267
|
+
throw new Error('Supplied execution differs from the owned run');
|
|
268
|
+
}
|
|
216
269
|
this.indexCache = undefined;
|
|
217
|
-
return new provenance_chain_1.ProvenanceChain(this.currentFile()).append(body);
|
|
270
|
+
return new provenance_chain_1.ProvenanceChain(this.currentFile()).append(execution ? { ...body, execution } : body);
|
|
218
271
|
}
|
|
219
272
|
/**
|
|
220
273
|
* Has this nonce been spent on ANY replica?
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ExecutionContext } from '../project/execution-context';
|
|
1
2
|
/**
|
|
2
3
|
* The session-context ledger: WHO attached (agent name@version) and — where a harness exposes it —
|
|
3
4
|
* WITH WHAT (model ids, token totals). REQ-564's whole point is that none of this existed anywhere
|
|
@@ -21,6 +22,7 @@ export interface UsageTotals {
|
|
|
21
22
|
cacheCreation: number;
|
|
22
23
|
}
|
|
23
24
|
export interface ClientRecord {
|
|
25
|
+
execution?: ExecutionContext;
|
|
24
26
|
kind: 'client';
|
|
25
27
|
sessionKey: string;
|
|
26
28
|
client: string;
|
|
@@ -29,6 +31,7 @@ export interface ClientRecord {
|
|
|
29
31
|
replica?: string;
|
|
30
32
|
}
|
|
31
33
|
export interface UsageRecord {
|
|
34
|
+
execution?: ExecutionContext;
|
|
32
35
|
kind: 'usage';
|
|
33
36
|
sessionKey: string;
|
|
34
37
|
models: string[];
|
|
@@ -39,7 +39,8 @@ exports.appendSessionContext = appendSessionContext;
|
|
|
39
39
|
exports.readSessionContext = readSessionContext;
|
|
40
40
|
exports.makeSessionStamper = makeSessionStamper;
|
|
41
41
|
exports.summarizeTranscript = summarizeTranscript;
|
|
42
|
-
// @implements A-SPEC-564.1
|
|
42
|
+
// @implements A-SPEC-564.1, A-SPEC-625
|
|
43
|
+
const execution_context_1 = require("../project/execution-context");
|
|
43
44
|
const fs = __importStar(require("node:fs"));
|
|
44
45
|
const path = __importStar(require("node:path"));
|
|
45
46
|
const replica_id_1 = require("./replica-id");
|
|
@@ -56,14 +57,19 @@ function appendSessionContext(root, rec) {
|
|
|
56
57
|
try {
|
|
57
58
|
if (!fs.existsSync(path.join(root, '.ax')))
|
|
58
59
|
return false;
|
|
60
|
+
const execution = (0, execution_context_1.observationExecution)(root, rec);
|
|
59
61
|
let replica = 'local';
|
|
60
|
-
|
|
61
|
-
replica =
|
|
62
|
+
if (execution)
|
|
63
|
+
replica = execution.replicaId;
|
|
64
|
+
else {
|
|
65
|
+
try {
|
|
66
|
+
replica = (0, replica_id_1.resolveReplicaId)(root) || 'local';
|
|
67
|
+
}
|
|
68
|
+
catch { /* keep the fallback */ }
|
|
62
69
|
}
|
|
63
|
-
catch { /* keep the fallback */ }
|
|
64
70
|
const file = path.join(root, '.ax', 'ledger', sessionContextFilename(replica));
|
|
65
71
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
66
|
-
fs.appendFileSync(file, `${JSON.stringify({ ...rec, replica })}\n`);
|
|
72
|
+
fs.appendFileSync(file, `${JSON.stringify({ ...rec, replica, ...(execution ? { execution } : {}) })}\n`);
|
|
67
73
|
return true;
|
|
68
74
|
}
|
|
69
75
|
catch {
|
|
@@ -98,6 +104,11 @@ function readSessionContext(root) {
|
|
|
98
104
|
const r = JSON.parse(s);
|
|
99
105
|
if (r && typeof r === 'object' && (r.kind === 'client' || r.kind === 'usage')
|
|
100
106
|
&& typeof r.sessionKey === 'string' && typeof r.ts === 'string') {
|
|
107
|
+
if ('execution' in r) {
|
|
108
|
+
r.execution = (0, execution_context_1.validateExecution)(r.execution);
|
|
109
|
+
if (name !== sessionContextFilename(r.execution.replicaId) || r.replica !== r.execution.replicaId)
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
101
112
|
out.push({ r: r, i: i++ });
|
|
102
113
|
}
|
|
103
114
|
}
|
|
@@ -114,24 +125,31 @@ function readSessionContext(root) {
|
|
|
114
125
|
* session has one identity; a root-less call defers rather than consumes the chance.
|
|
115
126
|
*/
|
|
116
127
|
function makeSessionStamper(info, append = appendSessionContext, sessionKey = `mcp-${process.pid}`) {
|
|
117
|
-
|
|
128
|
+
const stamped = new Set();
|
|
118
129
|
return (root) => {
|
|
119
|
-
if (
|
|
130
|
+
if (typeof root !== 'string' || root === '')
|
|
120
131
|
return;
|
|
121
132
|
try {
|
|
133
|
+
const execution = (0, execution_context_1.executionForRoot)(root);
|
|
134
|
+
const key = execution ? JSON.stringify(execution) : 'legacy';
|
|
135
|
+
if (stamped.has(key))
|
|
136
|
+
return;
|
|
122
137
|
const v = (() => { try {
|
|
123
138
|
return info();
|
|
124
139
|
}
|
|
125
140
|
catch {
|
|
126
141
|
return undefined;
|
|
127
142
|
} })();
|
|
128
|
-
|
|
143
|
+
const success = append(root, {
|
|
129
144
|
kind: 'client',
|
|
130
145
|
sessionKey,
|
|
131
146
|
client: v?.name ?? 'unknown',
|
|
132
147
|
clientVersion: v?.version ?? 'unknown',
|
|
133
148
|
ts: new Date().toISOString(),
|
|
149
|
+
...(execution ? { execution } : {}),
|
|
134
150
|
});
|
|
151
|
+
if (success)
|
|
152
|
+
stamped.add(key);
|
|
135
153
|
}
|
|
136
154
|
catch { /* observation must never break the observed call */ }
|
|
137
155
|
};
|
|
@@ -8,7 +8,7 @@ exports.classifyArchitectureReversal = classifyArchitectureReversal;
|
|
|
8
8
|
exports.classifySecret = classifySecret;
|
|
9
9
|
exports.classifyBlastRadius = classifyBlastRadius;
|
|
10
10
|
exports.assessRisk = assessRisk;
|
|
11
|
-
// @implements A-SPEC-125.1
|
|
11
|
+
// @implements A-SPEC-125.1, A-SPEC-627
|
|
12
12
|
const risk_types_1 = require("./risk-types");
|
|
13
13
|
const AWS_KEY_RE = /AKIA[0-9A-Z]{16}/;
|
|
14
14
|
const PEM_KEY_RE = /-----BEGIN [A-Z ]*PRIVATE KEY-----/;
|
|
@@ -263,7 +263,7 @@ function scopeAxToProject(cmd, projectRoot) {
|
|
|
263
263
|
// 'echo x > .ax/approvals/grants/forged.json' passed while the Write gate refused the same file —
|
|
264
264
|
// the exact two-copies drift this repository has paid for with isTestFile and a restated section
|
|
265
265
|
// splitter. The directories are now written once; each rule keeps its own shape around them.
|
|
266
|
-
const PROTECTED_AX_ALT = 'specs|decisions|ledger|cpg_cache|state|roles|approvals';
|
|
266
|
+
const PROTECTED_AX_ALT = 'specs|decisions|ledger|cpg_cache|state|roles|approvals|entities';
|
|
267
267
|
const PROTECTED_AX_PATH_RE = new RegExp(String.raw `(^|/)\.ax/(${PROTECTED_AX_ALT})(/|$)`);
|
|
268
268
|
// A removal verb + a protected .ax path anywhere in a shell command — the command form of
|
|
269
269
|
// the governance-path delete, which the structured `kind:'delete'` branch alone would miss.
|
|
@@ -45,7 +45,7 @@ exports.protectedFileKindOf = protectedFileKindOf;
|
|
|
45
45
|
exports.protectedKindOf = protectedKindOf;
|
|
46
46
|
exports.shellSegments = shellSegments;
|
|
47
47
|
exports.resolvesInside = resolvesInside;
|
|
48
|
-
// @implements A-SPEC-163
|
|
48
|
+
// @implements A-SPEC-163, A-SPEC-627
|
|
49
49
|
const fs = __importStar(require("node:fs"));
|
|
50
50
|
const path = __importStar(require("node:path"));
|
|
51
51
|
const os = __importStar(require("node:os"));
|
|
@@ -193,6 +193,7 @@ const PROTECTED_DIRS = [
|
|
|
193
193
|
path.join('.ax', 'roles'),
|
|
194
194
|
path.join('.ax', 'cpg_cache'),
|
|
195
195
|
path.join('.ax', 'state'),
|
|
196
|
+
path.join('.ax', 'entities'),
|
|
196
197
|
'.claude',
|
|
197
198
|
];
|
|
198
199
|
/**
|
|
@@ -46,6 +46,8 @@ exports.wiredSpecsDir = wiredSpecsDir;
|
|
|
46
46
|
exports.evaluateHook = evaluateHook;
|
|
47
47
|
exports.readSpecsSync = readSpecsSync;
|
|
48
48
|
// @implements A-SPEC-278
|
|
49
|
+
// @implements A-SPEC-643
|
|
50
|
+
// @implements A-SPEC-642
|
|
49
51
|
// @implements A-SPEC-194, A-SPEC-195
|
|
50
52
|
const registry_1 = require("../messages/registry");
|
|
51
53
|
const root_1 = require("../project/root");
|
|
@@ -410,7 +412,6 @@ exports.DEFAULT_SPECS_DIR = '.ax/specs';
|
|
|
410
412
|
function evaluateHook(input, specsDir, opts) {
|
|
411
413
|
// @implements A-SPEC-133 — resolved once so every gate in this call shares one timestamp.
|
|
412
414
|
const nowTs = opts?.now ?? new Date().toISOString();
|
|
413
|
-
const ledgerFile = path.join(opts.projectRoot, '.ax', 'ledger', 'provenance.jsonl');
|
|
414
415
|
// @implements A-SPEC-125.4
|
|
415
416
|
// Bash/shell risk gate: this branch runs BEFORE the Write/Edit phaseCheck logic below and
|
|
416
417
|
// is intentionally fail-CLOSED — a classifier throw on the Bash surface denies, never
|
|
@@ -483,6 +484,16 @@ function evaluateHook(input, specsDir, opts) {
|
|
|
483
484
|
// creating `.ax/ledger` is the wiring working, not a plant. Refusing there would deny the
|
|
484
485
|
// first legitimate use in every new project. The single-use guarantee still holds: the record
|
|
485
486
|
// lands in the resolved root, which is the same file every later call resolves to.
|
|
487
|
+
// @implements A-SPEC-643 — the writer's replica chain, resolved only on this path so ordinary hook
|
|
488
|
+
// calls leave no footprint; the spent check below still spans every chain in the directory.
|
|
489
|
+
const ledgerFile = (0, ledger_store_2.writerChainFile)(path.join(opts.projectRoot, '.ax', 'ledger'));
|
|
490
|
+
// @implements A-SPEC-642 — a covered command is still denied, and no nonce spent, while the
|
|
491
|
+
// merged ledger holds an unreconciled cross-replica double-spend; the reason names the exit.
|
|
492
|
+
{
|
|
493
|
+
const conflict = (0, ledger_store_2.ledgerConflictReason)(path.dirname(ledgerFile));
|
|
494
|
+
if (conflict)
|
|
495
|
+
return deny(conflict);
|
|
496
|
+
}
|
|
486
497
|
if ((0, provenance_chain_1.blankNonce)(acting?.nonce)) {
|
|
487
498
|
return deny('the approval declared single-use (nonce) but the value is empty — single-use cannot be enforced, so it is denied');
|
|
488
499
|
}
|
|
@@ -53,6 +53,8 @@ exports.guardCountOrZero = guardCountOrZero;
|
|
|
53
53
|
exports.readGuardCount = readGuardCount;
|
|
54
54
|
exports.writeGuardCount = writeGuardCount;
|
|
55
55
|
const fs = __importStar(require("node:fs"));
|
|
56
|
+
// @implements A-SPEC-645
|
|
57
|
+
// @implements A-SPEC-642
|
|
56
58
|
const npx_bin_1 = require("../project/npx-bin");
|
|
57
59
|
const approval_queue_1 = require("../governance/approval-queue");
|
|
58
60
|
const dependency_delta_1 = require("../guardrail/dependency-delta");
|
|
@@ -68,6 +70,7 @@ const cycle_detect_1 = require("../cpg/cycle-detect");
|
|
|
68
70
|
const provenance_chain_1 = require("../governance/provenance-chain");
|
|
69
71
|
const test_evidence_1 = require("../review/test-evidence");
|
|
70
72
|
const test_outcomes_1 = require("../review/test-outcomes");
|
|
73
|
+
const display_id_aliases_1 = require("../governance/display-id-aliases");
|
|
71
74
|
const config_1 = require("../config/config");
|
|
72
75
|
const pre_tool_use_1 = require("./pre-tool-use");
|
|
73
76
|
const governance_history_1 = require("../guardrail/governance-history");
|
|
@@ -501,8 +504,12 @@ function evaluateStop(specs, evidence) {
|
|
|
501
504
|
// Tampered audit trail blocks finishing — wires the provenance chain's verify into the live gate
|
|
502
505
|
// (review finding: verify() previously had no product caller, so the chain's guarantee was inert).
|
|
503
506
|
if (evidence?.provenance && !evidence.provenance.ok) {
|
|
504
|
-
|
|
505
|
-
|
|
507
|
+
// @implements A-SPEC-642 — a cross-replica double-spend has no sequence; "seq undefined" hid that.
|
|
508
|
+
const provenanceDetail = evidence.provenance.brokenAt === undefined
|
|
509
|
+
? `provenance ledger conflict: ${evidence.provenance.detail}`
|
|
510
|
+
: `provenance chain broken at seq ${evidence.provenance.brokenAt}: ${evidence.provenance.detail}`;
|
|
511
|
+
problems.push(`[ART-2] ${provenanceDetail}`);
|
|
512
|
+
structured.push({ article: 'ART-2', detail: provenanceDetail });
|
|
506
513
|
}
|
|
507
514
|
// @implements A-SPEC-191 (§4a) — an existing-but-unreadable findings ledger is not a clean turn:
|
|
508
515
|
// it may hold an open critical, and a "clean" verdict here would also CLEAR standing ART-7 debt.
|
|
@@ -913,7 +920,11 @@ if (require.main === module) {
|
|
|
913
920
|
if (redFirstMode !== 'off') {
|
|
914
921
|
const head = (0, node_child_process_1.execFileSync)('git', ['rev-parse', 'HEAD'], { cwd: stopProjectRoot(), stdio: ['ignore', 'pipe', 'ignore'], env: (0, root_1.cleanSubprocessEnv)() }).toString().trim();
|
|
915
922
|
changedAspecs = changedAnchoredAspecs(stopProjectRoot());
|
|
916
|
-
|
|
923
|
+
// @implements A-SPEC-645 — a renumbered A-SPEC keeps its red-first evidence: records are
|
|
924
|
+
// grouped under the display id the entity carries now, per the ledger's rename records.
|
|
925
|
+
const { FileLedgerStore: LedgerForAliases } = require('../governance/ledger-store');
|
|
926
|
+
const canonical = (0, display_id_aliases_1.displayIdCanonicalizer)(new LedgerForAliases(path.join(stopProjectRoot(), '.ax', 'ledger')).loadAll());
|
|
927
|
+
outcomesByAspec = (0, test_outcomes_1.groupOutcomesByAspec)((0, test_outcomes_1.readOutcomes)(stopProjectRoot()), head, canonical);
|
|
917
928
|
}
|
|
918
929
|
}
|
|
919
930
|
catch {
|