@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
|
@@ -18,9 +18,12 @@ function describeApproval(spec, resolve) {
|
|
|
18
18
|
? { id: pid, status: parent.status, sealed: !!(0, spec_digest_1.sealOf)(parent).approvedDigest, resolved: true }
|
|
19
19
|
: { id: pid, status: 'missing', sealed: false, resolved: false };
|
|
20
20
|
});
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
21
|
+
// `blockers` keeps its meaning — what stands between this document and its approval — so an
|
|
22
|
+
// approved document reports none. The same predicate evaluated on the document AS IT STANDS is
|
|
23
|
+
// what a re-seal would be refused on; a document sealed by hand never passed it, and the acts
|
|
24
|
+
// that re-seal (renumber, integration, re-approval) judge exactly this list (A-SPEC-641).
|
|
25
|
+
const resealBlockers = (0, approval_blockers_1.approvalBlockers)(spec, resolve);
|
|
26
|
+
const blockers = spec.status === 'approved' ? [] : resealBlockers;
|
|
24
27
|
return {
|
|
25
28
|
id: spec.id,
|
|
26
29
|
type: spec.type,
|
|
@@ -29,5 +32,6 @@ function describeApproval(spec, resolve) {
|
|
|
29
32
|
approvedDigest: seal.approvedDigest,
|
|
30
33
|
parents,
|
|
31
34
|
blockers,
|
|
35
|
+
resealBlockers,
|
|
32
36
|
};
|
|
33
37
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export interface EntityGitSnapshot {
|
|
2
|
+
schema: 'holmes-entity-git-snapshot/1';
|
|
3
|
+
commit: string;
|
|
4
|
+
tree: string;
|
|
5
|
+
workspacePrefix: string;
|
|
6
|
+
workspaceId: string;
|
|
7
|
+
storeId: string;
|
|
8
|
+
storeLocator: string;
|
|
9
|
+
/** Names occupied outside the selected store; immutable reads always populate this inventory. */
|
|
10
|
+
otherStoreDisplayIds?: string[];
|
|
11
|
+
entities: Array<{
|
|
12
|
+
entityId: string;
|
|
13
|
+
displayId: string;
|
|
14
|
+
documentLocator: string;
|
|
15
|
+
recordLocator: string;
|
|
16
|
+
documentBlob: string;
|
|
17
|
+
recordBlob: string;
|
|
18
|
+
document: string;
|
|
19
|
+
record: string;
|
|
20
|
+
}>;
|
|
21
|
+
files: Array<{
|
|
22
|
+
path: string;
|
|
23
|
+
mode: string;
|
|
24
|
+
oid: string;
|
|
25
|
+
}>;
|
|
26
|
+
}
|
|
27
|
+
export interface EntityGitInputs {
|
|
28
|
+
base: EntityGitSnapshot;
|
|
29
|
+
left: EntityGitSnapshot;
|
|
30
|
+
right: EntityGitSnapshot;
|
|
31
|
+
}
|
|
32
|
+
/** Version of the workspace-scoped logical stage inventory, not the on-disk index bytes/stat cache. */
|
|
33
|
+
export declare function readEntityGitIndex(root: string): {
|
|
34
|
+
version: string;
|
|
35
|
+
entries: Array<{
|
|
36
|
+
path: string;
|
|
37
|
+
mode: string;
|
|
38
|
+
oid: string;
|
|
39
|
+
stage: number;
|
|
40
|
+
}>;
|
|
41
|
+
};
|
|
42
|
+
/** Binary-safe object reads; names, paths and replace refs cannot redirect an OID request. */
|
|
43
|
+
export declare function readEntityGitBlobs(root: string, oids: string[]): Map<string, Buffer>;
|
|
44
|
+
export declare function readEntityGitInputs(root: string, revisions: {
|
|
45
|
+
base: string;
|
|
46
|
+
left: string;
|
|
47
|
+
right: string;
|
|
48
|
+
}, storeLocator: string): EntityGitInputs;
|
|
49
|
+
/** Read immutable objects only; dirty files, local identity and Git replace refs are not input truth. */
|
|
50
|
+
export declare function readEntityGitSnapshot(root: string, revision: string, storeLocator: string): EntityGitSnapshot;
|
|
@@ -0,0 +1,276 @@
|
|
|
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.readEntityGitIndex = readEntityGitIndex;
|
|
37
|
+
exports.readEntityGitBlobs = readEntityGitBlobs;
|
|
38
|
+
exports.readEntityGitInputs = readEntityGitInputs;
|
|
39
|
+
exports.readEntityGitSnapshot = readEntityGitSnapshot;
|
|
40
|
+
// @implements A-SPEC-629.1, A-SPEC-629
|
|
41
|
+
// @implements A-SPEC-636
|
|
42
|
+
// @implements A-SPEC-637
|
|
43
|
+
const fs = __importStar(require("node:fs"));
|
|
44
|
+
const node_child_process_1 = require("node:child_process");
|
|
45
|
+
const node_crypto_1 = require("node:crypto");
|
|
46
|
+
const spec_parser_1 = require("./spec-parser");
|
|
47
|
+
const entity_transaction_1 = require("./entity-transaction");
|
|
48
|
+
/** Version of the workspace-scoped logical stage inventory, not the on-disk index bytes/stat cache. */
|
|
49
|
+
function readEntityGitIndex(root) {
|
|
50
|
+
const git = gitReader(root);
|
|
51
|
+
const prefix = utf8(git(['rev-parse', '--show-prefix'])).replace(/\n$/, '');
|
|
52
|
+
if (prefix && (!prefix.endsWith('/') || !locator(prefix.slice(0, -1))))
|
|
53
|
+
fail('Unsafe workspace prefix.');
|
|
54
|
+
const bytes = git(['ls-files', '--stage', '-z', '--full-name']);
|
|
55
|
+
const entries = [];
|
|
56
|
+
const seen = new Set();
|
|
57
|
+
for (const entry of utf8(bytes).split('\0')) {
|
|
58
|
+
if (!entry)
|
|
59
|
+
continue;
|
|
60
|
+
const tab = entry.indexOf('\t'), header = entry.slice(0, tab).split(' '), fullPath = entry.slice(tab + 1);
|
|
61
|
+
if (tab < 0 || header.length !== 3 || !/^[0-7]{6}$/.test(header[0]) || !OID.test(header[1]) || !/^[0-3]$/.test(header[2]) || !locator(fullPath) || !fullPath.startsWith(prefix))
|
|
62
|
+
fail('Malformed Git index inventory.');
|
|
63
|
+
const file = fullPath.slice(prefix.length), key = header[2] + ':' + file;
|
|
64
|
+
if (!locator(file) || seen.has(key))
|
|
65
|
+
fail('Unsafe or duplicate Git index entry.');
|
|
66
|
+
seen.add(key);
|
|
67
|
+
entries.push({ path: file, mode: header[0], oid: header[1], stage: Number(header[2]) });
|
|
68
|
+
}
|
|
69
|
+
return { version: 'sha256:' + (0, node_crypto_1.createHash)('sha256').update(prefix).update('\0').update(bytes).digest('hex'), entries };
|
|
70
|
+
}
|
|
71
|
+
/** Binary-safe object reads; names, paths and replace refs cannot redirect an OID request. */
|
|
72
|
+
function readEntityGitBlobs(root, oids) {
|
|
73
|
+
if (!Array.isArray(oids) || oids.some(oid => typeof oid !== 'string' || !OID.test(oid)))
|
|
74
|
+
fail('Git blob reads require immutable object OIDs.');
|
|
75
|
+
const ids = [...new Set(oids)], blobs = new Map();
|
|
76
|
+
if (!ids.length)
|
|
77
|
+
return blobs;
|
|
78
|
+
const output = gitReader(root)(['cat-file', '--batch'], ids.join('\n') + '\n');
|
|
79
|
+
let offset = 0;
|
|
80
|
+
for (const oid of ids) {
|
|
81
|
+
const end = output.indexOf(10, offset);
|
|
82
|
+
if (end < 0)
|
|
83
|
+
fail('Truncated Git blob response.');
|
|
84
|
+
const header = output.subarray(offset, end).toString('ascii').split(' '), size = Number(header[2]);
|
|
85
|
+
if (header.length !== 3 || header[0] !== oid || header[1] !== 'blob' || !Number.isSafeInteger(size) || size < 0 || end + 1 + size >= output.length || output[end + 1 + size] !== 10)
|
|
86
|
+
fail('Invalid or missing Git blob response.');
|
|
87
|
+
blobs.set(oid, output.subarray(end + 1, end + 1 + size));
|
|
88
|
+
offset = end + size + 2;
|
|
89
|
+
}
|
|
90
|
+
if (offset !== output.length)
|
|
91
|
+
fail('Unexpected trailing Git blob response.');
|
|
92
|
+
return blobs;
|
|
93
|
+
}
|
|
94
|
+
function readEntityGitInputs(root, revisions, storeLocator) {
|
|
95
|
+
if (!revisions || typeof revisions !== 'object' || Object.keys(revisions).length !== 3 || !['base', 'left', 'right'].every(key => Object.prototype.hasOwnProperty.call(revisions, key)))
|
|
96
|
+
fail('Expected exactly base, left and right revisions.');
|
|
97
|
+
const git = gitReader(root);
|
|
98
|
+
// Pin all names before loading any tree; subsequent checks never resolve the names again.
|
|
99
|
+
const commits = { base: resolveCommit(git, revisions.base), left: resolveCommit(git, revisions.left), right: resolveCommit(git, revisions.right) };
|
|
100
|
+
for (const side of [commits.left, commits.right])
|
|
101
|
+
git(['merge-base', '--is-ancestor', commits.base, side], undefined, 'Selected base is not a proven ancestor of both Git inputs.');
|
|
102
|
+
// @implements A-SPEC-636 — a refusal caused by one input names that side and its commit.
|
|
103
|
+
const sideSnapshot = (side) => {
|
|
104
|
+
try {
|
|
105
|
+
return readEntityGitSnapshot(root, commits[side], storeLocator);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (error instanceof entity_transaction_1.EntityStoreError && error.code === 'invalid-git-snapshot')
|
|
109
|
+
throw new entity_transaction_1.EntityStoreError('invalid-git-snapshot', side + ' ' + commits[side] + ': ' + error.message);
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
const inputs = { base: sideSnapshot('base'), left: sideSnapshot('left'), right: sideSnapshot('right') };
|
|
114
|
+
for (const side of [inputs.left, inputs.right]) {
|
|
115
|
+
if (side.workspaceId !== inputs.base.workspaceId)
|
|
116
|
+
fail('Git inputs have different canonical workspace identity.');
|
|
117
|
+
if (side.storeId !== inputs.base.storeId)
|
|
118
|
+
fail('Git inputs have different canonical store identity.');
|
|
119
|
+
}
|
|
120
|
+
return inputs;
|
|
121
|
+
}
|
|
122
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
123
|
+
const OID = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
124
|
+
const DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
125
|
+
function fail(message) { throw new entity_transaction_1.EntityStoreError('invalid-git-snapshot', message); }
|
|
126
|
+
function locator(value) {
|
|
127
|
+
return typeof value === 'string' && value.length > 0 && !/[\\:\x00]/.test(value) && value.split('/').every(part => part && part !== '.' && part !== '..');
|
|
128
|
+
}
|
|
129
|
+
function uuid(value) { return typeof value === 'string' && UUID.test(value); }
|
|
130
|
+
function object(text, keys) {
|
|
131
|
+
let value;
|
|
132
|
+
try {
|
|
133
|
+
value = JSON.parse(text);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return fail('Canonical record contains invalid JSON.');
|
|
137
|
+
}
|
|
138
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).length !== keys.length || keys.some(key => !Object.prototype.hasOwnProperty.call(value, key)))
|
|
139
|
+
fail('Unsupported canonical record schema or fields.');
|
|
140
|
+
return value;
|
|
141
|
+
}
|
|
142
|
+
function utf8(bytes) {
|
|
143
|
+
const text = bytes.toString('utf8');
|
|
144
|
+
if (!Buffer.from(text, 'utf8').equals(bytes))
|
|
145
|
+
fail('Unsupported non-UTF-8 canonical bytes or path.');
|
|
146
|
+
return text;
|
|
147
|
+
}
|
|
148
|
+
function gitReader(root) {
|
|
149
|
+
const cwd = fs.realpathSync(root);
|
|
150
|
+
// A caller's Git environment must not redirect a root-bound read to another repository or index.
|
|
151
|
+
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith('GIT_')));
|
|
152
|
+
env.GIT_NO_REPLACE_OBJECTS = '1';
|
|
153
|
+
env.GIT_OPTIONAL_LOCKS = '0';
|
|
154
|
+
return (args, input, failure = 'Git snapshot read failed: unavailable revision/object, unsupported repository or 64 MiB response limit.') => {
|
|
155
|
+
try {
|
|
156
|
+
return (0, node_child_process_1.execFileSync)('git', ['-C', cwd, ...args], { env, input, maxBuffer: 64 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return fail(failure);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function resolveCommit(git, revision) {
|
|
164
|
+
if (typeof revision !== 'string' || !revision || /^-/.test(revision) || /[\x00\r\n]/.test(revision))
|
|
165
|
+
fail('Unsafe Git revision.');
|
|
166
|
+
const commit = utf8(git(['rev-parse', '--verify', '--end-of-options', revision + '^{commit}'])).trim();
|
|
167
|
+
if (!OID.test(commit))
|
|
168
|
+
fail('Git revision did not resolve to a commit object.');
|
|
169
|
+
return commit;
|
|
170
|
+
}
|
|
171
|
+
/** Read immutable objects only; dirty files, local identity and Git replace refs are not input truth. */
|
|
172
|
+
function readEntityGitSnapshot(root, revision, storeLocator) {
|
|
173
|
+
if (!locator(storeLocator))
|
|
174
|
+
fail('Unsafe store locator.');
|
|
175
|
+
const git = gitReader(root), commit = resolveCommit(git, revision);
|
|
176
|
+
const tree = utf8(git(['rev-parse', '--verify', '--end-of-options', commit + '^{tree}'])).trim();
|
|
177
|
+
if (!OID.test(tree))
|
|
178
|
+
fail('Git commit has no supported tree object.');
|
|
179
|
+
const workspacePrefix = utf8(git(['rev-parse', '--show-prefix'])).replace(/\n$/, '');
|
|
180
|
+
if (workspacePrefix && (!workspacePrefix.endsWith('/') || !locator(workspacePrefix.slice(0, -1))))
|
|
181
|
+
fail('Unsafe workspace prefix.');
|
|
182
|
+
const files = [];
|
|
183
|
+
for (const entry of utf8(git(['ls-tree', '-r', '-z', '--full-tree', commit])).split('\0')) {
|
|
184
|
+
if (!entry)
|
|
185
|
+
continue;
|
|
186
|
+
const tab = entry.indexOf('\t'), header = entry.slice(0, tab).split(' '), fullPath = entry.slice(tab + 1);
|
|
187
|
+
if (tab < 0 || header.length !== 3 || !OID.test(header[2]) || !locator(fullPath))
|
|
188
|
+
fail('Malformed Git tree inventory.');
|
|
189
|
+
if (!fullPath.startsWith(workspacePrefix))
|
|
190
|
+
continue;
|
|
191
|
+
const file = fullPath.slice(workspacePrefix.length);
|
|
192
|
+
if (!locator(file))
|
|
193
|
+
fail('Unsafe workspace file locator.');
|
|
194
|
+
// @implements A-SPEC-637 — gitlinks enter the inventory (mode 160000) so the planner can exclude or refuse them.
|
|
195
|
+
const gitlink = header[1] === 'commit' && header[0] === '160000';
|
|
196
|
+
if (!gitlink && (header[1] !== 'blob' || !['100644', '100755', '120000'].includes(header[0])))
|
|
197
|
+
fail('Unsupported Git tree object mode: ' + file);
|
|
198
|
+
files.push({ path: file, mode: header[0], oid: header[2] });
|
|
199
|
+
}
|
|
200
|
+
files.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
201
|
+
const byPath = new Map(files.map(file => [file.path, file]));
|
|
202
|
+
if (byPath.size !== files.length)
|
|
203
|
+
fail('Duplicate Git file inventory.');
|
|
204
|
+
const regular = (file) => {
|
|
205
|
+
const entry = byPath.get(file);
|
|
206
|
+
if (!entry)
|
|
207
|
+
return fail('Missing canonical document or mapping: ' + file);
|
|
208
|
+
if (!['100644', '100755'].includes(entry.mode))
|
|
209
|
+
fail('Canonical document/mapping requires regular mode, not a symlink: ' + file);
|
|
210
|
+
return entry;
|
|
211
|
+
};
|
|
212
|
+
const blobs = new Map();
|
|
213
|
+
const read = (paths) => {
|
|
214
|
+
const ids = [...new Set(paths.map(file => regular(file).oid))].filter(oid => !blobs.has(oid));
|
|
215
|
+
for (const [oid, bytes] of readEntityGitBlobs(root, ids))
|
|
216
|
+
blobs.set(oid, utf8(bytes));
|
|
217
|
+
};
|
|
218
|
+
const text = (file) => {
|
|
219
|
+
const value = blobs.get(regular(file).oid);
|
|
220
|
+
if (value === undefined)
|
|
221
|
+
return fail('Canonical blob was not loaded: ' + file);
|
|
222
|
+
return value;
|
|
223
|
+
};
|
|
224
|
+
const registrations = files.filter(file => /^\.ax\/entities\/stores\/[^/]+\.json$/.test(file.path));
|
|
225
|
+
const recordFiles = files.filter(file => /^\.ax\/entities\/records\/[^/]+\.json$/.test(file.path));
|
|
226
|
+
read(['.ax/workspace.json', ...registrations.map(file => file.path), ...recordFiles.map(file => file.path)]);
|
|
227
|
+
const manifest = object(text('.ax/workspace.json'), ['schema', 'workspaceId']);
|
|
228
|
+
if (manifest.schema !== 'holmes-workspace/1' || !uuid(manifest.workspaceId))
|
|
229
|
+
fail('Unsupported canonical workspace identity.');
|
|
230
|
+
const stores = new Map(), storePaths = new Set();
|
|
231
|
+
for (const file of registrations) {
|
|
232
|
+
const registration = object(text(file.path), ['schema', 'workspaceId', 'storeId', 'storeLocator', 'adoptionDigest']);
|
|
233
|
+
if (registration.schema !== 'holmes-entity-store/1' || registration.workspaceId !== manifest.workspaceId || !uuid(registration.storeId) || file.path !== '.ax/entities/stores/' + registration.storeId + '.json' || !locator(registration.storeLocator) || typeof registration.adoptionDigest !== 'string' || !DIGEST.test(registration.adoptionDigest) || storePaths.has(registration.storeLocator))
|
|
234
|
+
fail('Invalid or duplicate store registration mapping.');
|
|
235
|
+
stores.set(registration.storeId, registration.storeLocator);
|
|
236
|
+
storePaths.add(registration.storeLocator);
|
|
237
|
+
}
|
|
238
|
+
const selected = [...stores].find(([, value]) => value === storeLocator);
|
|
239
|
+
if (!selected)
|
|
240
|
+
fail('Configured store has no canonical adoption mapping.');
|
|
241
|
+
const selectedRecords = [];
|
|
242
|
+
const ids = new Set(), documents = new Set(), otherStoreDisplayIds = new Set();
|
|
243
|
+
for (const file of recordFiles) {
|
|
244
|
+
const record = object(text(file.path), ['schema', 'workspaceId', 'storeId', 'entityId', 'displayId', 'documentLocator', 'origin']);
|
|
245
|
+
const origin = object(JSON.stringify(record.origin), ['kind', 'operationId']);
|
|
246
|
+
if (record.schema !== 'holmes-entity/1' || record.workspaceId !== manifest.workspaceId || !uuid(record.entityId) || file.path !== '.ax/entities/records/' + record.entityId + '.json' || !uuid(record.storeId) || !stores.has(record.storeId) || typeof record.displayId !== 'string' || !record.displayId || !locator(record.documentLocator) || !record.documentLocator.endsWith('.md') || !['adopted', 'created'].includes(String(origin.kind)) || !uuid(origin.operationId))
|
|
247
|
+
fail('Invalid entity document mapping.');
|
|
248
|
+
if (record.storeId !== selected[0]) {
|
|
249
|
+
otherStoreDisplayIds.add(record.displayId);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (ids.has(record.displayId) || documents.has(record.documentLocator))
|
|
253
|
+
fail('Duplicate display ID or document mapping.');
|
|
254
|
+
ids.add(record.displayId);
|
|
255
|
+
documents.add(record.documentLocator);
|
|
256
|
+
selectedRecords.push({ entityId: record.entityId, displayId: record.displayId, documentLocator: record.documentLocator, recordLocator: file.path });
|
|
257
|
+
}
|
|
258
|
+
const documentFiles = files.filter(file => file.path.startsWith(storeLocator + '/') && file.path.endsWith('.md'));
|
|
259
|
+
if (documentFiles.length !== selectedRecords.length || documentFiles.some(file => !documents.has(file.path.slice(storeLocator.length + 1))))
|
|
260
|
+
fail('Document inventory differs from canonical entity mappings.');
|
|
261
|
+
read(selectedRecords.map(record => storeLocator + '/' + record.documentLocator));
|
|
262
|
+
const entities = selectedRecords.map(record => {
|
|
263
|
+
const documentPath = storeLocator + '/' + record.documentLocator, document = text(documentPath);
|
|
264
|
+
let parsed;
|
|
265
|
+
try {
|
|
266
|
+
parsed = (0, spec_parser_1.parseSpec)(document);
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
return fail('Unparseable canonical document: ' + documentPath);
|
|
270
|
+
}
|
|
271
|
+
if (parsed.id !== record.displayId)
|
|
272
|
+
fail('Document ID differs from its entity mapping: ' + documentPath);
|
|
273
|
+
return { ...record, documentBlob: regular(documentPath).oid, recordBlob: regular(record.recordLocator).oid, document, record: text(record.recordLocator) };
|
|
274
|
+
}).sort((a, b) => a.entityId < b.entityId ? -1 : a.entityId > b.entityId ? 1 : 0);
|
|
275
|
+
return { schema: 'holmes-entity-git-snapshot/1', commit, tree, workspacePrefix, workspaceId: manifest.workspaceId, storeId: selected[0], storeLocator, entities, files, otherStoreDisplayIds: [...otherStoreDisplayIds].sort() };
|
|
276
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { GraphBasis } from '../rtm/graph-store';
|
|
2
|
+
export declare const DISCHARGE_SCHEMA = "holmes-integration-derived-discharge/1";
|
|
3
|
+
export interface DerivedDischargeRecord {
|
|
4
|
+
schema: typeof DISCHARGE_SCHEMA;
|
|
5
|
+
operationId: string;
|
|
6
|
+
workspaceId: string;
|
|
7
|
+
storeId: string;
|
|
8
|
+
/** `entityContentVersion` of the exact `derived.json` bytes this record discharges. */
|
|
9
|
+
obligationVersion: string;
|
|
10
|
+
/** The plan's destination commit — what the integration was applied onto. */
|
|
11
|
+
destinationHead: string;
|
|
12
|
+
graph: {
|
|
13
|
+
basis: GraphBasis;
|
|
14
|
+
nodes: number;
|
|
15
|
+
edges: number;
|
|
16
|
+
};
|
|
17
|
+
semantic: {
|
|
18
|
+
status: 'discharged';
|
|
19
|
+
tier: string;
|
|
20
|
+
computed: number;
|
|
21
|
+
cached: number;
|
|
22
|
+
} | {
|
|
23
|
+
status: 'not-applicable';
|
|
24
|
+
tier: 'none';
|
|
25
|
+
};
|
|
26
|
+
/** Every changed locator of the plan, classified against the current working tree. */
|
|
27
|
+
changedPaths: {
|
|
28
|
+
intact: string[];
|
|
29
|
+
drifted: string[];
|
|
30
|
+
missing: string[];
|
|
31
|
+
};
|
|
32
|
+
dischargedAt: string;
|
|
33
|
+
}
|
|
34
|
+
export interface DerivedObligationStatus {
|
|
35
|
+
operationId: string;
|
|
36
|
+
obligationVersion?: string;
|
|
37
|
+
state: 'pending' | 'discharged' | 'invalid';
|
|
38
|
+
reason?: string;
|
|
39
|
+
dischargedAt?: string;
|
|
40
|
+
graphBasisCommit?: string;
|
|
41
|
+
}
|
|
42
|
+
/** The rebuild paths a discharge runs — injected so this module never reaches into the MCP layer. */
|
|
43
|
+
export interface DerivedRebuild {
|
|
44
|
+
publishGraph(): Promise<{
|
|
45
|
+
basis: GraphBasis;
|
|
46
|
+
nodes: number;
|
|
47
|
+
edges: number;
|
|
48
|
+
}>;
|
|
49
|
+
warmSemantic(): Promise<{
|
|
50
|
+
status: 'discharged';
|
|
51
|
+
tier: string;
|
|
52
|
+
computed: number;
|
|
53
|
+
cached: number;
|
|
54
|
+
} | {
|
|
55
|
+
status: 'not-applicable';
|
|
56
|
+
tier: 'none';
|
|
57
|
+
} | {
|
|
58
|
+
status: 'unavailable';
|
|
59
|
+
tier: string;
|
|
60
|
+
}>;
|
|
61
|
+
}
|
|
62
|
+
/** Read-only: every completed operation on this replica that carries derived obligations, and where each stands. */
|
|
63
|
+
export declare function inspectDerivedObligations(root: string): DerivedObligationStatus[];
|
|
64
|
+
/**
|
|
65
|
+
* Discharge one completed operation's obligations on this replica. Verifies completion and the exact
|
|
66
|
+
* obligation bytes, rebuilds through the injected paths, verifies the changed paths and publishes one
|
|
67
|
+
* immutable record; a second writer (or a second call) returns the existing record instead.
|
|
68
|
+
*/
|
|
69
|
+
export declare function dischargeDerivedObligations(root: string, operationId: string, rebuild: DerivedRebuild): Promise<{
|
|
70
|
+
ok: true;
|
|
71
|
+
record: DerivedDischargeRecord;
|
|
72
|
+
alreadyDischarged: boolean;
|
|
73
|
+
}>;
|
|
@@ -0,0 +1,233 @@
|
|
|
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.DISCHARGE_SCHEMA = void 0;
|
|
37
|
+
exports.inspectDerivedObligations = inspectDerivedObligations;
|
|
38
|
+
exports.dischargeDerivedObligations = dischargeDerivedObligations;
|
|
39
|
+
// @implements A-SPEC-632
|
|
40
|
+
/**
|
|
41
|
+
* Discharge of a completed integration's derived-state obligations.
|
|
42
|
+
*
|
|
43
|
+
* `applyEntityIntegration` ends by writing `derived.json` (graph: pending, semantic: pending) into
|
|
44
|
+
* the operation's completed directory and the completed retry verifies those bytes exactly, so the
|
|
45
|
+
* obligation itself is immutable history. This module adds the second, immutable half:
|
|
46
|
+
* `discharge.json` states what was rebuilt, from which basis, and whether the tree still holds the
|
|
47
|
+
* integration's bytes. The rebuild is not new machinery — the caller injects the graph publication
|
|
48
|
+
* (REQ-631) and the semantic warming path reindex already uses — so discharge is verification plus
|
|
49
|
+
* binding. Derived state is replica-local (`.ax/state`), therefore so is the record: nothing under
|
|
50
|
+
* `.ax/entities`, the specs, the shared ledger or Git is touched.
|
|
51
|
+
*/
|
|
52
|
+
const fs = __importStar(require("node:fs"));
|
|
53
|
+
const path = __importStar(require("node:path"));
|
|
54
|
+
const node_util_1 = require("node:util");
|
|
55
|
+
const entity_transaction_1 = require("./entity-transaction");
|
|
56
|
+
const entity_integration_transaction_1 = require("./entity-integration-transaction");
|
|
57
|
+
exports.DISCHARGE_SCHEMA = 'holmes-integration-derived-discharge/1';
|
|
58
|
+
const OBLIGATION_SCHEMA = 'holmes-integration-derived-obligations/1';
|
|
59
|
+
const COMPLETED = '.ax/state/entity-completed';
|
|
60
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
61
|
+
const RECORD_KEYS = ['schema', 'operationId', 'workspaceId', 'storeId', 'obligationVersion', 'destinationHead', 'graph', 'semantic', 'changedPaths', 'dischargedAt'];
|
|
62
|
+
/** The obligation `applyEntityIntegration` wrote for this plan — the same computation the completed retry verifies. */
|
|
63
|
+
function expectedObligation(plan) {
|
|
64
|
+
return {
|
|
65
|
+
schema: OBLIGATION_SCHEMA, operationId: plan.operationId, workspaceId: plan.workspaceId, storeId: plan.storeId,
|
|
66
|
+
target: (0, entity_integration_transaction_1.entityIntegrationTarget)(plan), graph: 'pending', semantic: 'pending',
|
|
67
|
+
changedPaths: [...new Set(plan.steps.map((step) => step.change.locator))].sort(),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function parseRecord(bytes) {
|
|
71
|
+
let value;
|
|
72
|
+
try {
|
|
73
|
+
value = JSON.parse(bytes.toString('utf8'));
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
throw new entity_transaction_1.EntityStoreError('discharge-conflict', 'The discharge record is not valid JSON.');
|
|
77
|
+
}
|
|
78
|
+
const record = value;
|
|
79
|
+
const shaped = !!record && typeof record === 'object' && !Array.isArray(record)
|
|
80
|
+
&& (0, node_util_1.isDeepStrictEqual)(Object.keys(record).sort(), [...RECORD_KEYS].sort())
|
|
81
|
+
&& record.schema === exports.DISCHARGE_SCHEMA && typeof record.operationId === 'string' && typeof record.obligationVersion === 'string'
|
|
82
|
+
&& typeof record.dischargedAt === 'string' && !!record.graph && typeof record.graph === 'object' && !!record.semantic && typeof record.semantic === 'object'
|
|
83
|
+
&& !!record.changedPaths && typeof record.changedPaths === 'object';
|
|
84
|
+
if (!shaped)
|
|
85
|
+
throw new entity_transaction_1.EntityStoreError('discharge-conflict', 'The discharge record has an unknown shape.');
|
|
86
|
+
return record;
|
|
87
|
+
}
|
|
88
|
+
/** An existing record for this obligation, undefined when absent; a record bound elsewhere is a conflict. */
|
|
89
|
+
function existingRecord(file, operationId, obligationVersion) {
|
|
90
|
+
const bytes = (0, entity_transaction_1.readEntityBytes)(file);
|
|
91
|
+
if (!bytes)
|
|
92
|
+
return undefined;
|
|
93
|
+
const record = parseRecord(bytes);
|
|
94
|
+
if (record.operationId !== operationId || record.obligationVersion !== obligationVersion) {
|
|
95
|
+
throw new entity_transaction_1.EntityStoreError('discharge-conflict', 'An existing discharge record binds a different operation or obligation; preserve it and inspect before retrying.');
|
|
96
|
+
}
|
|
97
|
+
return record;
|
|
98
|
+
}
|
|
99
|
+
/** Read-only: every completed operation on this replica that carries derived obligations, and where each stands. */
|
|
100
|
+
function inspectDerivedObligations(root) {
|
|
101
|
+
let dir;
|
|
102
|
+
try {
|
|
103
|
+
dir = (0, entity_transaction_1.entityDirectory)(fs.realpathSync(root), COMPLETED);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
let names;
|
|
109
|
+
try {
|
|
110
|
+
names = fs.readdirSync(dir).filter((name) => UUID.test(name)).sort();
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
const out = [];
|
|
116
|
+
for (const operationId of names) {
|
|
117
|
+
const operation = path.join(dir, operationId);
|
|
118
|
+
let derivedBytes;
|
|
119
|
+
try {
|
|
120
|
+
derivedBytes = (0, entity_transaction_1.readEntityBytes)(path.join(operation, 'derived.json'));
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
out.push({ operationId, state: 'invalid', reason: 'derived.json is not a readable regular file' });
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (!derivedBytes)
|
|
127
|
+
continue; // a completed operation without derived obligations is not this module's concern
|
|
128
|
+
let derived = null;
|
|
129
|
+
try {
|
|
130
|
+
derived = JSON.parse(derivedBytes.toString('utf8'));
|
|
131
|
+
}
|
|
132
|
+
catch { /* reported below */ }
|
|
133
|
+
if (!derived || derived.schema !== OBLIGATION_SCHEMA || derived.operationId !== operationId) {
|
|
134
|
+
out.push({ operationId, state: 'invalid', reason: 'derived.json does not carry this operation\'s obligations' });
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const obligationVersion = (0, entity_transaction_1.entityContentVersion)(derivedBytes);
|
|
138
|
+
try {
|
|
139
|
+
const record = existingRecord(path.join(operation, 'discharge.json'), operationId, obligationVersion);
|
|
140
|
+
if (!record)
|
|
141
|
+
out.push({ operationId, obligationVersion, state: 'pending' });
|
|
142
|
+
else
|
|
143
|
+
out.push({ operationId, obligationVersion, state: 'discharged', dischargedAt: record.dischargedAt, graphBasisCommit: record.graph.basis.sourceCommit });
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
out.push({ operationId, obligationVersion, state: 'invalid', reason: error instanceof Error ? error.message : String(error) });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
/** Classify every changed locator of the plan against the current tree; the last step for a locator is its final state. */
|
|
152
|
+
function verifyChangedPaths(root, plan) {
|
|
153
|
+
const final = new Map();
|
|
154
|
+
for (const step of plan.steps)
|
|
155
|
+
final.set(step.change.locator, step.change);
|
|
156
|
+
const result = { intact: [], drifted: [], missing: [] };
|
|
157
|
+
for (const [locator, change] of [...final.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) {
|
|
158
|
+
let current;
|
|
159
|
+
try {
|
|
160
|
+
current = (0, entity_transaction_1.readEntityBytes)(path.join((0, entity_transaction_1.entityDirectory)(root, path.posix.dirname(locator)), path.posix.basename(locator)));
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
current = undefined;
|
|
164
|
+
}
|
|
165
|
+
if (change.after === null) {
|
|
166
|
+
(current === undefined ? result.intact : result.drifted).push(locator);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (current === undefined)
|
|
170
|
+
result.missing.push(locator);
|
|
171
|
+
else if (current.equals(Buffer.from(change.after, 'base64')))
|
|
172
|
+
result.intact.push(locator);
|
|
173
|
+
else
|
|
174
|
+
result.drifted.push(locator);
|
|
175
|
+
}
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Discharge one completed operation's obligations on this replica. Verifies completion and the exact
|
|
180
|
+
* obligation bytes, rebuilds through the injected paths, verifies the changed paths and publishes one
|
|
181
|
+
* immutable record; a second writer (or a second call) returns the existing record instead.
|
|
182
|
+
*/
|
|
183
|
+
async function dischargeDerivedObligations(root, operationId, rebuild) {
|
|
184
|
+
if (!UUID.test(operationId))
|
|
185
|
+
throw new entity_transaction_1.EntityStoreError('invalid-plan', 'Provide the completed integration operation UUID.');
|
|
186
|
+
root = fs.realpathSync(root);
|
|
187
|
+
const observed = (0, entity_integration_transaction_1.inspectEntityIntegrationRecovery)(root, operationId);
|
|
188
|
+
if (observed.state !== 'completed') {
|
|
189
|
+
throw new entity_transaction_1.EntityStoreError('discharge-requires-completion', 'Derived obligations are discharged only for a completed integration; recover the pending operation first.');
|
|
190
|
+
}
|
|
191
|
+
const plan = observed.plan;
|
|
192
|
+
const directory = (0, entity_transaction_1.entityDirectory)(root, COMPLETED + '/' + operationId);
|
|
193
|
+
const derivedBytes = (0, entity_transaction_1.readEntityBytes)(path.join(directory, 'derived.json'));
|
|
194
|
+
if (!derivedBytes)
|
|
195
|
+
throw new entity_transaction_1.EntityStoreError('obligation-conflict', 'The completed integration has no derived obligations record.');
|
|
196
|
+
let derived;
|
|
197
|
+
try {
|
|
198
|
+
derived = JSON.parse(derivedBytes.toString('utf8'));
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
throw new entity_transaction_1.EntityStoreError('obligation-conflict', 'The derived obligations record is not valid JSON.');
|
|
202
|
+
}
|
|
203
|
+
if (!(0, node_util_1.isDeepStrictEqual)(derived, expectedObligation(plan))) {
|
|
204
|
+
throw new entity_transaction_1.EntityStoreError('obligation-conflict', 'Derived obligations differ from the completed plan; the historical record was altered and is preserved as-is.');
|
|
205
|
+
}
|
|
206
|
+
const obligationVersion = (0, entity_transaction_1.entityContentVersion)(derivedBytes);
|
|
207
|
+
const dischargeFile = path.join(directory, 'discharge.json');
|
|
208
|
+
const existing = existingRecord(dischargeFile, operationId, obligationVersion);
|
|
209
|
+
if (existing)
|
|
210
|
+
return { ok: true, record: existing, alreadyDischarged: true };
|
|
211
|
+
const changedPaths = verifyChangedPaths(root, plan);
|
|
212
|
+
const graph = await rebuild.publishGraph();
|
|
213
|
+
const semantic = await rebuild.warmSemantic();
|
|
214
|
+
if (semantic.status === 'unavailable') {
|
|
215
|
+
throw new entity_transaction_1.EntityStoreError('semantic-unavailable', `The configured semantic tier (${semantic.tier}) produced no embeddings; the graph was published and the obligation stays pending — retry once the tier is available.`);
|
|
216
|
+
}
|
|
217
|
+
const record = {
|
|
218
|
+
schema: exports.DISCHARGE_SCHEMA, operationId, workspaceId: plan.workspaceId, storeId: plan.storeId, obligationVersion,
|
|
219
|
+
destinationHead: plan.destination.head, graph, semantic, changedPaths, dischargedAt: new Date().toISOString(),
|
|
220
|
+
};
|
|
221
|
+
try {
|
|
222
|
+
(0, entity_transaction_1.publishEntityRecord)(root, COMPLETED + '/' + operationId + '/discharge.json', JSON.stringify(record));
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
if (error.code !== 'EEXIST')
|
|
226
|
+
throw error;
|
|
227
|
+
const winner = existingRecord(dischargeFile, operationId, obligationVersion); // a concurrent discharger published first
|
|
228
|
+
if (!winner)
|
|
229
|
+
throw error;
|
|
230
|
+
return { ok: true, record: winner, alreadyDischarged: true };
|
|
231
|
+
}
|
|
232
|
+
return { ok: true, record, alreadyDischarged: false };
|
|
233
|
+
}
|