@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
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createAdoptionHandlers = createAdoptionHandlers;
|
|
4
|
+
const spec_store_1 = require("../../spec/spec-store");
|
|
5
|
+
const validator_1 = require("../../spec/validator");
|
|
6
|
+
const legacy_format_1 = require("../../spec/legacy-format");
|
|
7
|
+
const approval_blockers_1 = require("../../spec/approval-blockers");
|
|
8
|
+
const scan_1 = require("../../reverse/scan");
|
|
9
|
+
const draft_1 = require("../../reverse/draft");
|
|
10
|
+
const anchor_1 = require("../../reverse/anchor");
|
|
11
|
+
function createAdoptionHandlers(context) {
|
|
12
|
+
return {
|
|
13
|
+
// @implements A-SPEC-126
|
|
14
|
+
// Brownfield reverse engineering. Three explicitly-invoked tools, nothing running as a side
|
|
15
|
+
// effect of an ordinary session, and every write path opt-in: scan writes nothing at all, draft
|
|
16
|
+
// and anchor are dry-run by DEFAULT.
|
|
17
|
+
/**
|
|
18
|
+
* Read-only inventory of a brownfield target: candidate clusters, coverage, and what the scan
|
|
19
|
+
* could not resolve. Deliberately does NOT call assertRepoTopLevel — a target that is not a git
|
|
20
|
+
* repository is a supported case here, reported as `isGit: false`.
|
|
21
|
+
*/
|
|
22
|
+
async reverse_scan(a) {
|
|
23
|
+
// @implements A-SPEC-189 §10 (round 11) — `reverse_scan`/`reverse_draft` never call
|
|
24
|
+
// `projectRootOf`, so §7's refusal marker never reached them and their own POINTED sentence
|
|
25
|
+
// ("<root> is not a directory") arrived at the wire as a raw -32603 fault. A refusal about the
|
|
26
|
+
// caller's own argument is a refusal wherever it is authored.
|
|
27
|
+
context.assertReadableRoot('reverse_scan', a.root);
|
|
28
|
+
// `surfaceByCluster` is drafting evidence, not reading material. On the calibration target it
|
|
29
|
+
// is 11,460 characters across 15 clusters, and this response enters context on every scan —
|
|
30
|
+
// paying that to READ what only DRAFTING consumes. `reverse_draft` reads it in-process instead.
|
|
31
|
+
const { surfaceByCluster: _drafting, ...response } = (0, scan_1.reverseScan)(a.root, { maxFlagged: a.maxFlagged });
|
|
32
|
+
return response;
|
|
33
|
+
},
|
|
34
|
+
/**
|
|
35
|
+
* Draft H-SPEC/A-SPEC/T-SPEC documents for the recovered clusters under an EXISTING parent REQ.
|
|
36
|
+
*
|
|
37
|
+
* The REQ is the human's to write and this tool refuses without one — a requirement states
|
|
38
|
+
* business intent, which is not in the code. Everything emitted is `status: draft`, and writing
|
|
39
|
+
* requires an explicit `dryRun: false`.
|
|
40
|
+
*
|
|
41
|
+
* WHERE THE DOCUMENTS LAND: in the SERVER's configured spec store (`HOLMES_SPECS`, default
|
|
42
|
+
* `.ax/specs` relative to the server's working directory) — not inside `root`. In the intended
|
|
43
|
+
* adoption the two are the same directory, because the server runs inside the target it governs;
|
|
44
|
+
* pointing `root` at a different repository drafts that repository's slices into THIS store,
|
|
45
|
+
* which is a governance decision the caller has to make deliberately.
|
|
46
|
+
*/
|
|
47
|
+
async reverse_draft(a) {
|
|
48
|
+
context.assertReadableRoot('reverse_draft', a.root);
|
|
49
|
+
if (typeof a.parentReqId !== 'string' || a.parentReqId.trim() === '') {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
reason: 'parentReqId is required. A REQ states business intent, which does not exist in the code ' +
|
|
53
|
+
'and cannot be recovered from it — write the REQ first, then re-run with its id.',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const parentReqId = a.parentReqId.trim();
|
|
57
|
+
const specs = await context.listSpecs();
|
|
58
|
+
const req = specs.find((s) => s.id === parentReqId);
|
|
59
|
+
// An unresolvable parent would emit an orphan H-SPEC: a chain that only looks complete.
|
|
60
|
+
if (!req)
|
|
61
|
+
return { ok: false, reason: `parent REQ ${parentReqId} was not found in the spec store — create it first` };
|
|
62
|
+
// @implements A-SPEC-184
|
|
63
|
+
// A document with no `type:` is not a wrong-kind parent — it is an older-format one, and saying
|
|
64
|
+
// "is a undefined" left the adopter holding a visible document with nowhere to go. When the type
|
|
65
|
+
// IS declared, `legacyMessage` returns null and the original wording stands: that path was
|
|
66
|
+
// already accurate and REQ-184 does not touch it.
|
|
67
|
+
const formatWhy = (0, legacy_format_1.legacyMessage)(req);
|
|
68
|
+
if (formatWhy)
|
|
69
|
+
return { ok: false, reason: formatWhy };
|
|
70
|
+
if (req.type !== 'REQ')
|
|
71
|
+
return { ok: false, reason: `${parentReqId} is a ${req.type}; drafts must hang off a REQ` };
|
|
72
|
+
const report = (0, scan_1.reverseScan)(a.root);
|
|
73
|
+
// A cluster already drafted is recognised by the `reverse_cluster` key its documents carry, so
|
|
74
|
+
// re-running over the same tree never duplicates a slice.
|
|
75
|
+
const alreadyDrafted = new Set(specs.map((s) => s.frontmatter.reverse_cluster).filter((k) => typeof k === 'string'));
|
|
76
|
+
const selected = a.cluster ? report.clusters.filter((c) => (0, draft_1.clusterKeyOf)(c) === a.cluster) : report.clusters;
|
|
77
|
+
// A key that matches nothing must not read as success: "drafted 0" is indistinguishable from
|
|
78
|
+
// "everything was already drafted", so a mistyped key would silently skip the work.
|
|
79
|
+
if (a.cluster && selected.length === 0) {
|
|
80
|
+
const keys = report.clusters.map(draft_1.clusterKeyOf);
|
|
81
|
+
return {
|
|
82
|
+
ok: false,
|
|
83
|
+
reason: `no cluster matches key "${a.cluster}" — reverse_scan reports ${keys.length} cluster(s)`,
|
|
84
|
+
availableClusters: keys.slice(0, 50),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
let base = (0, draft_1.nextIdBase)(specs.map((s) => s.id));
|
|
88
|
+
const drafted = [];
|
|
89
|
+
const skipped = [];
|
|
90
|
+
const refused = [];
|
|
91
|
+
for (const cluster of selected) {
|
|
92
|
+
const key = (0, draft_1.clusterKeyOf)(cluster);
|
|
93
|
+
if (alreadyDrafted.has(key)) {
|
|
94
|
+
skipped.push({ clusterKey: key, reason: 'already drafted' });
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const d = (0, draft_1.draftSpecs)(cluster, req.id, String(base), {
|
|
98
|
+
surface: report.surfaceByCluster[key] ?? [],
|
|
99
|
+
testFiles: cluster.testFiles ?? [],
|
|
100
|
+
testsUnmatched: report.coverage.testsUnmatched,
|
|
101
|
+
});
|
|
102
|
+
if (!d.ok) {
|
|
103
|
+
refused.push({ clusterKey: key, reason: d.reason });
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
// Validate BEFORE writing: a draft that fails the project's own validator is not evidence of
|
|
107
|
+
// anything, and emitting one would put a broken document into the chain.
|
|
108
|
+
const known = [...specs, ...drafted.flatMap((x) => x.specs), ...d.specs];
|
|
109
|
+
const resolve = (id) => known.find((s) => s.id === id) ?? null;
|
|
110
|
+
const errors = d.specs.flatMap((s) => (0, validator_1.validateSpec)(s, resolve).findings.filter((f) => f.level === 'error').map((f) => ({ spec: s.id, ...f })));
|
|
111
|
+
if (errors.length) {
|
|
112
|
+
refused.push({ clusterKey: key, reason: 'drafted documents failed validation', findings: errors });
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
drafted.push(d);
|
|
116
|
+
base++;
|
|
117
|
+
}
|
|
118
|
+
const dryRun = a.dryRun !== false; // opt IN to writing
|
|
119
|
+
// @implements A-SPEC-188 — the minted id sits above every PARSEABLE id, but a file list()
|
|
120
|
+
// cannot read is invisible to nextIdBase, so its path can collide with a fresh draft. Same
|
|
121
|
+
// rule as spec_create: a path already occupied is a human's to look at, never overwritten.
|
|
122
|
+
// The whole cluster moves to `refused` — a partially-written chain only looks complete.
|
|
123
|
+
// Judged in the DRY RUN too (round-3): a preview that lists a cluster as drafted which the
|
|
124
|
+
// real run would refuse is a preview that lies.
|
|
125
|
+
const written = [];
|
|
126
|
+
for (const d of drafted) {
|
|
127
|
+
const clash = context.occupiedTarget(d.specs);
|
|
128
|
+
if (clash) {
|
|
129
|
+
refused.push({ clusterKey: d.clusterKey, reason: `초안 목적지(${clash})에 스토어가 읽지 못하는 파일이 이미 있습니다 — 덮어쓰지 않습니다. 사람이 확인해 옮기거나 고친 뒤 다시 실행하십시오.` });
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (!dryRun) {
|
|
133
|
+
try {
|
|
134
|
+
for (const s of d.specs)
|
|
135
|
+
await context.writeSpec(s);
|
|
136
|
+
}
|
|
137
|
+
catch (e) {
|
|
138
|
+
if (e instanceof spec_store_1.TargetPathOccupiedError) {
|
|
139
|
+
refused.push({ clusterKey: d.clusterKey, reason: `초안 목적지(${e.occupiedPath})에 스토어가 읽지 못하는 파일이 이미 있습니다 — 덮어쓰지 않습니다. 사람이 확인해 옮기거나 고친 뒤 다시 실행하십시오.` });
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
throw e;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
written.push(d);
|
|
146
|
+
}
|
|
147
|
+
// @implements A-SPEC-299
|
|
148
|
+
// Deliberately NOT changed. This looked like the same false pass as spec_slice_approve, but an
|
|
149
|
+
// approved store-integrity test pins `ok: true` for a run that refused every cluster because a
|
|
150
|
+
// destination held an unreadable file: nothing was written, nothing was damaged, and `ok` here
|
|
151
|
+
// means the call completed without harm. That is a defensible contract, and the case against
|
|
152
|
+
// it was analogy rather than an observed failure. Evidence beats symmetry.
|
|
153
|
+
return { ok: true, dryRun, parentReqId: req.id, clusters: report.clusters.length, drafted: written, skipped, refused };
|
|
154
|
+
},
|
|
155
|
+
/**
|
|
156
|
+
* Insert `@implements` anchors into source files. Dry-run by DEFAULT, and anchoring to a
|
|
157
|
+
* non-approved A-SPEC is refused — a reverse-engineered draft describes code; description does
|
|
158
|
+
* not confer approval.
|
|
159
|
+
*/
|
|
160
|
+
async reverse_anchor(a) {
|
|
161
|
+
const specs = await context.listSpecs();
|
|
162
|
+
const approved = specs.filter((s) => s.type === 'A-SPEC' && s.status === 'approved').map((s) => s.id);
|
|
163
|
+
const plan = (0, anchor_1.planAnchors)(a.root, a.mapping ?? [], approved);
|
|
164
|
+
// @implements A-SPEC-182
|
|
165
|
+
// The third place the harness refuses over an unapproved A-SPEC. Enriched HERE rather than
|
|
166
|
+
// inside planAnchors, which is a pure planner holding only the approved-id list — this is the
|
|
167
|
+
// boundary that has the spec objects, so the planner stays testable without them.
|
|
168
|
+
const byId = new Map(specs.map((s) => [s.id, s]));
|
|
169
|
+
// ONE payload per distinct A-SPEC, on a sibling field — not appended to every refused entry.
|
|
170
|
+
// Review measured the first attempt, which memoized only the COMPUTATION: 149 files mapped to
|
|
171
|
+
// one draft A-SPEC still concatenated the same 361-character sentence 149 times, 48 KB of pure
|
|
172
|
+
// duplication in a single tool result. The comment claimed a property the code did not have,
|
|
173
|
+
// which is how it survived a round of review — hence the size assertion in the test.
|
|
174
|
+
const blockers = {};
|
|
175
|
+
for (const r of plan.refused) {
|
|
176
|
+
if (!/is not approved$/.test(r.reason) || blockers[r.aspec] !== undefined)
|
|
177
|
+
continue;
|
|
178
|
+
const why = (0, approval_blockers_1.blockerSummary)(byId.get(r.aspec), (id) => byId.get(id) ?? null, r.aspec);
|
|
179
|
+
if (why)
|
|
180
|
+
blockers[r.aspec] = why.trim();
|
|
181
|
+
}
|
|
182
|
+
const applied = (0, anchor_1.applyAnchors)(a.root, plan.edits, { dryRun: a.dryRun !== false });
|
|
183
|
+
return { ...plan, ...applied, blockers };
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { type DerivedRebuild } from '../../spec/entity-integration-discharge';
|
|
2
|
+
import { EntityIntegrationPlan } from '../../spec/entity-integration-transaction';
|
|
3
|
+
export type IntegrationSealTransition = {
|
|
4
|
+
kind: 'spec-unsealed';
|
|
5
|
+
id: string;
|
|
6
|
+
} | {
|
|
7
|
+
kind: 'spec-approved';
|
|
8
|
+
id: string;
|
|
9
|
+
digest: string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* @implements A-SPEC-635 — the seal transitions an integration performs, derived from its plan: every
|
|
13
|
+
* `unseal` step withdraws a seal, every `approve` step issues one whose digest is read from the
|
|
14
|
+
* published document bytes and must be that document's own `specDigest` (otherwise the seal is not
|
|
15
|
+
* what the transition policy computed → `audit-conflict`). Through the public tool a forged step is
|
|
16
|
+
* already refused `plan-changed` by step revalidation; this check is the audit's own invariant.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* @implements A-SPEC-645
|
|
20
|
+
* The `entity:`/`old-id:`/`new-id:` inputs for every mapping step whose record changes its display id,
|
|
21
|
+
* so an integration's renumbers are readable from the ledger like entity_renumber's. Without them the
|
|
22
|
+
* evidence consumers had no way to follow a document an integration renamed.
|
|
23
|
+
*/
|
|
24
|
+
export declare function renumberedMappingInputs(steps: ReadonlyArray<{
|
|
25
|
+
phase: string;
|
|
26
|
+
entityId?: string;
|
|
27
|
+
change: {
|
|
28
|
+
locator: string;
|
|
29
|
+
before: string | null;
|
|
30
|
+
after: string | null;
|
|
31
|
+
};
|
|
32
|
+
}>, renames?: ReadonlyArray<{
|
|
33
|
+
entityId: string;
|
|
34
|
+
oldDisplayId: string;
|
|
35
|
+
newDisplayId: string;
|
|
36
|
+
}>): string[];
|
|
37
|
+
export declare function integrationSealTransitions(operation: EntityIntegrationPlan): IntegrationSealTransition[];
|
|
38
|
+
export declare function createEntityIntegrationHandlers(context: {
|
|
39
|
+
storeRoot?: string;
|
|
40
|
+
rebuildFor?: (root: string) => DerivedRebuild;
|
|
41
|
+
}): {
|
|
42
|
+
entity_integrate(a: {
|
|
43
|
+
root?: string;
|
|
44
|
+
operation?: string;
|
|
45
|
+
base?: string;
|
|
46
|
+
left?: string;
|
|
47
|
+
right?: string;
|
|
48
|
+
operationId?: string;
|
|
49
|
+
plan?: unknown;
|
|
50
|
+
}): Promise<{
|
|
51
|
+
ok: true;
|
|
52
|
+
state: "completed";
|
|
53
|
+
operationId: string;
|
|
54
|
+
} | {
|
|
55
|
+
ok: true;
|
|
56
|
+
state: "abandoned";
|
|
57
|
+
operationId: string;
|
|
58
|
+
restored: string[];
|
|
59
|
+
untouched: string[];
|
|
60
|
+
preserved: string[];
|
|
61
|
+
} | {
|
|
62
|
+
ok: true;
|
|
63
|
+
record: import("../../spec/entity-integration-discharge").DerivedDischargeRecord;
|
|
64
|
+
alreadyDischarged: boolean;
|
|
65
|
+
} | {
|
|
66
|
+
ok: boolean;
|
|
67
|
+
obligations: import("../../spec/entity-integration-discharge").DerivedObligationStatus[];
|
|
68
|
+
plan?: undefined;
|
|
69
|
+
target?: undefined;
|
|
70
|
+
executable?: undefined;
|
|
71
|
+
code?: undefined;
|
|
72
|
+
reason?: undefined;
|
|
73
|
+
} | {
|
|
74
|
+
ok: boolean;
|
|
75
|
+
plan: import("../../spec/entity-integration-transaction").EntityIntegrationRecoveryInspection;
|
|
76
|
+
target: string;
|
|
77
|
+
executable: boolean;
|
|
78
|
+
obligations?: undefined;
|
|
79
|
+
code?: undefined;
|
|
80
|
+
reason?: undefined;
|
|
81
|
+
} | {
|
|
82
|
+
blockers: string[];
|
|
83
|
+
target?: string | undefined;
|
|
84
|
+
plan?: EntityIntegrationPlan | undefined;
|
|
85
|
+
preview: import("../../spec/entity-integration-transaction").EntityIntegrationCandidate;
|
|
86
|
+
ok: boolean;
|
|
87
|
+
executable: boolean;
|
|
88
|
+
obligations?: undefined;
|
|
89
|
+
code?: undefined;
|
|
90
|
+
reason?: undefined;
|
|
91
|
+
} | {
|
|
92
|
+
ok: boolean;
|
|
93
|
+
code: string;
|
|
94
|
+
reason: string;
|
|
95
|
+
obligations?: undefined;
|
|
96
|
+
plan?: undefined;
|
|
97
|
+
target?: undefined;
|
|
98
|
+
executable?: undefined;
|
|
99
|
+
}>;
|
|
100
|
+
};
|
|
@@ -0,0 +1,324 @@
|
|
|
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
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.renumberedMappingInputs = renumberedMappingInputs;
|
|
37
|
+
exports.integrationSealTransitions = integrationSealTransitions;
|
|
38
|
+
exports.createEntityIntegrationHandlers = createEntityIntegrationHandlers;
|
|
39
|
+
// @implements A-SPEC-629
|
|
40
|
+
// @implements A-SPEC-645
|
|
41
|
+
// @implements A-SPEC-642
|
|
42
|
+
// @implements A-SPEC-639
|
|
43
|
+
// @implements A-SPEC-632
|
|
44
|
+
// @implements A-SPEC-635
|
|
45
|
+
const fs = __importStar(require("node:fs"));
|
|
46
|
+
const path = __importStar(require("node:path"));
|
|
47
|
+
const entity_store_1 = require("../../spec/entity-store");
|
|
48
|
+
const root_1 = require("../../project/root");
|
|
49
|
+
const entity_transaction_1 = require("../../spec/entity-transaction");
|
|
50
|
+
const entity_integration_discharge_1 = require("../../spec/entity-integration-discharge");
|
|
51
|
+
const entity_integration_transaction_1 = require("../../spec/entity-integration-transaction");
|
|
52
|
+
const approval_grants_1 = require("../../governance/approval-grants");
|
|
53
|
+
const approval_queue_1 = require("../../governance/approval-queue");
|
|
54
|
+
const ledger_store_1 = require("../../governance/ledger-store");
|
|
55
|
+
const provenance_chain_1 = require("../../governance/provenance-chain");
|
|
56
|
+
const spec_parser_1 = require("../../spec/spec-parser");
|
|
57
|
+
const spec_digest_1 = require("../../spec/spec-digest");
|
|
58
|
+
/**
|
|
59
|
+
* @implements A-SPEC-635 — the seal transitions an integration performs, derived from its plan: every
|
|
60
|
+
* `unseal` step withdraws a seal, every `approve` step issues one whose digest is read from the
|
|
61
|
+
* published document bytes and must be that document's own `specDigest` (otherwise the seal is not
|
|
62
|
+
* what the transition policy computed → `audit-conflict`). Through the public tool a forged step is
|
|
63
|
+
* already refused `plan-changed` by step revalidation; this check is the audit's own invariant.
|
|
64
|
+
*/
|
|
65
|
+
/**
|
|
66
|
+
* @implements A-SPEC-645
|
|
67
|
+
* The `entity:`/`old-id:`/`new-id:` inputs for every mapping step whose record changes its display id,
|
|
68
|
+
* so an integration's renumbers are readable from the ledger like entity_renumber's. Without them the
|
|
69
|
+
* evidence consumers had no way to follow a document an integration renamed.
|
|
70
|
+
*/
|
|
71
|
+
function renumberedMappingInputs(steps, renames = []) {
|
|
72
|
+
const out = [];
|
|
73
|
+
for (const r of renames)
|
|
74
|
+
for (const input of ['entity:' + r.entityId, 'old-id:' + r.oldDisplayId, 'new-id:' + r.newDisplayId])
|
|
75
|
+
if (!out.includes(input))
|
|
76
|
+
out.push(input);
|
|
77
|
+
const decode = (bytes) => {
|
|
78
|
+
if (bytes === null)
|
|
79
|
+
return null;
|
|
80
|
+
try {
|
|
81
|
+
const value = JSON.parse(Buffer.from(bytes, 'base64').toString('utf8'));
|
|
82
|
+
return value && typeof value === 'object' ? value : null;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
for (const step of steps) {
|
|
89
|
+
if (step.phase !== 'mapping' || !step.change.locator.startsWith('.ax/entities/records/'))
|
|
90
|
+
continue;
|
|
91
|
+
const before = decode(step.change.before), after = decode(step.change.after);
|
|
92
|
+
if (!before || !after || typeof before.displayId !== 'string' || typeof after.displayId !== 'string' || before.displayId === after.displayId)
|
|
93
|
+
continue;
|
|
94
|
+
const entity = step.entityId ?? (typeof after.entityId === 'string' ? after.entityId : undefined);
|
|
95
|
+
for (const input of [...(entity ? ['entity:' + entity] : []), 'old-id:' + before.displayId, 'new-id:' + after.displayId])
|
|
96
|
+
if (!out.includes(input))
|
|
97
|
+
out.push(input);
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
function integrationSealTransitions(operation) {
|
|
102
|
+
const transitions = [];
|
|
103
|
+
for (const step of operation.steps) {
|
|
104
|
+
if (step.phase === 'unseal' && step.specId)
|
|
105
|
+
transitions.push({ kind: 'spec-unsealed', id: step.specId });
|
|
106
|
+
else if (step.phase === 'approve' && step.specId) {
|
|
107
|
+
if (typeof step.change.after !== 'string')
|
|
108
|
+
throw new entity_store_1.EntityStoreError('audit-conflict', 'Approve step ' + step.specId + ' publishes no document.');
|
|
109
|
+
let digest, computed;
|
|
110
|
+
try {
|
|
111
|
+
const parsed = (0, spec_parser_1.parseSpec)(Buffer.from(step.change.after, 'base64').toString('utf8'));
|
|
112
|
+
digest = parsed.frontmatter.approved_digest;
|
|
113
|
+
computed = (0, spec_digest_1.specDigest)(parsed);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
throw new entity_store_1.EntityStoreError('audit-conflict', 'Approve step ' + step.specId + ' publishes an unreadable document.');
|
|
117
|
+
}
|
|
118
|
+
if (typeof digest !== 'string' || digest !== computed)
|
|
119
|
+
throw new entity_store_1.EntityStoreError('audit-conflict', 'Approve step ' + step.specId + ' is not sealed by its own digest.');
|
|
120
|
+
transitions.push({ kind: 'spec-approved', id: step.specId, digest });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return transitions;
|
|
124
|
+
}
|
|
125
|
+
function createEntityIntegrationHandlers(context) {
|
|
126
|
+
return {
|
|
127
|
+
async entity_integrate(a) {
|
|
128
|
+
try {
|
|
129
|
+
if (!context.storeRoot)
|
|
130
|
+
throw new entity_store_1.EntityStoreError('unsupported-store', 'Git integration requires a configured filesystem spec store.');
|
|
131
|
+
const bound = (0, entity_store_1.entityStoreBinding)(context.storeRoot);
|
|
132
|
+
if (a.root !== undefined && fs.realpathSync((0, root_1.resolveProjectRoot)(a.root, { includeGit: false }).root) !== bound.root)
|
|
133
|
+
throw new entity_store_1.EntityStoreError('foreign-workspace', 'The requested workspace differs from the configured store.');
|
|
134
|
+
const recoveryTarget = (plan) => 'entity-store:integrate-recover:' + (0, entity_transaction_1.entityContentVersion)(JSON.stringify(plan));
|
|
135
|
+
const requireStore = (plan) => {
|
|
136
|
+
if (plan.storeLocator !== bound.locator)
|
|
137
|
+
throw new entity_store_1.EntityStoreError('foreign-store', 'Integration plan differs from the configured store.');
|
|
138
|
+
};
|
|
139
|
+
// @implements A-SPEC-642 — an authority-spending operation is refused, naming the exit, while the
|
|
140
|
+
// merged ledger holds an unreconciled double-spend; the audit adapter's `audit-conflict` stays behind.
|
|
141
|
+
if (a.operation === 'apply' || a.operation === 'recover' || a.operation === 'abandon' || a.operation === 'discharge') {
|
|
142
|
+
const conflict = (0, ledger_store_1.ledgerConflictReason)(path.join(bound.root, '.ax', 'ledger'));
|
|
143
|
+
if (conflict)
|
|
144
|
+
throw new entity_store_1.EntityStoreError('ledger-conflict', conflict);
|
|
145
|
+
}
|
|
146
|
+
// @implements A-SPEC-632 — replica-local discharge of a completed operation's derived obligations.
|
|
147
|
+
if (a.operation === 'obligations')
|
|
148
|
+
return { ok: true, obligations: (0, entity_integration_discharge_1.inspectDerivedObligations)(bound.root) };
|
|
149
|
+
if (a.operation === 'discharge') {
|
|
150
|
+
if (typeof a.operationId !== 'string')
|
|
151
|
+
throw new entity_store_1.EntityStoreError('invalid-request', 'Provide the completed integration operationId.');
|
|
152
|
+
if (!context.rebuildFor)
|
|
153
|
+
throw new entity_store_1.EntityStoreError('unsupported-store', 'Discharge requires graph and semantic rebuild wiring.');
|
|
154
|
+
return await (0, entity_integration_discharge_1.dischargeDerivedObligations)(bound.root, a.operationId, context.rebuildFor(bound.root));
|
|
155
|
+
}
|
|
156
|
+
if (a.operation === 'recovery-plan') {
|
|
157
|
+
if (typeof a.operationId !== 'string')
|
|
158
|
+
throw new entity_store_1.EntityStoreError('invalid-request', 'Provide the integration operationId.');
|
|
159
|
+
const plan = (0, entity_integration_transaction_1.inspectEntityIntegrationRecovery)(bound.root, a.operationId);
|
|
160
|
+
requireStore(plan.plan);
|
|
161
|
+
return { ok: true, plan, target: recoveryTarget(plan), executable: plan.conflicts.length === 0 };
|
|
162
|
+
}
|
|
163
|
+
if (a.operation === 'abandon') {
|
|
164
|
+
// @implements A-SPEC-639 — an authorized exit for a pending operation a third state blocked.
|
|
165
|
+
const id = a.plan?.operationId;
|
|
166
|
+
if (typeof id !== 'string')
|
|
167
|
+
throw new entity_store_1.EntityStoreError('invalid-request', 'Provide the complete inspected recovery plan to abandon.');
|
|
168
|
+
const recovery = (0, entity_integration_transaction_1.validateEntityIntegrationRecoveryRequest)(bound.root, a.plan);
|
|
169
|
+
requireStore(recovery.plan);
|
|
170
|
+
const target = 'entity-store:integrate-abandon:' + (0, entity_transaction_1.entityContentVersion)(JSON.stringify(a.plan));
|
|
171
|
+
let env;
|
|
172
|
+
try {
|
|
173
|
+
env = process.env.HOLMES_APPROVAL ? JSON.parse(process.env.HOLMES_APPROVAL) : undefined;
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
env = undefined;
|
|
177
|
+
}
|
|
178
|
+
const authorities = new Set();
|
|
179
|
+
const requireAuthority = () => {
|
|
180
|
+
const action = { kind: 'config-write', target };
|
|
181
|
+
const resolved = (0, approval_grants_1.resolveApproval)(bound.root, env, action, new Date().toISOString());
|
|
182
|
+
if (!resolved)
|
|
183
|
+
throw new entity_store_1.EntityStoreError('approval-required', 'Current authority is required to abandon this integration.' + (0, approval_queue_1.queueHint)(bound.root, { ...action, why: 'Abandon the blocked integration: revert its own publications, preserve foreign bytes, release ownership.' }));
|
|
184
|
+
authorities.add((0, provenance_chain_1.authorizationRef)(resolved.approval.actor, resolved.approval.token));
|
|
185
|
+
return resolved;
|
|
186
|
+
};
|
|
187
|
+
requireAuthority();
|
|
188
|
+
const audit = (operation, outcome) => {
|
|
189
|
+
const authority = requireAuthority(), ledger = new ledger_store_1.FileLedgerStore(path.join(bound.root, '.ax/ledger'));
|
|
190
|
+
if (!ledger.verifyAll().ok)
|
|
191
|
+
throw new entity_store_1.EntityStoreError('audit-conflict', 'Abandonment audit cannot extend an invalid ledger.');
|
|
192
|
+
const ref = 'entity-operation:' + operation.operationId;
|
|
193
|
+
if (ledger.loadAll().some(event => event.kind === 'entity-integration-abandoned' && event.inputs?.includes(ref)))
|
|
194
|
+
return;
|
|
195
|
+
ledger.append({ ts: new Date().toISOString(), actor: authority.approval.actor, kind: 'entity-integration-abandoned', summary: 'Abandoned Git entity integration ' + operation.operationId,
|
|
196
|
+
inputs: [ref, (0, entity_integration_transaction_1.entityIntegrationTarget)(operation), 'workspace:' + operation.workspaceId, 'store:' + operation.storeId, recovery.journalVersion, target,
|
|
197
|
+
...outcome.restored.map(locator => 'restored:' + locator), ...outcome.preserved.map(locator => 'preserved:' + locator), ...authorities],
|
|
198
|
+
rationale: authority.approval.rationale, authorization: (0, provenance_chain_1.authorizationRef)(authority.approval.actor, authority.approval.token) });
|
|
199
|
+
};
|
|
200
|
+
return await (0, entity_integration_transaction_1.abandonEntityIntegration)(bound.root, recovery, requireAuthority, audit);
|
|
201
|
+
}
|
|
202
|
+
if (a.operation === 'apply' || a.operation === 'recover') {
|
|
203
|
+
let recovery;
|
|
204
|
+
if (a.operation === 'recover') {
|
|
205
|
+
const id = a.plan?.operationId;
|
|
206
|
+
if (typeof id !== 'string')
|
|
207
|
+
throw new entity_store_1.EntityStoreError('invalid-request', 'Provide the complete inspected recovery plan.');
|
|
208
|
+
recovery = (0, entity_integration_transaction_1.validateEntityIntegrationRecoveryRequest)(bound.root, a.plan);
|
|
209
|
+
}
|
|
210
|
+
const plan = recovery ? recovery.plan : (0, entity_integration_transaction_1.validateEntityIntegrationPlan)(a.plan);
|
|
211
|
+
requireStore(plan);
|
|
212
|
+
const target = recovery ? recoveryTarget(recovery) : (0, entity_integration_transaction_1.entityIntegrationTarget)(plan);
|
|
213
|
+
let env;
|
|
214
|
+
try {
|
|
215
|
+
env = process.env.HOLMES_APPROVAL ? JSON.parse(process.env.HOLMES_APPROVAL) : undefined;
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
env = undefined;
|
|
219
|
+
}
|
|
220
|
+
const grants = new Set(), authorities = new Set(), usedGrantRefs = new Set();
|
|
221
|
+
const grantRef = (approval) => 'grant-use:' + (0, entity_transaction_1.entityContentVersion)(JSON.stringify({ actor: approval.actor, token: approval.token, nonce: approval.nonce, scope: approval.scope, expires: approval.expires }));
|
|
222
|
+
const requireAuthority = (specId) => {
|
|
223
|
+
const action = { kind: specId ? 'spec-approve' : 'config-write', target: specId ?? target };
|
|
224
|
+
const resolved = (0, approval_grants_1.resolveApproval)(bound.root, env, action, new Date().toISOString());
|
|
225
|
+
if (!resolved)
|
|
226
|
+
throw new entity_store_1.EntityStoreError(specId ? 'spec-approval-required' : 'approval-required', 'Current authority is required for this integration.' + (0, approval_queue_1.queueHint)(bound.root, { ...action, why: 'Integrate the exact Git inputs while preserving entity identity and official seal transitions.' }));
|
|
227
|
+
if (resolved.source === 'grant' && resolved.approval.nonce)
|
|
228
|
+
grants.add(resolved.approval.nonce);
|
|
229
|
+
if (resolved.source === 'grant')
|
|
230
|
+
usedGrantRefs.add(grantRef(resolved.approval));
|
|
231
|
+
authorities.add((0, provenance_chain_1.authorizationRef)(resolved.approval.actor, resolved.approval.token));
|
|
232
|
+
return resolved;
|
|
233
|
+
};
|
|
234
|
+
const bindingFor = (operation) => ['entity-operation:' + operation.operationId, (0, entity_integration_transaction_1.entityIntegrationTarget)(operation), 'workspace:' + operation.workspaceId, 'store:' + operation.storeId,
|
|
235
|
+
...['base', 'left', 'right'].map(side => 'git-' + side + ':' + operation.commits[side])];
|
|
236
|
+
const priorGrantUses = (ledger, operation) => {
|
|
237
|
+
if (!ledger.verifyAll().ok)
|
|
238
|
+
throw new entity_store_1.EntityStoreError('audit-conflict', 'Integration cannot rely on an invalid ledger.');
|
|
239
|
+
const binding = bindingFor(operation), refs = new Set();
|
|
240
|
+
for (const event of ledger.loadAll().filter(event => event.kind === 'entity-integration-grant-used' && event.inputs?.includes(binding[0]))) {
|
|
241
|
+
const usages = (event.inputs ?? []).filter(ref => ref.startsWith('grant-use:'));
|
|
242
|
+
if (binding.some(ref => !event.inputs?.includes(ref)) || usages.length === 0 || usages.some(ref => !/^grant-use:sha256:[0-9a-f]{64}$/.test(ref) || refs.has(ref)))
|
|
243
|
+
throw new entity_store_1.EntityStoreError('audit-conflict', 'Integration grant usage has conflicting bindings.');
|
|
244
|
+
for (const ref of usages)
|
|
245
|
+
refs.add(ref);
|
|
246
|
+
}
|
|
247
|
+
return refs;
|
|
248
|
+
};
|
|
249
|
+
const recordGrantUse = (operation) => {
|
|
250
|
+
const authority = requireAuthority(), ledger = new ledger_store_1.FileLedgerStore(path.join(bound.root, '.ax/ledger'));
|
|
251
|
+
const prior = priorGrantUses(ledger, operation), additional = [...usedGrantRefs].filter(ref => !prior.has(ref));
|
|
252
|
+
for (const ref of prior)
|
|
253
|
+
usedGrantRefs.add(ref);
|
|
254
|
+
if (!additional.length)
|
|
255
|
+
return;
|
|
256
|
+
ledger.append({ ts: new Date().toISOString(), actor: authority.approval.actor, kind: 'entity-integration-grant-used', summary: 'Recorded grant usage for integration ' + operation.operationId,
|
|
257
|
+
inputs: [...bindingFor(operation), target, ...additional, ...authorities], rationale: authority.approval.rationale, authorization: (0, provenance_chain_1.authorizationRef)(authority.approval.actor, authority.approval.token) });
|
|
258
|
+
};
|
|
259
|
+
const audit = (operation, verifyOnly = false, renumbered = []) => {
|
|
260
|
+
const authority = requireAuthority(), ledger = new ledger_store_1.FileLedgerStore(path.join(bound.root, '.ax/ledger'));
|
|
261
|
+
const binding = bindingFor(operation);
|
|
262
|
+
for (const ref of priorGrantUses(ledger, operation))
|
|
263
|
+
usedGrantRefs.add(ref);
|
|
264
|
+
const events = ledger.loadAll(), existing = events.filter(event => event.kind === 'entity-integrated' && event.inputs?.includes(binding[0]));
|
|
265
|
+
if (existing.length) {
|
|
266
|
+
if (existing.length !== 1 || binding.some(ref => !existing[0].inputs?.includes(ref)))
|
|
267
|
+
throw new entity_store_1.EntityStoreError('audit-conflict', 'Integration completion audit has conflicting bindings.');
|
|
268
|
+
for (const ref of existing[0].inputs ?? [])
|
|
269
|
+
if (ref.startsWith('grant-use:'))
|
|
270
|
+
usedGrantRefs.add(ref);
|
|
271
|
+
// Retain the original completion event, but persist any new authority use
|
|
272
|
+
// before recovery/verification consumes that grant file.
|
|
273
|
+
recordGrantUse(operation);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (verifyOnly)
|
|
277
|
+
throw new entity_store_1.EntityStoreError('audit-conflict', 'Completion receipt has no matching integration audit.');
|
|
278
|
+
recordGrantUse(operation);
|
|
279
|
+
// @implements A-SPEC-635 — the seal history the official acts would have written, attributed
|
|
280
|
+
// to the per-spec authority and bound to this operation; skipped when already present so
|
|
281
|
+
// recovery after an interrupted audit appends each exactly once.
|
|
282
|
+
for (const transition of integrationSealTransitions(operation)) {
|
|
283
|
+
if (events.some(event => event.kind === transition.kind && event.inputs?.includes(transition.id) && event.inputs?.includes(binding[0])))
|
|
284
|
+
continue;
|
|
285
|
+
const sealAuthority = requireAuthority(transition.id);
|
|
286
|
+
ledger.append({ ts: new Date().toISOString(), actor: sealAuthority.approval.actor, kind: transition.kind,
|
|
287
|
+
summary: transition.kind === 'spec-unsealed' ? 'unsealed ' + transition.id + ' (integration ' + operation.operationId + ')' : 'approved ' + transition.id + ' sealing ' + transition.digest,
|
|
288
|
+
inputs: transition.kind === 'spec-unsealed' ? [transition.id, binding[0]] : [transition.id, transition.digest, binding[0]],
|
|
289
|
+
rationale: sealAuthority.approval.rationale, authorization: (0, provenance_chain_1.authorizationRef)(sealAuthority.approval.actor, sealAuthority.approval.token) });
|
|
290
|
+
}
|
|
291
|
+
ledger.append({ ts: new Date().toISOString(), actor: authority.approval.actor, kind: 'entity-integrated', summary: 'Completed Git entity integration ' + operation.operationId,
|
|
292
|
+
inputs: [...binding, target, ...new Set(operation.steps.flatMap(step => [
|
|
293
|
+
...(step.entityId ? ['entity:' + step.entityId] : []), (step.change.after === null ? 'deleted-path:' : 'changed-path:') + step.change.locator
|
|
294
|
+
])),
|
|
295
|
+
...renumberedMappingInputs(operation.steps, renumbered).filter(input => !input.startsWith('entity:')), // A-SPEC-645
|
|
296
|
+
'derived-obligations:' + operation.operationId, ...authorities, ...usedGrantRefs],
|
|
297
|
+
rationale: authority.approval.rationale, authorization: (0, provenance_chain_1.authorizationRef)(authority.approval.actor, authority.approval.token) });
|
|
298
|
+
};
|
|
299
|
+
requireAuthority();
|
|
300
|
+
const result = recovery ? await (0, entity_integration_transaction_1.recoverEntityIntegration)(bound.root, recovery, requireAuthority, audit, recordGrantUse)
|
|
301
|
+
: await (0, entity_integration_transaction_1.applyEntityIntegration)(bound.root, plan, requireAuthority, audit, recordGrantUse);
|
|
302
|
+
for (const grant of (0, approval_grants_1.readGrants)(bound.root).grants)
|
|
303
|
+
if (grant.nonce && usedGrantRefs.has(grantRef(grant)))
|
|
304
|
+
grants.add(grant.nonce);
|
|
305
|
+
for (const nonce of grants)
|
|
306
|
+
(0, approval_grants_1.consumeGrantFile)(bound.root, nonce);
|
|
307
|
+
return result;
|
|
308
|
+
}
|
|
309
|
+
if (a.operation !== undefined && a.operation !== 'plan')
|
|
310
|
+
throw new entity_store_1.EntityStoreError('unsupported-operation', 'Use plan, apply, recovery-plan, recover, abandon, discharge or obligations.');
|
|
311
|
+
if (typeof a.base !== 'string' || typeof a.left !== 'string' || typeof a.right !== 'string')
|
|
312
|
+
throw new entity_store_1.EntityStoreError('invalid-request', 'Provide base, left and right Git revisions.');
|
|
313
|
+
const reviewed = await (0, entity_integration_transaction_1.reviewEntityIntegration)(bound.root, { base: a.base, left: a.left, right: a.right }, bound.locator);
|
|
314
|
+
return { ok: true, executable: !!reviewed.plan, ...reviewed, ...(reviewed.plan ? { target: (0, entity_integration_transaction_1.entityIntegrationTarget)(reviewed.plan) } : {}),
|
|
315
|
+
blockers: [...reviewed.preview.conflicts, ...reviewed.preview.pending.map(item => item.file + ': ' + item.reason)] };
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
if (error instanceof entity_store_1.EntityStoreError)
|
|
319
|
+
return { ok: false, code: error.code, reason: error.message };
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
}
|