@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,711 @@
|
|
|
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.entitySourcePublicationOwner = void 0;
|
|
37
|
+
exports.withEntitySourceInspection = withEntitySourceInspection;
|
|
38
|
+
exports.withRecoveredSourcePublication = withRecoveredSourcePublication;
|
|
39
|
+
exports.entityRenumberTarget = entityRenumberTarget;
|
|
40
|
+
exports.prepareEntityRenumberPlan = prepareEntityRenumberPlan;
|
|
41
|
+
exports.validateEntityRenumberPlan = validateEntityRenumberPlan;
|
|
42
|
+
exports.entityRenumberRecoveryTarget = entityRenumberRecoveryTarget;
|
|
43
|
+
exports.validateEntityRenumberRecoveryPlan = validateEntityRenumberRecoveryPlan;
|
|
44
|
+
exports.planEntityRenumberRecovery = planEntityRenumberRecovery;
|
|
45
|
+
exports.abandonEntityRenumber = abandonEntityRenumber;
|
|
46
|
+
exports.recoverEntityRenumber = recoverEntityRenumber;
|
|
47
|
+
exports.applyEntityRenumber = applyEntityRenumber;
|
|
48
|
+
// @implements A-SPEC-629, A-SPEC-628
|
|
49
|
+
// @implements A-SPEC-641
|
|
50
|
+
// @implements A-SPEC-640
|
|
51
|
+
// @implements A-SPEC-639
|
|
52
|
+
// @implements A-SPEC-634
|
|
53
|
+
const path = __importStar(require("node:path"));
|
|
54
|
+
const fs = __importStar(require("node:fs"));
|
|
55
|
+
const node_crypto_1 = require("node:crypto");
|
|
56
|
+
const node_util_1 = require("node:util");
|
|
57
|
+
const entity_store_1 = require("./entity-store");
|
|
58
|
+
const entity_renumber_1 = require("./entity-renumber");
|
|
59
|
+
const renumber_1 = require("./renumber");
|
|
60
|
+
const spec_parser_1 = require("./spec-parser");
|
|
61
|
+
const transition_policy_1 = require("./transition-policy");
|
|
62
|
+
const entity_transaction_1 = require("./entity-transaction");
|
|
63
|
+
const workspace_identity_1 = require("../project/workspace-identity");
|
|
64
|
+
// A reserved coordination namespace, independent of every configured spec store (A-SPEC-634: the value lives in entity-transaction).
|
|
65
|
+
const sourcePublication = entity_transaction_1.SOURCE_PUBLICATION_LOCATOR;
|
|
66
|
+
const entitySourcePublicationOwner = (root) => (0, entity_transaction_1.entityLockOwner)(root, sourcePublication);
|
|
67
|
+
exports.entitySourcePublicationOwner = entitySourcePublicationOwner;
|
|
68
|
+
/** Caller holds the destination store first; inspection shares the existing publication namespace. */
|
|
69
|
+
async function withEntitySourceInspection(root, act) {
|
|
70
|
+
return (0, entity_transaction_1.withEntityStoreLockAsync)(root, sourcePublication, async () => {
|
|
71
|
+
assertSourceReservation(root, '');
|
|
72
|
+
const owner = (0, entity_transaction_1.entityLockOwner)(root, sourcePublication);
|
|
73
|
+
const result = await act();
|
|
74
|
+
if (!owner || !(0, node_util_1.isDeepStrictEqual)((0, entity_transaction_1.entityLockOwner)(root, sourcePublication), owner))
|
|
75
|
+
throw new entity_transaction_1.EntityStoreError('unknown-owner', 'Source inspection ownership changed.');
|
|
76
|
+
assertSourceReservation(root, '');
|
|
77
|
+
return result;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const isRenumberJournal = (schema) => schema === 'holmes-entity-renumber-journal/1' || schema === 'holmes-entity-renumber-journal/2';
|
|
81
|
+
/** Store ownership is acquired first. Publication callbacks are synchronous. */
|
|
82
|
+
function sourceOwnershipReceipt(operationId, journalVersion, owner) {
|
|
83
|
+
return JSON.stringify({ schema: 'holmes-renumber-source-owner/1', operationId, journalVersion, owner });
|
|
84
|
+
}
|
|
85
|
+
function sourceOwnershipPath(journalFile, owner) {
|
|
86
|
+
return path.posix.dirname(journalFile) + '/source-owners/' + owner.token + '.json';
|
|
87
|
+
}
|
|
88
|
+
function assertSourceOwnershipReceipt(root, receipt) {
|
|
89
|
+
if (receipt && !safeRead(root, receipt.file)?.equals(receipt.bytes))
|
|
90
|
+
throw new entity_transaction_1.EntityStoreError('plan-changed', 'Resumed source ownership receipt changed before publication.');
|
|
91
|
+
}
|
|
92
|
+
function withRecoveredSourcePublication(root, operationId, expected, act, journal) {
|
|
93
|
+
const publish = () => { assertSourceReservation(root, operationId); return act(); };
|
|
94
|
+
const owner = (0, entity_transaction_1.entityLockOwner)(root, sourcePublication);
|
|
95
|
+
if (!owner) {
|
|
96
|
+
// A completed retry only verifies bytes and audit. It creates no new source obligation.
|
|
97
|
+
if (!journal)
|
|
98
|
+
return publish();
|
|
99
|
+
return (0, entity_transaction_1.withEntityStoreLock)(root, sourcePublication, () => {
|
|
100
|
+
assertSourceReservation(root, operationId);
|
|
101
|
+
if (version(safeRead(root, journal.file)) !== journal.version)
|
|
102
|
+
throw new entity_transaction_1.EntityStoreError('plan-changed', 'Source ownership cannot bind a changed journal.');
|
|
103
|
+
const acquired = (0, entity_transaction_1.entityLockOwner)(root, sourcePublication);
|
|
104
|
+
if (!acquired)
|
|
105
|
+
throw new entity_transaction_1.EntityStoreError('unknown-owner', 'Resumed publication requires source ownership.');
|
|
106
|
+
(0, entity_transaction_1.publishEntityRecord)(root, sourceOwnershipPath(journal.file, acquired), sourceOwnershipReceipt(operationId, journal.version, acquired));
|
|
107
|
+
return act();
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
if (!expected || !(0, node_util_1.isDeepStrictEqual)(owner, expected))
|
|
111
|
+
throw new entity_transaction_1.EntityStoreError('store-busy', 'Shared source publication belongs to another or unbound operation.');
|
|
112
|
+
return (0, entity_transaction_1.withEntityRecoveryLock)(root, sourcePublication, () => {
|
|
113
|
+
if (!(0, node_util_1.isDeepStrictEqual)((0, entity_transaction_1.entityLockOwner)(root, sourcePublication), expected))
|
|
114
|
+
throw new entity_transaction_1.EntityStoreError('plan-changed', 'Source publication ownership changed before recovery.');
|
|
115
|
+
return publish();
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/** Exceptions release physical locks, but the durable operation still reserves publication. */
|
|
119
|
+
function assertSourceReservation(root, operationId) {
|
|
120
|
+
const pending = (0, entity_transaction_1.entityDirectory)(root, '.ax/state/entity-transactions');
|
|
121
|
+
if (!fs.existsSync(pending))
|
|
122
|
+
return;
|
|
123
|
+
for (const operation of fs.readdirSync(pending).sort()) {
|
|
124
|
+
const bytes = safeRead(root, '.ax/state/entity-transactions/' + operation + '/journal.json');
|
|
125
|
+
if (!bytes)
|
|
126
|
+
throw new entity_transaction_1.EntityStoreError('store-busy', 'Pending publication changed or has no journal; inspect before retrying.');
|
|
127
|
+
let journal;
|
|
128
|
+
try {
|
|
129
|
+
journal = JSON.parse(bytes.toString('utf8'));
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
throw new entity_transaction_1.EntityStoreError('invalid-plan', 'An unreadable pending journal prevents source publication.');
|
|
133
|
+
}
|
|
134
|
+
if ((isRenumberJournal(journal?.schema) || journal?.schema === 'holmes-entity-integration-journal/1') && operation !== operationId)
|
|
135
|
+
throw new entity_transaction_1.EntityStoreError('store-busy', 'Another pending operation reserves shared source publication; recover that operation first.');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const encoded = (text) => Buffer.from(text).toString('base64');
|
|
139
|
+
const version = (bytes) => bytes ? (0, entity_transaction_1.entityContentVersion)(bytes) : null;
|
|
140
|
+
function safeRead(root, file) {
|
|
141
|
+
if (!file || /[\\:\x00]/.test(file) || file.split('/').some(p => !p || p === '.' || p === '..'))
|
|
142
|
+
throw new entity_transaction_1.EntityStoreError('unsafe-path', 'Renumber input is not a canonical workspace locator.');
|
|
143
|
+
return (0, entity_transaction_1.readEntityBytes)(path.join((0, entity_transaction_1.entityDirectory)(root, path.posix.dirname(file)), path.posix.basename(file)));
|
|
144
|
+
}
|
|
145
|
+
function entityRenumberTarget(plan) {
|
|
146
|
+
return 'entity-store:renumber:' + (0, entity_transaction_1.entityContentVersion)(JSON.stringify(plan));
|
|
147
|
+
}
|
|
148
|
+
/** Build candidates through the same transition policy as the ordinary official tools. No writes. */
|
|
149
|
+
function prepareEntityRenumberPlan(root, snapshot, changes, sources, operationId = (0, node_crypto_1.randomUUID)(), originalSources = new Map()) {
|
|
150
|
+
const proposal = (0, entity_renumber_1.planEntityRenumber)(snapshot.entries, changes);
|
|
151
|
+
if (!proposal.ok)
|
|
152
|
+
throw new entity_transaction_1.EntityStoreError('plan-conflict', proposal.conflicts.join('\n'));
|
|
153
|
+
if (sources.diagnostics.length)
|
|
154
|
+
throw new entity_transaction_1.EntityStoreError('source-coverage', 'Resolve source discovery diagnostics before application.');
|
|
155
|
+
for (const file of sources.files)
|
|
156
|
+
for (const id of file.anchorIds) {
|
|
157
|
+
if (snapshot.referenceNamespaces.some(r => r.displayId === id && r.storeId !== snapshot.storeId))
|
|
158
|
+
throw new entity_transaction_1.EntityStoreError('ambiguous-reference', 'An anchor names multiple activated stores: ' + id);
|
|
159
|
+
}
|
|
160
|
+
const texts = new Map(snapshot.entries.map(e => [e.entityId, e.text]));
|
|
161
|
+
const documents = new Map(snapshot.entries.map(e => [snapshot.storeLocator + '/' + e.documentLocator, encoded(e.text)]));
|
|
162
|
+
const steps = [], policyFiles = new Map(), approvalTargets = new Set();
|
|
163
|
+
const specs = () => [...texts.values()].map(spec_parser_1.parseSpec);
|
|
164
|
+
const add = (phase, locator, before, after, specId) => {
|
|
165
|
+
if (before !== after)
|
|
166
|
+
steps.push({ phase, ...(specId ? { specId } : {}), change: { locator, before, after } });
|
|
167
|
+
};
|
|
168
|
+
for (const id of proposal.unsealOrder) {
|
|
169
|
+
const entry = proposal.entries.find(e => e.oldId === id), before = texts.get(entry.entityId);
|
|
170
|
+
const prepared = (0, transition_policy_1.prepareLifecycleSpec)((0, spec_parser_1.parseSpec)(before), specs(), 'unseal');
|
|
171
|
+
if (!prepared.ok)
|
|
172
|
+
throw new entity_transaction_1.EntityStoreError('spec-policy-refused', prepared.reason);
|
|
173
|
+
const after = (0, entity_renumber_1.replaceFrontmatter)(before, prepared.candidate), locator = snapshot.storeLocator + '/' + entry.documentLocator;
|
|
174
|
+
add('unseal', locator, encoded(before), encoded(after), id);
|
|
175
|
+
texts.set(entry.entityId, after);
|
|
176
|
+
documents.set(locator, encoded(after));
|
|
177
|
+
approvalTargets.add(id);
|
|
178
|
+
}
|
|
179
|
+
const desired = new Map();
|
|
180
|
+
for (const entry of proposal.entries) {
|
|
181
|
+
const before = texts.get(entry.entityId), prior = (0, spec_parser_1.parseSpec)(before), transformed = (0, spec_parser_1.parseSpec)(entry.after);
|
|
182
|
+
const candidate = { ...prior, id: transformed.id, dependsOn: transformed.dependsOn };
|
|
183
|
+
const after = candidate.id !== prior.id || !(0, node_util_1.isDeepStrictEqual)(candidate.dependsOn, prior.dependsOn) ? (0, entity_renumber_1.replaceFrontmatter)(before, candidate) : before;
|
|
184
|
+
texts.set(entry.entityId, after);
|
|
185
|
+
desired.set(snapshot.storeLocator + '/' + entry.newDocumentLocator, encoded(after));
|
|
186
|
+
if (prior.status === 'approved' && entry.oldId !== entry.newId) {
|
|
187
|
+
approvalTargets.add(entry.oldId);
|
|
188
|
+
approvalTargets.add(entry.newId);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Derive all destinations from the original UUID map, including simultaneous swaps.
|
|
192
|
+
for (const locator of [...new Set([...documents.keys(), ...desired.keys()])].sort())
|
|
193
|
+
add('document', locator, documents.get(locator) ?? null, desired.get(locator) ?? null);
|
|
194
|
+
for (const entry of proposal.entries) {
|
|
195
|
+
const original = snapshot.entries.find(e => e.entityId === entry.entityId);
|
|
196
|
+
const mapping = JSON.parse(original.mapping);
|
|
197
|
+
if (entry.oldId !== entry.newId || entry.documentLocator !== entry.newDocumentLocator) {
|
|
198
|
+
add('mapping', '.ax/entities/records/' + entry.entityId + '.json', encoded(original.mapping), encoded(JSON.stringify({ ...mapping, displayId: entry.newId, documentLocator: entry.newDocumentLocator })));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
for (const source of sources.files)
|
|
202
|
+
add('source', source.file, encoded(source.before), encoded(source.after));
|
|
203
|
+
for (const id of proposal.approveOrder) {
|
|
204
|
+
const entry = proposal.entries.find(e => e.newId === id), before = texts.get(entry.entityId);
|
|
205
|
+
const prepared = (0, transition_policy_1.prepareApprovedSpec)((0, spec_parser_1.parseSpec)(before), specs(), all => parent => all.find(s => s.id === parent) ?? null, file => {
|
|
206
|
+
const bytes = originalSources.has(file) ? Buffer.from(originalSources.get(file)) : safeRead(root, file);
|
|
207
|
+
policyFiles.set(file, version(bytes));
|
|
208
|
+
return sources.files.find(source => source.file === file)?.after ?? bytes?.toString('utf8') ?? null;
|
|
209
|
+
});
|
|
210
|
+
// A-SPEC-641: a multi-document act names the document it refuses on.
|
|
211
|
+
if (!prepared.ok)
|
|
212
|
+
throw new entity_transaction_1.EntityStoreError('spec-policy-refused', id + ': ' + prepared.reason);
|
|
213
|
+
const after = (0, entity_renumber_1.replaceFrontmatter)(before, prepared.candidate);
|
|
214
|
+
add('approve', snapshot.storeLocator + '/' + entry.newDocumentLocator, encoded(before), encoded(after), id);
|
|
215
|
+
texts.set(entry.entityId, after);
|
|
216
|
+
approvalTargets.add(id);
|
|
217
|
+
}
|
|
218
|
+
return { schema: 'holmes-entity-renumber/1', operationId, snapshot, changes: changes.map(c => ({ ...c })), sources,
|
|
219
|
+
policyFiles: [...policyFiles].sort(([a], [b]) => a.localeCompare(b)).map(([file, version]) => ({ file, version })), approvalTargets: [...approvalTargets].sort(), steps };
|
|
220
|
+
}
|
|
221
|
+
function inputVersions(plan) {
|
|
222
|
+
const expected = new Map();
|
|
223
|
+
const bind = (file, digest) => {
|
|
224
|
+
if (expected.has(file) && expected.get(file) !== digest)
|
|
225
|
+
throw new entity_transaction_1.EntityStoreError('plan-conflict', 'Different inputs name the same path: ' + file);
|
|
226
|
+
expected.set(file, digest);
|
|
227
|
+
};
|
|
228
|
+
const s = plan.snapshot;
|
|
229
|
+
bind('.ax/entities/stores/' + s.storeId + '.json', s.registrationVersion);
|
|
230
|
+
for (const entry of s.entries) {
|
|
231
|
+
bind(s.storeLocator + '/' + entry.documentLocator, (0, entity_transaction_1.entityContentVersion)(entry.text));
|
|
232
|
+
bind('.ax/entities/records/' + entry.entityId + '.json', entry.mappingVersion);
|
|
233
|
+
}
|
|
234
|
+
for (const source of plan.sources.inventory)
|
|
235
|
+
bind(source.file, source.version);
|
|
236
|
+
bind('.gitignore', plan.sources.ignoreVersion);
|
|
237
|
+
for (const file of plan.policyFiles)
|
|
238
|
+
bind(file.file, file.version);
|
|
239
|
+
for (const step of plan.steps)
|
|
240
|
+
if (!expected.has(step.change.locator))
|
|
241
|
+
bind(step.change.locator, step.change.before === null ? null : (0, entity_transaction_1.entityContentVersion)(Buffer.from(step.change.before, 'base64')));
|
|
242
|
+
return expected;
|
|
243
|
+
}
|
|
244
|
+
function assertVersions(root, expected) {
|
|
245
|
+
for (const [file, digest] of expected)
|
|
246
|
+
if (version(safeRead(root, file)) !== digest)
|
|
247
|
+
throw new entity_transaction_1.EntityStoreError('plan-changed', 'Renumber input changed: ' + file);
|
|
248
|
+
}
|
|
249
|
+
function assertMembership(root, plan, expected) {
|
|
250
|
+
const actualDocuments = [];
|
|
251
|
+
const walk = (relative) => {
|
|
252
|
+
const dir = (0, entity_transaction_1.entityDirectory)(root, relative);
|
|
253
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
254
|
+
const file = relative + '/' + entry.name;
|
|
255
|
+
if (entry.isSymbolicLink())
|
|
256
|
+
throw new entity_transaction_1.EntityStoreError('unsafe-path', 'A symlink appeared in the specification inventory.');
|
|
257
|
+
if (entry.isDirectory())
|
|
258
|
+
walk(file);
|
|
259
|
+
else if (entry.name.endsWith('.md')) {
|
|
260
|
+
safeRead(root, file);
|
|
261
|
+
actualDocuments.push(file);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
walk(plan.snapshot.storeLocator);
|
|
266
|
+
const documents = [...expected].filter(([file, digest]) => digest !== null && file.startsWith(plan.snapshot.storeLocator + '/') && file.endsWith('.md')).map(([file]) => file).sort();
|
|
267
|
+
if (!(0, node_util_1.isDeepStrictEqual)(actualDocuments.sort(), documents))
|
|
268
|
+
throw new entity_transaction_1.EntityStoreError('plan-changed', 'The document inventory changed outside the renumber operation.');
|
|
269
|
+
const records = (0, entity_transaction_1.entityDirectory)(root, '.ax/entities/records');
|
|
270
|
+
const names = fs.readdirSync(records).filter(name => name.endsWith('.json')).sort();
|
|
271
|
+
const expectedNames = plan.snapshot.referenceNamespaces.map(r => r.entityId + '.json').sort();
|
|
272
|
+
if (names.length !== expectedNames.length || names.some((name, i) => name !== expectedNames[i]))
|
|
273
|
+
throw new entity_transaction_1.EntityStoreError('plan-changed', 'The entity namespace inventory changed.');
|
|
274
|
+
for (const ref of plan.snapshot.referenceNamespaces.filter(r => r.storeId !== plan.snapshot.storeId)) {
|
|
275
|
+
const bytes = safeRead(root, '.ax/entities/records/' + ref.entityId + '.json');
|
|
276
|
+
let current;
|
|
277
|
+
try {
|
|
278
|
+
current = JSON.parse(bytes?.toString('utf8') ?? 'null');
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
throw new entity_transaction_1.EntityStoreError('plan-changed', 'An entity namespace record is unreadable.');
|
|
282
|
+
}
|
|
283
|
+
if (!current || Object.entries(ref).some(([key, value]) => current[key] !== value))
|
|
284
|
+
throw new entity_transaction_1.EntityStoreError('ambiguous-reference', 'Another store changed the reference namespace.');
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function validateEntityRenumberPlan(value) {
|
|
288
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
289
|
+
throw new entity_transaction_1.EntityStoreError('invalid-plan', 'Provide the exact inspected renumber plan.');
|
|
290
|
+
const plan = value;
|
|
291
|
+
const keys = ['schema', 'operationId', 'snapshot', 'changes', 'sources', 'policyFiles', 'approvalTargets', 'steps'];
|
|
292
|
+
if (Object.keys(plan).length !== keys.length || keys.some(k => !Object.prototype.hasOwnProperty.call(plan, k)) || plan.schema !== 'holmes-entity-renumber/1' || typeof plan.operationId !== 'string' || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(plan.operationId) || !Array.isArray(plan.changes) || plan.changes.some(c => !c || typeof c.entityId !== 'string' || typeof c.newDisplayId !== 'string'))
|
|
293
|
+
throw new entity_transaction_1.EntityStoreError('invalid-plan', 'Unsupported renumber plan fields.');
|
|
294
|
+
const object = (v, fields) => {
|
|
295
|
+
if (!v || typeof v !== 'object' || Array.isArray(v) || Object.keys(v).length !== fields.length || fields.some(k => !Object.prototype.hasOwnProperty.call(v, k)))
|
|
296
|
+
throw new entity_transaction_1.EntityStoreError('invalid-plan', 'Unsupported nested renumber plan fields.');
|
|
297
|
+
};
|
|
298
|
+
const uuid = (v) => typeof v === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(v);
|
|
299
|
+
const digest = (v) => typeof v === 'string' && /^sha256:[0-9a-f]{64}$/.test(v);
|
|
300
|
+
const locator = (v) => typeof v === 'string' && !!v && !/[\\:\x00]/.test(v) && v.split('/').every(p => !!p && p !== '.' && p !== '..');
|
|
301
|
+
const fail = () => { throw new entity_transaction_1.EntityStoreError('invalid-plan', 'Invalid renumber snapshot or source inventory.'); };
|
|
302
|
+
for (const change of plan.changes)
|
|
303
|
+
object(change, ['entityId', 'newDisplayId']);
|
|
304
|
+
object(plan.snapshot, ['workspaceId', 'storeId', 'storeLocator', 'registrationVersion', 'entries', 'referenceNamespaces']);
|
|
305
|
+
const s = plan.snapshot;
|
|
306
|
+
if (!uuid(s.workspaceId) || !uuid(s.storeId) || !locator(s.storeLocator) || !digest(s.registrationVersion) || !Array.isArray(s.entries) || !Array.isArray(s.referenceNamespaces))
|
|
307
|
+
fail();
|
|
308
|
+
for (const entry of s.entries) {
|
|
309
|
+
object(entry, ['entityId', 'documentLocator', 'text', 'mapping', 'mappingVersion']);
|
|
310
|
+
if (!uuid(entry.entityId) || !locator(entry.documentLocator) || typeof entry.text !== 'string' || typeof entry.mapping !== 'string' || (0, entity_transaction_1.entityContentVersion)(entry.mapping) !== entry.mappingVersion)
|
|
311
|
+
fail();
|
|
312
|
+
let mapping;
|
|
313
|
+
try {
|
|
314
|
+
mapping = JSON.parse(entry.mapping);
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
fail();
|
|
318
|
+
}
|
|
319
|
+
object(mapping, ['schema', 'workspaceId', 'storeId', 'entityId', 'displayId', 'documentLocator', 'origin']);
|
|
320
|
+
object(mapping.origin, ['kind', 'operationId']);
|
|
321
|
+
if (mapping.schema !== 'holmes-entity/1' || mapping.entityId !== entry.entityId || mapping.workspaceId !== s.workspaceId || mapping.storeId !== s.storeId || mapping.documentLocator !== entry.documentLocator || mapping.displayId !== (0, spec_parser_1.parseSpec)(entry.text).id || !uuid(mapping.origin.operationId) || !['adopted', 'created'].includes(mapping.origin.kind))
|
|
322
|
+
fail();
|
|
323
|
+
}
|
|
324
|
+
const seen = new Set();
|
|
325
|
+
for (const ref of s.referenceNamespaces) {
|
|
326
|
+
object(ref, ['entityId', 'displayId', 'storeId', 'workspaceId']);
|
|
327
|
+
if (!uuid(ref.entityId) || !uuid(ref.storeId) || !uuid(ref.workspaceId) || typeof ref.displayId !== 'string' || seen.has(ref.entityId))
|
|
328
|
+
fail();
|
|
329
|
+
seen.add(ref.entityId);
|
|
330
|
+
}
|
|
331
|
+
if (s.entries.some(e => !s.referenceNamespaces.some(r => r.entityId === e.entityId && r.storeId === s.storeId && r.workspaceId === s.workspaceId && r.displayId === (0, spec_parser_1.parseSpec)(e.text).id)) || s.referenceNamespaces.filter(r => r.storeId === s.storeId).length !== s.entries.length)
|
|
332
|
+
fail();
|
|
333
|
+
object(plan.sources, ['files', 'inventory', 'diagnostics', 'proseCandidates', 'ignoreVersion']);
|
|
334
|
+
if (!Array.isArray(plan.sources.files) || !Array.isArray(plan.sources.inventory) || !Array.isArray(plan.sources.diagnostics) || !Array.isArray(plan.sources.proseCandidates) || (plan.sources.ignoreVersion !== null && !digest(plan.sources.ignoreVersion)) || !Array.isArray(plan.policyFiles) || !Array.isArray(plan.approvalTargets) || !Array.isArray(plan.steps))
|
|
335
|
+
fail();
|
|
336
|
+
for (const source of plan.sources.files) {
|
|
337
|
+
object(source, ['file', 'before', 'after', 'version', 'anchorIds']);
|
|
338
|
+
if (!locator(source.file) || typeof source.before !== 'string' || typeof source.after !== 'string' || source.version !== (0, entity_transaction_1.entityContentVersion)(source.before) || !Array.isArray(source.anchorIds) || source.anchorIds.some(id => typeof id !== 'string'))
|
|
339
|
+
fail();
|
|
340
|
+
}
|
|
341
|
+
for (const file of [...plan.sources.inventory, ...plan.policyFiles]) {
|
|
342
|
+
object(file, ['file', 'version']);
|
|
343
|
+
if (!locator(file.file) || (file.version !== null && !digest(file.version)))
|
|
344
|
+
fail();
|
|
345
|
+
}
|
|
346
|
+
for (const step of plan.steps) {
|
|
347
|
+
if (!step || typeof step !== 'object' || Array.isArray(step))
|
|
348
|
+
fail();
|
|
349
|
+
object(step, ['phase', 'change', ...(step.specId === undefined ? [] : ['specId'])]);
|
|
350
|
+
object(step.change, ['locator', 'before', 'after']);
|
|
351
|
+
if (!['unseal', 'document', 'mapping', 'source', 'approve'].includes(step.phase) || !locator(step.change.locator))
|
|
352
|
+
fail();
|
|
353
|
+
for (const bytes of [step.change.before, step.change.after])
|
|
354
|
+
if (bytes !== null && (typeof bytes !== 'string' || Buffer.from(bytes, 'base64').toString('base64') !== bytes))
|
|
355
|
+
fail();
|
|
356
|
+
}
|
|
357
|
+
// Neither application nor recovery trusts after bytes: both regenerate the complete program.
|
|
358
|
+
return JSON.parse(JSON.stringify(plan));
|
|
359
|
+
}
|
|
360
|
+
function entityRenumberRecoveryTarget(plan) {
|
|
361
|
+
return 'entity-store:renumber-recover:' + (0, entity_transaction_1.entityContentVersion)(JSON.stringify(plan));
|
|
362
|
+
}
|
|
363
|
+
function validateEntityRenumberRecoveryPlan(value) {
|
|
364
|
+
const keys = ['schema', 'workspaceId', 'storeLocator', 'operationId', 'journalVersion', 'lockOwner', 'appliedSteps', 'state'];
|
|
365
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).length !== keys.length || keys.some(k => !Object.prototype.hasOwnProperty.call(value, k)))
|
|
366
|
+
throw new entity_transaction_1.EntityStoreError('invalid-plan', 'Provide the exact inspected recovery plan.');
|
|
367
|
+
const p = value;
|
|
368
|
+
const uuid = (v) => typeof v === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(v);
|
|
369
|
+
const digest = (v) => typeof v === 'string' && /^sha256:[0-9a-f]{64}$/.test(v);
|
|
370
|
+
if (p.schema !== 'holmes-entity-renumber-recovery/1' || !uuid(p.workspaceId) || !uuid(p.operationId) || !digest(p.journalVersion) || typeof p.storeLocator !== 'string' || !p.storeLocator || /[\\:\x00]/.test(p.storeLocator) || p.storeLocator.split('/').some(part => !part || part === '.' || part === '..') || !Number.isSafeInteger(p.appliedSteps) || p.appliedSteps < -1 || !['pending', 'applied', 'conflict'].includes(p.state))
|
|
371
|
+
throw new entity_transaction_1.EntityStoreError('invalid-plan', 'Invalid recovery identifiers or state.');
|
|
372
|
+
if (p.lockOwner !== null && (!p.lockOwner || typeof p.lockOwner !== 'object' || Object.keys(p.lockOwner).length !== 2 || !uuid(p.lockOwner.token) || !digest(p.lockOwner.version)))
|
|
373
|
+
throw new entity_transaction_1.EntityStoreError('invalid-plan', 'Invalid recovery ownership binding.');
|
|
374
|
+
return JSON.parse(JSON.stringify(p));
|
|
375
|
+
}
|
|
376
|
+
async function retryCompletedRenumber(storeRoot, request, authorize, audit) {
|
|
377
|
+
const bound = (0, entity_store_1.entityStoreBinding)(storeRoot), directory = '.ax/state/entity-completed/' + request.operationId;
|
|
378
|
+
const receiptBytes = safeRead(bound.root, directory + '/completion.json'), journalBytes = safeRead(bound.root, directory + '/journal.json');
|
|
379
|
+
if (!receiptBytes && !journalBytes)
|
|
380
|
+
return undefined;
|
|
381
|
+
if (!receiptBytes || !journalBytes)
|
|
382
|
+
throw new entity_transaction_1.EntityStoreError('completion-conflict', 'Completion requires both the original receipt and journal.');
|
|
383
|
+
authorize();
|
|
384
|
+
const owner = (0, entity_transaction_1.entityLockOwner)(bound.root, bound.locator);
|
|
385
|
+
const prepare = async () => {
|
|
386
|
+
const assertReceipt = () => {
|
|
387
|
+
if (!safeRead(bound.root, directory + '/completion.json')?.equals(receiptBytes) || !safeRead(bound.root, directory + '/journal.json')?.equals(journalBytes) || safeRead(bound.root, '.ax/state/entity-transactions/' + request.operationId + '/journal.json'))
|
|
388
|
+
throw new entity_transaction_1.EntityStoreError('completion-conflict', 'Completion evidence changed or has a conflicting pending copy.');
|
|
389
|
+
};
|
|
390
|
+
assertReceipt();
|
|
391
|
+
let receipt;
|
|
392
|
+
try {
|
|
393
|
+
receipt = JSON.parse(receiptBytes.toString('utf8'));
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
throw new entity_transaction_1.EntityStoreError('completion-conflict', 'Malformed completion receipt.');
|
|
397
|
+
}
|
|
398
|
+
const fields = ['schema', 'operationId', 'storeLocator', 'lockOwner', 'journalVersion'];
|
|
399
|
+
if (!receipt || Object.keys(receipt).length !== fields.length || fields.some(k => !Object.prototype.hasOwnProperty.call(receipt, k)) || receipt.schema !== 'holmes-entity-completion/1' || receipt.operationId !== request.operationId || receipt.storeLocator !== bound.locator || receipt.journalVersion !== (0, entity_transaction_1.entityContentVersion)(journalBytes))
|
|
400
|
+
throw new entity_transaction_1.EntityStoreError('completion-conflict', 'Completion does not bind this operation and journal.');
|
|
401
|
+
// Reuse the closed owner/schema validator, without treating the former owner as current authority.
|
|
402
|
+
validateEntityRenumberRecoveryPlan({ schema: 'holmes-entity-renumber-recovery/1', workspaceId: request.schema === 'holmes-entity-renumber/1' ? request.snapshot.workspaceId : request.workspaceId, storeLocator: bound.locator,
|
|
403
|
+
operationId: request.operationId, journalVersion: receipt.journalVersion, lockOwner: receipt.lockOwner, appliedSteps: 0, state: 'pending' });
|
|
404
|
+
if (!receipt.lockOwner)
|
|
405
|
+
throw new entity_transaction_1.EntityStoreError('completion-conflict', 'Completion has no owned publication receipt.');
|
|
406
|
+
if (owner && !(0, node_util_1.isDeepStrictEqual)(owner, receipt.lockOwner))
|
|
407
|
+
throw new entity_transaction_1.EntityStoreError('plan-changed', 'The current lock belongs to a different operation.');
|
|
408
|
+
const { plan, sourceOwner, ownershipReceipt } = readRenumberJournal(storeRoot, request.operationId, true);
|
|
409
|
+
if (request.schema === 'holmes-entity-renumber/1') {
|
|
410
|
+
if (!(0, node_util_1.isDeepStrictEqual)(request, plan))
|
|
411
|
+
throw new entity_transaction_1.EntityStoreError('plan-changed', 'The requested plan differs from the completed operation.');
|
|
412
|
+
}
|
|
413
|
+
else if (request.workspaceId !== plan.snapshot.workspaceId || request.storeLocator !== bound.locator || request.journalVersion !== receipt.journalVersion || request.appliedSteps > plan.steps.length || (request.lockOwner && !(0, node_util_1.isDeepStrictEqual)(request.lockOwner, receipt.lockOwner)))
|
|
414
|
+
throw new entity_transaction_1.EntityStoreError('plan-changed', 'The recovery request differs from the completed operation.');
|
|
415
|
+
if (appliedPrefix(bound.root, plan) !== plan.steps.length)
|
|
416
|
+
throw new entity_transaction_1.EntityStoreError('external-change', 'The completed operation no longer matches current files.');
|
|
417
|
+
const proposal = (0, entity_renumber_1.planEntityRenumber)(plan.snapshot.entries, plan.changes);
|
|
418
|
+
if (!proposal.ok)
|
|
419
|
+
throw new entity_transaction_1.EntityStoreError('invalid-plan', proposal.conflicts.join('\n'));
|
|
420
|
+
const originals = new Map(plan.sources.files.map(source => [source.file, source.before]));
|
|
421
|
+
const sources = await (0, renumber_1.planSourceRenumber)(bound.root, proposal.entries.filter(e => e.oldId !== e.newId).map(e => ({ oldId: e.oldId, newId: e.newId })), path.relative(bound.root, bound.store).split(path.sep).join('/'), originals);
|
|
422
|
+
const canonical = prepareEntityRenumberPlan(bound.root, plan.snapshot, plan.changes, sources, plan.operationId, originals);
|
|
423
|
+
if (!(0, node_util_1.isDeepStrictEqual)(canonical, plan))
|
|
424
|
+
throw new entity_transaction_1.EntityStoreError('spec-policy-refused', 'Completed journal differs from canonical source or specification transitions.');
|
|
425
|
+
const expected = inputVersions(canonical);
|
|
426
|
+
for (const step of canonical.steps)
|
|
427
|
+
expected.set(step.change.locator, step.change.after === null ? null : (0, entity_transaction_1.entityContentVersion)(Buffer.from(step.change.after, 'base64')));
|
|
428
|
+
return () => withRecoveredSourcePublication(bound.root, plan.operationId, sourceOwner, () => {
|
|
429
|
+
assertSourceOwnershipReceipt(bound.root, ownershipReceipt);
|
|
430
|
+
if (owner && !(0, node_util_1.isDeepStrictEqual)((0, entity_transaction_1.entityLockOwner)(bound.root, bound.locator), owner))
|
|
431
|
+
throw new entity_transaction_1.EntityStoreError('plan-changed', 'Completion ownership changed while preparing recovery.');
|
|
432
|
+
assertReceipt();
|
|
433
|
+
assertVersions(bound.root, expected);
|
|
434
|
+
assertMembership(bound.root, canonical, expected);
|
|
435
|
+
authorize();
|
|
436
|
+
audit(canonical, true);
|
|
437
|
+
assertSourceOwnershipReceipt(bound.root, ownershipReceipt);
|
|
438
|
+
assertReceipt();
|
|
439
|
+
assertVersions(bound.root, expected);
|
|
440
|
+
return { ok: true, state: 'completed', operationId: canonical.operationId };
|
|
441
|
+
});
|
|
442
|
+
};
|
|
443
|
+
if (!owner)
|
|
444
|
+
return (0, entity_transaction_1.withEntityStoreLockAsync)(bound.root, bound.locator, async () => (await prepare())());
|
|
445
|
+
const finish = await prepare();
|
|
446
|
+
if (!(0, node_util_1.isDeepStrictEqual)((0, entity_transaction_1.entityLockOwner)(bound.root, bound.locator), owner))
|
|
447
|
+
throw new entity_transaction_1.EntityStoreError('plan-changed', 'Completion ownership changed before recovery claim.');
|
|
448
|
+
return (0, entity_transaction_1.withEntityRecoveryLock)(bound.root, bound.locator, () => {
|
|
449
|
+
return finish();
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
function readRenumberJournal(storeRoot, operationId, completed = false) {
|
|
453
|
+
const bound = (0, entity_store_1.entityStoreBinding)(storeRoot);
|
|
454
|
+
const file = '.ax/state/' + (completed ? 'entity-completed/' : 'entity-transactions/') + operationId + '/journal.json', bytes = safeRead(bound.root, file);
|
|
455
|
+
if (!bytes)
|
|
456
|
+
throw new entity_transaction_1.EntityStoreError('plan-changed', 'The pending renumber journal disappeared.');
|
|
457
|
+
let journal;
|
|
458
|
+
try {
|
|
459
|
+
journal = JSON.parse(bytes.toString('utf8'));
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
throw new entity_transaction_1.EntityStoreError('invalid-plan', 'Malformed renumber journal.');
|
|
463
|
+
}
|
|
464
|
+
if (!journal || !isRenumberJournal(journal.schema) || Object.keys(journal).length !== (journal.schema === 'holmes-entity-renumber-journal/2' ? 3 : 2) || !journal.plan)
|
|
465
|
+
throw new entity_transaction_1.EntityStoreError('invalid-plan', 'Unsupported renumber journal fields.');
|
|
466
|
+
const plan = validateEntityRenumberPlan(journal.plan), identity = (0, workspace_identity_1.workspaceIdentity)(bound.root);
|
|
467
|
+
let sourceOwner = journal.schema === 'holmes-entity-renumber-journal/2' ? journal.sourceOwner : null;
|
|
468
|
+
if (journal.schema === 'holmes-entity-renumber-journal/2') {
|
|
469
|
+
if (!sourceOwner)
|
|
470
|
+
throw new entity_transaction_1.EntityStoreError('invalid-plan', 'Source publication requires a journal ownership binding.');
|
|
471
|
+
validateEntityRenumberRecoveryPlan({ schema: 'holmes-entity-renumber-recovery/1', workspaceId: plan.snapshot.workspaceId,
|
|
472
|
+
storeLocator: bound.locator, operationId, journalVersion: (0, entity_transaction_1.entityContentVersion)(bytes), lockOwner: sourceOwner, appliedSteps: 0, state: 'pending' });
|
|
473
|
+
}
|
|
474
|
+
if (plan.operationId !== operationId || plan.snapshot.storeLocator !== bound.locator || identity.state !== 'registered' || identity.workspaceId !== plan.snapshot.workspaceId)
|
|
475
|
+
throw new entity_transaction_1.EntityStoreError('foreign-workspace', 'Journal identity differs from this workspace and store.');
|
|
476
|
+
let registration;
|
|
477
|
+
try {
|
|
478
|
+
registration = JSON.parse(safeRead(bound.root, '.ax/entities/stores/' + plan.snapshot.storeId + '.json')?.toString('utf8') ?? 'null');
|
|
479
|
+
}
|
|
480
|
+
catch {
|
|
481
|
+
throw new entity_transaction_1.EntityStoreError('invalid-plan', 'The registered store is unreadable.');
|
|
482
|
+
}
|
|
483
|
+
if (!registration || registration.schema !== 'holmes-entity-store/1' || registration.workspaceId !== plan.snapshot.workspaceId || registration.storeId !== plan.snapshot.storeId || registration.storeLocator !== bound.locator)
|
|
484
|
+
throw new entity_transaction_1.EntityStoreError('foreign-workspace', 'The journal does not identify the configured registered store.');
|
|
485
|
+
const currentSourceOwner = (0, entity_transaction_1.entityLockOwner)(bound.root, sourcePublication);
|
|
486
|
+
let ownershipReceipt;
|
|
487
|
+
if (currentSourceOwner && !(0, node_util_1.isDeepStrictEqual)(currentSourceOwner, sourceOwner)) {
|
|
488
|
+
const receiptFile = sourceOwnershipPath(file, currentSourceOwner), receiptBytes = safeRead(bound.root, receiptFile);
|
|
489
|
+
if (receiptBytes) {
|
|
490
|
+
if (receiptBytes.toString('utf8') !== sourceOwnershipReceipt(operationId, (0, entity_transaction_1.entityContentVersion)(bytes), currentSourceOwner))
|
|
491
|
+
throw new entity_transaction_1.EntityStoreError('plan-changed', 'Resumed source ownership receipt differs from this operation, journal or current owner.');
|
|
492
|
+
sourceOwner = currentSourceOwner;
|
|
493
|
+
ownershipReceipt = { file: receiptFile, bytes: receiptBytes };
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return { bound, file, bytes, plan, sourceOwner, ownershipReceipt };
|
|
497
|
+
}
|
|
498
|
+
function appliedPrefix(root, plan) {
|
|
499
|
+
// @implements A-SPEC-640 — a lone aside beside an absent target is read as the target.
|
|
500
|
+
const expected = inputVersions(plan), actual = new Map([...expected].map(([file]) => [file, version((0, entity_transaction_1.observeEntityTarget)(root, file).bytes)]));
|
|
501
|
+
const matches = () => [...expected].every(([file, digest]) => actual.get(file) === digest);
|
|
502
|
+
let applied = matches() ? 0 : -1;
|
|
503
|
+
plan.steps.forEach((step, i) => {
|
|
504
|
+
expected.set(step.change.locator, step.change.after === null ? null : (0, entity_transaction_1.entityContentVersion)(Buffer.from(step.change.after, 'base64')));
|
|
505
|
+
if (matches())
|
|
506
|
+
applied = i + 1;
|
|
507
|
+
});
|
|
508
|
+
return applied;
|
|
509
|
+
}
|
|
510
|
+
function planEntityRenumberRecovery(storeRoot) {
|
|
511
|
+
const bound = (0, entity_store_1.entityStoreBinding)(storeRoot), pending = (0, entity_transaction_1.entityDirectory)(bound.root, '.ax/state/entity-transactions');
|
|
512
|
+
const lockOwner = (0, entity_transaction_1.entityLockOwner)(bound.root, bound.locator);
|
|
513
|
+
const sourceOwner = (0, entity_transaction_1.entityLockOwner)(bound.root, sourcePublication);
|
|
514
|
+
const candidates = [];
|
|
515
|
+
if (fs.existsSync(pending))
|
|
516
|
+
for (const operation of fs.readdirSync(pending).sort()) {
|
|
517
|
+
const bytes = safeRead(bound.root, '.ax/state/entity-transactions/' + operation + '/journal.json');
|
|
518
|
+
if (!bytes)
|
|
519
|
+
throw new entity_transaction_1.EntityStoreError('invalid-plan', 'Pending operation has no readable journal.');
|
|
520
|
+
let raw;
|
|
521
|
+
try {
|
|
522
|
+
raw = JSON.parse(bytes.toString('utf8'));
|
|
523
|
+
}
|
|
524
|
+
catch {
|
|
525
|
+
throw new entity_transaction_1.EntityStoreError('invalid-plan', 'Malformed pending journal.');
|
|
526
|
+
}
|
|
527
|
+
if (isRenumberJournal(raw?.schema) && raw.plan?.snapshot?.storeLocator === bound.locator)
|
|
528
|
+
candidates.push({ operation, completed: false });
|
|
529
|
+
}
|
|
530
|
+
const completed = (0, entity_transaction_1.entityDirectory)(bound.root, '.ax/state/entity-completed');
|
|
531
|
+
if ((lockOwner || sourceOwner) && fs.existsSync(completed))
|
|
532
|
+
for (const operation of fs.readdirSync(completed).sort()) {
|
|
533
|
+
const receiptBytes = safeRead(bound.root, '.ax/state/entity-completed/' + operation + '/completion.json');
|
|
534
|
+
if (!receiptBytes)
|
|
535
|
+
continue;
|
|
536
|
+
let receipt;
|
|
537
|
+
try {
|
|
538
|
+
receipt = JSON.parse(receiptBytes.toString('utf8'));
|
|
539
|
+
}
|
|
540
|
+
catch {
|
|
541
|
+
throw new entity_transaction_1.EntityStoreError('completion-conflict', 'Unreadable completed operation receipt.');
|
|
542
|
+
}
|
|
543
|
+
if (receipt?.schema !== 'holmes-entity-completion/1' || receipt.storeLocator !== bound.locator)
|
|
544
|
+
continue;
|
|
545
|
+
if (lockOwner && !(0, node_util_1.isDeepStrictEqual)(receipt.lockOwner, lockOwner))
|
|
546
|
+
continue;
|
|
547
|
+
const journalBytes = safeRead(bound.root, '.ax/state/entity-completed/' + operation + '/journal.json');
|
|
548
|
+
if (!journalBytes || receipt.operationId !== operation || receipt.journalVersion !== (0, entity_transaction_1.entityContentVersion)(journalBytes))
|
|
549
|
+
throw new entity_transaction_1.EntityStoreError('completion-conflict', 'Completed journal differs from its owned receipt.');
|
|
550
|
+
let raw;
|
|
551
|
+
try {
|
|
552
|
+
raw = JSON.parse(journalBytes.toString('utf8'));
|
|
553
|
+
}
|
|
554
|
+
catch {
|
|
555
|
+
throw new entity_transaction_1.EntityStoreError('completion-conflict', 'Unreadable completed journal.');
|
|
556
|
+
}
|
|
557
|
+
if (isRenumberJournal(raw?.schema) && (lockOwner || (sourceOwner && (0, node_util_1.isDeepStrictEqual)(readRenumberJournal(storeRoot, operation, true).sourceOwner, sourceOwner))))
|
|
558
|
+
candidates.push({ operation, completed: true });
|
|
559
|
+
}
|
|
560
|
+
if (candidates.length !== 1)
|
|
561
|
+
throw new entity_transaction_1.EntityStoreError('recovery-selection', 'Recovery requires one pending renumber or completed operation retaining this lock.');
|
|
562
|
+
const { bytes, plan } = readRenumberJournal(storeRoot, candidates[0].operation, candidates[0].completed), appliedSteps = appliedPrefix(bound.root, plan);
|
|
563
|
+
if (!(0, node_util_1.isDeepStrictEqual)((0, entity_transaction_1.entityLockOwner)(bound.root, bound.locator), lockOwner))
|
|
564
|
+
throw new entity_transaction_1.EntityStoreError('plan-changed', 'Recovery ownership changed during inspection.');
|
|
565
|
+
if (!(0, node_util_1.isDeepStrictEqual)((0, entity_transaction_1.entityLockOwner)(bound.root, sourcePublication), sourceOwner))
|
|
566
|
+
throw new entity_transaction_1.EntityStoreError('plan-changed', 'Source ownership changed during inspection.');
|
|
567
|
+
return { schema: 'holmes-entity-renumber-recovery/1', workspaceId: plan.snapshot.workspaceId, storeLocator: bound.locator,
|
|
568
|
+
operationId: plan.operationId, journalVersion: (0, entity_transaction_1.entityContentVersion)(bytes), lockOwner,
|
|
569
|
+
appliedSteps, state: appliedSteps < 0 ? 'conflict' : appliedSteps === plan.steps.length ? 'applied' : 'pending' };
|
|
570
|
+
}
|
|
571
|
+
/** No publication until current source semantics and official transition candidates match the journal. */
|
|
572
|
+
/**
|
|
573
|
+
* @implements A-SPEC-639 — abandon a pending renumber a third state blocked: revert the operation's
|
|
574
|
+
* own publications, preserve foreign bytes, audit and retire it as abandoned under both ownerships.
|
|
575
|
+
*/
|
|
576
|
+
async function abandonEntityRenumber(storeRoot, inspected, authorize, audit) {
|
|
577
|
+
const request = validateEntityRenumberRecoveryPlan(inspected);
|
|
578
|
+
const bound = (0, entity_store_1.entityStoreBinding)(storeRoot);
|
|
579
|
+
if (request.state === 'applied' || safeRead(bound.root, '.ax/state/entity-completed/' + request.operationId + '/completion.json'))
|
|
580
|
+
throw new entity_transaction_1.EntityStoreError('unsupported-recovery', 'Only a pending renumber can be abandoned; a completed operation is already retired.');
|
|
581
|
+
const observed = planEntityRenumberRecovery(storeRoot);
|
|
582
|
+
if (!(0, node_util_1.isDeepStrictEqual)(observed, request))
|
|
583
|
+
throw new entity_transaction_1.EntityStoreError('plan-changed', 'Inspect the current recovery plan before authorizing its abandonment.');
|
|
584
|
+
const { file, bytes, plan, sourceOwner, ownershipReceipt } = readRenumberJournal(storeRoot, observed.operationId);
|
|
585
|
+
authorize();
|
|
586
|
+
return (0, entity_transaction_1.withEntityRecoveryLock)(bound.root, bound.locator, () => withRecoveredSourcePublication(bound.root, plan.operationId, sourceOwner, () => {
|
|
587
|
+
if (!safeRead(bound.root, file)?.equals(bytes))
|
|
588
|
+
throw new entity_transaction_1.EntityStoreError('plan-changed', 'Recovery journal changed.');
|
|
589
|
+
assertSourceOwnershipReceipt(bound.root, ownershipReceipt);
|
|
590
|
+
if (observed.lockOwner && !(0, node_util_1.isDeepStrictEqual)((0, entity_transaction_1.entityLockOwner)(bound.root, bound.locator), observed.lockOwner))
|
|
591
|
+
throw new entity_transaction_1.EntityStoreError('plan-changed', 'Initial ownership changed before abandonment.');
|
|
592
|
+
authorize();
|
|
593
|
+
(0, entity_transaction_1.settleEntityTransients)(bound.root, plan.steps.map(step => step.change)); // @implements A-SPEC-640
|
|
594
|
+
const outcome = (0, entity_transaction_1.abandonPublishedSteps)(bound.root, plan.steps.map(step => step.change));
|
|
595
|
+
audit(plan, outcome);
|
|
596
|
+
(0, entity_transaction_1.abandonEntityOperation)(bound.root, plan.operationId, bound.locator, (0, entity_transaction_1.entityContentVersion)(bytes), outcome);
|
|
597
|
+
return { ok: true, state: 'abandoned', operationId: plan.operationId, ...outcome };
|
|
598
|
+
}, { file, version: (0, entity_transaction_1.entityContentVersion)(bytes) }));
|
|
599
|
+
}
|
|
600
|
+
async function recoverEntityRenumber(storeRoot, inspected, authorize, audit, beforePublish) {
|
|
601
|
+
const request = validateEntityRenumberRecoveryPlan(inspected);
|
|
602
|
+
const completed = await retryCompletedRenumber(storeRoot, request, authorize, audit);
|
|
603
|
+
if (completed)
|
|
604
|
+
return completed;
|
|
605
|
+
const observed = planEntityRenumberRecovery(storeRoot);
|
|
606
|
+
if (!(0, node_util_1.isDeepStrictEqual)(observed, inspected))
|
|
607
|
+
throw new entity_transaction_1.EntityStoreError('plan-changed', 'Inspect the current recovery plan before authorizing it.');
|
|
608
|
+
if (observed.appliedSteps < 0)
|
|
609
|
+
throw new entity_transaction_1.EntityStoreError('external-change', 'Files match no valid renumber stage; preserve external edits.');
|
|
610
|
+
const { bound, file, bytes, plan, sourceOwner, ownershipReceipt } = readRenumberJournal(storeRoot, observed.operationId);
|
|
611
|
+
authorize();
|
|
612
|
+
const proposal = (0, entity_renumber_1.planEntityRenumber)(plan.snapshot.entries, plan.changes);
|
|
613
|
+
if (!proposal.ok)
|
|
614
|
+
throw new entity_transaction_1.EntityStoreError('invalid-plan', proposal.conflicts.join('\n'));
|
|
615
|
+
const originals = new Map(plan.sources.files.map(source => [source.file, source.before]));
|
|
616
|
+
const sources = await (0, renumber_1.planSourceRenumber)(bound.root, proposal.entries.filter(e => e.oldId !== e.newId).map(e => ({ oldId: e.oldId, newId: e.newId })), path.relative(bound.root, bound.store).split(path.sep).join('/'), originals);
|
|
617
|
+
const canonical = prepareEntityRenumberPlan(bound.root, plan.snapshot, plan.changes, sources, plan.operationId, originals);
|
|
618
|
+
if (!(0, node_util_1.isDeepStrictEqual)(canonical, plan))
|
|
619
|
+
throw new entity_transaction_1.EntityStoreError('spec-policy-refused', 'Journal differs from canonical source or official specification transitions.');
|
|
620
|
+
if (!(0, node_util_1.isDeepStrictEqual)(planEntityRenumberRecovery(storeRoot), observed))
|
|
621
|
+
throw new entity_transaction_1.EntityStoreError('plan-changed', 'Recovery inputs changed while computing candidates.');
|
|
622
|
+
return (0, entity_transaction_1.withEntityRecoveryLock)(bound.root, bound.locator, () => withRecoveredSourcePublication(bound.root, plan.operationId, sourceOwner, () => {
|
|
623
|
+
const assertJournal = () => {
|
|
624
|
+
if (!safeRead(bound.root, file)?.equals(bytes))
|
|
625
|
+
throw new entity_transaction_1.EntityStoreError('plan-changed', 'Recovery journal changed.');
|
|
626
|
+
assertSourceOwnershipReceipt(bound.root, ownershipReceipt);
|
|
627
|
+
};
|
|
628
|
+
assertJournal();
|
|
629
|
+
if (observed.lockOwner && !(0, node_util_1.isDeepStrictEqual)((0, entity_transaction_1.entityLockOwner)(bound.root, bound.locator), observed.lockOwner))
|
|
630
|
+
throw new entity_transaction_1.EntityStoreError('plan-changed', 'Initial ownership changed before recovery.');
|
|
631
|
+
const prefix = appliedPrefix(bound.root, canonical);
|
|
632
|
+
if (prefix !== observed.appliedSteps)
|
|
633
|
+
throw new entity_transaction_1.EntityStoreError('plan-changed', 'The inspected renumber stage changed.');
|
|
634
|
+
const expected = inputVersions(canonical);
|
|
635
|
+
for (const step of canonical.steps.slice(0, prefix))
|
|
636
|
+
expected.set(step.change.locator, step.change.after === null ? null : (0, entity_transaction_1.entityContentVersion)(Buffer.from(step.change.after, 'base64')));
|
|
637
|
+
for (const id of canonical.approvalTargets)
|
|
638
|
+
authorize(id);
|
|
639
|
+
(0, entity_transaction_1.settleEntityTransients)(bound.root, canonical.steps.map(step => step.change)); // @implements A-SPEC-640 — after every refusal point, before the replay
|
|
640
|
+
for (const step of canonical.steps.slice(prefix)) {
|
|
641
|
+
assertJournal();
|
|
642
|
+
assertVersions(bound.root, expected);
|
|
643
|
+
assertMembership(bound.root, canonical, expected);
|
|
644
|
+
authorize();
|
|
645
|
+
for (const id of canonical.approvalTargets)
|
|
646
|
+
authorize(id);
|
|
647
|
+
beforePublish?.(canonical);
|
|
648
|
+
(0, entity_transaction_1.applyEntityByteChange)(bound.root, step.change);
|
|
649
|
+
expected.set(step.change.locator, step.change.after === null ? null : (0, entity_transaction_1.entityContentVersion)(Buffer.from(step.change.after, 'base64')));
|
|
650
|
+
}
|
|
651
|
+
assertJournal();
|
|
652
|
+
assertVersions(bound.root, expected);
|
|
653
|
+
assertMembership(bound.root, canonical, expected);
|
|
654
|
+
authorize();
|
|
655
|
+
audit(canonical);
|
|
656
|
+
assertJournal();
|
|
657
|
+
assertVersions(bound.root, expected);
|
|
658
|
+
(0, entity_transaction_1.retireEntityOperation)(bound.root, canonical.operationId, bound.locator, (0, entity_transaction_1.entityContentVersion)(bytes));
|
|
659
|
+
return { ok: true, state: 'completed', operationId: canonical.operationId };
|
|
660
|
+
}, { file, version: (0, entity_transaction_1.entityContentVersion)(bytes) }));
|
|
661
|
+
}
|
|
662
|
+
async function applyEntityRenumber(storeRoot, plan, authorize, audit, beforePublish) {
|
|
663
|
+
const completed = await retryCompletedRenumber(storeRoot, plan, authorize, (_plan, verifyOnly) => audit(verifyOnly));
|
|
664
|
+
if (completed)
|
|
665
|
+
return completed;
|
|
666
|
+
const bound = (0, entity_store_1.entityStoreBinding)(storeRoot);
|
|
667
|
+
return (0, entity_store_1.withEntityRenumberSnapshot)(storeRoot, async (snapshot, assertCurrent) => {
|
|
668
|
+
if (!(0, node_util_1.isDeepStrictEqual)(snapshot, plan.snapshot))
|
|
669
|
+
throw new entity_transaction_1.EntityStoreError('plan-changed', 'The activated document or identity inventory changed.');
|
|
670
|
+
const proposal = (0, entity_renumber_1.planEntityRenumber)(snapshot.entries, plan.changes);
|
|
671
|
+
if (!proposal.ok)
|
|
672
|
+
throw new entity_transaction_1.EntityStoreError('plan-conflict', proposal.conflicts.join('\n'));
|
|
673
|
+
const sources = await (0, renumber_1.planSourceRenumber)(bound.root, proposal.entries.filter(e => e.oldId !== e.newId).map(e => ({ oldId: e.oldId, newId: e.newId })), path.relative(bound.root, bound.store).split(path.sep).join('/'));
|
|
674
|
+
const fresh = prepareEntityRenumberPlan(bound.root, snapshot, plan.changes, sources, plan.operationId);
|
|
675
|
+
if (!(0, node_util_1.isDeepStrictEqual)(fresh, plan))
|
|
676
|
+
throw new entity_transaction_1.EntityStoreError('plan-changed', 'The plan differs from current canonical inputs and transition policy.');
|
|
677
|
+
const expected = inputVersions(fresh);
|
|
678
|
+
return (0, entity_transaction_1.withEntityStoreLock)(bound.root, sourcePublication, () => {
|
|
679
|
+
assertSourceReservation(bound.root, fresh.operationId);
|
|
680
|
+
assertCurrent();
|
|
681
|
+
assertVersions(bound.root, expected);
|
|
682
|
+
authorize();
|
|
683
|
+
for (const id of fresh.approvalTargets)
|
|
684
|
+
authorize(id);
|
|
685
|
+
const journal = JSON.stringify({ schema: 'holmes-entity-renumber-journal/2', plan: fresh, sourceOwner: (0, entity_transaction_1.entityLockOwner)(bound.root, sourcePublication) });
|
|
686
|
+
const journalLocator = '.ax/state/entity-transactions/' + fresh.operationId + '/journal.json';
|
|
687
|
+
(0, entity_transaction_1.publishEntityRecord)(bound.root, journalLocator, journal);
|
|
688
|
+
for (const step of fresh.steps) {
|
|
689
|
+
if (safeRead(bound.root, journalLocator)?.toString('utf8') !== journal)
|
|
690
|
+
throw new entity_transaction_1.EntityStoreError('plan-changed', 'Renumber journal changed during publication.');
|
|
691
|
+
assertVersions(bound.root, expected);
|
|
692
|
+
assertMembership(bound.root, fresh, expected);
|
|
693
|
+
authorize();
|
|
694
|
+
for (const id of fresh.approvalTargets)
|
|
695
|
+
authorize(id);
|
|
696
|
+
if (step.specId)
|
|
697
|
+
authorize(step.specId);
|
|
698
|
+
beforePublish?.(fresh);
|
|
699
|
+
(0, entity_transaction_1.applyEntityByteChange)(bound.root, step.change);
|
|
700
|
+
expected.set(step.change.locator, step.change.after === null ? null : (0, entity_transaction_1.entityContentVersion)(Buffer.from(step.change.after, 'base64')));
|
|
701
|
+
}
|
|
702
|
+
assertVersions(bound.root, expected);
|
|
703
|
+
assertMembership(bound.root, fresh, expected);
|
|
704
|
+
authorize();
|
|
705
|
+
audit();
|
|
706
|
+
assertVersions(bound.root, expected);
|
|
707
|
+
(0, entity_transaction_1.retireEntityOperation)(bound.root, fresh.operationId, bound.locator, (0, entity_transaction_1.entityContentVersion)(journal));
|
|
708
|
+
return { ok: true, state: 'completed', operationId: fresh.operationId };
|
|
709
|
+
});
|
|
710
|
+
});
|
|
711
|
+
}
|