@holmes-lab/holmes-kit 0.19.6 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +8 -1
  3. package/dist/.build-id +1 -1
  4. package/dist/holmes/cli/approve-status.d.ts +24 -0
  5. package/dist/holmes/cli/approve-status.js +166 -0
  6. package/dist/holmes/cli/approve-watch.d.ts +6 -0
  7. package/dist/holmes/cli/approve-watch.js +6 -0
  8. package/dist/holmes/cli/approve.d.ts +27 -0
  9. package/dist/holmes/cli/approve.js +68 -7
  10. package/dist/holmes/cli/cli-execution.d.ts +2 -0
  11. package/dist/holmes/cli/cli-execution.js +24 -0
  12. package/dist/holmes/cli/gitignore-merge.d.ts +0 -5
  13. package/dist/holmes/cli/gitignore-merge.js +6 -2
  14. package/dist/holmes/cli/index.js +176 -139
  15. package/dist/holmes/cli/playbook-skills.d.ts +2 -22
  16. package/dist/holmes/cli/playbook-skills.js +10 -54
  17. package/dist/holmes/governance/approval-grants.d.ts +22 -0
  18. package/dist/holmes/governance/approval-grants.js +55 -1
  19. package/dist/holmes/governance/approval-queue.d.ts +43 -4
  20. package/dist/holmes/governance/approval-queue.js +86 -12
  21. package/dist/holmes/governance/display-id-aliases.d.ts +29 -0
  22. package/dist/holmes/governance/display-id-aliases.js +86 -0
  23. package/dist/holmes/governance/ledger-store.d.ts +42 -14
  24. package/dist/holmes/governance/ledger-store.js +49 -3
  25. package/dist/holmes/governance/ledger-timeline.d.ts +3 -1
  26. package/dist/holmes/governance/ledger-timeline.js +11 -1
  27. package/dist/holmes/governance/provenance-chain.d.ts +2 -0
  28. package/dist/holmes/governance/provenance-chain.js +13 -2
  29. package/dist/holmes/governance/provenance-ledger.d.ts +28 -0
  30. package/dist/holmes/governance/provenance-ledger.js +69 -16
  31. package/dist/holmes/governance/session-context.d.ts +3 -0
  32. package/dist/holmes/governance/session-context.js +26 -8
  33. package/dist/holmes/guardrail/risk-classifier.js +2 -2
  34. package/dist/holmes/guardrail/write-target.js +2 -1
  35. package/dist/holmes/hooks/pre-tool-use.js +12 -1
  36. package/dist/holmes/hooks/stop.js +14 -3
  37. package/dist/holmes/mcp/handlers/adoption.d.ts +118 -0
  38. package/dist/holmes/mcp/handlers/adoption.js +186 -0
  39. package/dist/holmes/mcp/handlers/entity-integration.d.ts +100 -0
  40. package/dist/holmes/mcp/handlers/entity-integration.js +324 -0
  41. package/dist/holmes/mcp/handlers/entity-renumber.d.ts +137 -0
  42. package/dist/holmes/mcp/handlers/entity-renumber.js +228 -0
  43. package/dist/holmes/mcp/handlers/entity-store.d.ts +78 -0
  44. package/dist/holmes/mcp/handlers/entity-store.js +212 -0
  45. package/dist/holmes/mcp/handlers/graph-operations.d.ts +90 -0
  46. package/dist/holmes/mcp/handlers/graph-operations.js +260 -0
  47. package/dist/holmes/mcp/handlers/maintenance-analysis.d.ts +34 -0
  48. package/dist/holmes/mcp/handlers/maintenance-analysis.js +288 -0
  49. package/dist/holmes/mcp/handlers/maintenance-evidence.d.ts +40 -0
  50. package/dist/holmes/mcp/handlers/maintenance-evidence.js +104 -0
  51. package/dist/holmes/mcp/handlers/operator-inspection.d.ts +167 -0
  52. package/dist/holmes/mcp/handlers/operator-inspection.js +284 -0
  53. package/dist/holmes/mcp/handlers/phase-queries.d.ts +34 -0
  54. package/dist/holmes/mcp/handlers/phase-queries.js +86 -0
  55. package/dist/holmes/mcp/handlers/review-evidence.d.ts +47 -0
  56. package/dist/holmes/mcp/handlers/review-evidence.js +370 -0
  57. package/dist/holmes/mcp/handlers/review-queries.d.ts +68 -0
  58. package/dist/holmes/mcp/handlers/review-queries.js +79 -0
  59. package/dist/holmes/mcp/handlers/risk-assessment.d.ts +25 -0
  60. package/dist/holmes/mcp/handlers/risk-assessment.js +181 -0
  61. package/dist/holmes/mcp/handlers/slice-orchestration.d.ts +55 -0
  62. package/dist/holmes/mcp/handlers/slice-orchestration.js +324 -0
  63. package/dist/holmes/mcp/handlers/spec-approval.d.ts +105 -0
  64. package/dist/holmes/mcp/handlers/spec-approval.js +504 -0
  65. package/dist/holmes/mcp/handlers/spec-authoring.d.ts +57 -0
  66. package/dist/holmes/mcp/handlers/spec-authoring.js +326 -0
  67. package/dist/holmes/mcp/handlers/spec-lifecycle.d.ts +122 -0
  68. package/dist/holmes/mcp/handlers/spec-lifecycle.js +339 -0
  69. package/dist/holmes/mcp/handlers/spec-queries.d.ts +30 -0
  70. package/dist/holmes/mcp/handlers/spec-queries.js +74 -0
  71. package/dist/holmes/mcp/handlers/taint-screening.d.ts +48 -0
  72. package/dist/holmes/mcp/handlers/taint-screening.js +183 -0
  73. package/dist/holmes/mcp/handlers/test-execution.d.ts +63 -0
  74. package/dist/holmes/mcp/handlers/test-execution.js +210 -0
  75. package/dist/holmes/mcp/handlers/workspace-identity.d.ts +30 -0
  76. package/dist/holmes/mcp/handlers/workspace-identity.js +68 -0
  77. package/dist/holmes/mcp/handlers/workspace-queries.d.ts +77 -0
  78. package/dist/holmes/mcp/handlers/workspace-queries.js +193 -0
  79. package/dist/holmes/mcp/handlers.d.ts +653 -424
  80. package/dist/holmes/mcp/handlers.js +267 -3250
  81. package/dist/holmes/mcp/server.js +2 -1
  82. package/dist/holmes/mcp/tool-schemas.js +43 -2
  83. package/dist/holmes/project/execution-context.d.ts +17 -0
  84. package/dist/holmes/project/execution-context.js +120 -0
  85. package/dist/holmes/project/installer-markers.d.ts +34 -0
  86. package/dist/holmes/project/installer-markers.js +65 -0
  87. package/dist/holmes/project/root.d.ts +3 -1
  88. package/dist/holmes/project/root.js +4 -3
  89. package/dist/holmes/project/workspace-identity.d.ts +29 -0
  90. package/dist/holmes/project/workspace-identity.js +181 -0
  91. package/dist/holmes/review/test-outcomes.d.ts +6 -1
  92. package/dist/holmes/review/test-outcomes.js +23 -5
  93. package/dist/holmes/review/test-runner.d.ts +18 -0
  94. package/dist/holmes/review/test-runner.js +132 -4
  95. package/dist/holmes/rtm/graph-store.d.ts +50 -1
  96. package/dist/holmes/rtm/graph-store.js +244 -3
  97. package/dist/holmes/rtm/incremental.d.ts +1 -0
  98. package/dist/holmes/rtm/incremental.js +12 -3
  99. package/dist/holmes/rtm/localize.js +7 -0
  100. package/dist/holmes/rtm/rtm-graph.d.ts +9 -0
  101. package/dist/holmes/rtm/rtm-graph.js +13 -0
  102. package/dist/holmes/semantic/vector-cache.d.ts +16 -1
  103. package/dist/holmes/semantic/vector-cache.js +155 -19
  104. package/dist/holmes/spec/approval-status.d.ts +10 -0
  105. package/dist/holmes/spec/approval-status.js +7 -3
  106. package/dist/holmes/spec/entity-git-snapshot.d.ts +50 -0
  107. package/dist/holmes/spec/entity-git-snapshot.js +276 -0
  108. package/dist/holmes/spec/entity-integration-discharge.d.ts +73 -0
  109. package/dist/holmes/spec/entity-integration-discharge.js +233 -0
  110. package/dist/holmes/spec/entity-integration-transaction.d.ts +179 -0
  111. package/dist/holmes/spec/entity-integration-transaction.js +1221 -0
  112. package/dist/holmes/spec/entity-integration.d.ts +207 -0
  113. package/dist/holmes/spec/entity-integration.js +747 -0
  114. package/dist/holmes/spec/entity-renumber-transaction.d.ts +82 -0
  115. package/dist/holmes/spec/entity-renumber-transaction.js +711 -0
  116. package/dist/holmes/spec/entity-renumber.d.ts +32 -0
  117. package/dist/holmes/spec/entity-renumber.js +156 -0
  118. package/dist/holmes/spec/entity-store.d.ts +135 -0
  119. package/dist/holmes/spec/entity-store.js +1051 -0
  120. package/dist/holmes/spec/entity-transaction.d.ts +89 -0
  121. package/dist/holmes/spec/entity-transaction.js +701 -0
  122. package/dist/holmes/spec/renumber.d.ts +58 -0
  123. package/dist/holmes/spec/renumber.js +200 -1
  124. package/dist/holmes/spec/spec-store.d.ts +3 -2
  125. package/dist/holmes/spec/spec-store.js +23 -2
  126. package/dist/holmes/spec/transition-policy.d.ts +36 -0
  127. package/dist/holmes/spec/transition-policy.js +135 -0
  128. package/package.json +1 -1
@@ -0,0 +1,1221 @@
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.prepareEntityIntegrationTransitions = prepareEntityIntegrationTransitions;
37
+ exports.validateEntityIntegrationPlan = validateEntityIntegrationPlan;
38
+ exports.entityIntegrationTarget = entityIntegrationTarget;
39
+ exports.applyEntityIntegration = applyEntityIntegration;
40
+ exports.abandonEntityIntegration = abandonEntityIntegration;
41
+ exports.recoverEntityIntegration = recoverEntityIntegration;
42
+ exports.validateEntityIntegrationRecoveryRequest = validateEntityIntegrationRecoveryRequest;
43
+ exports.inspectEntityIntegrationRecovery = inspectEntityIntegrationRecovery;
44
+ exports.validateEntityIntegrationRecoveryProgram = validateEntityIntegrationRecoveryProgram;
45
+ exports.withValidatedEntityIntegrationPlan = withValidatedEntityIntegrationPlan;
46
+ exports.prepareEntityIntegrationPlan = prepareEntityIntegrationPlan;
47
+ exports.reviewEntityIntegration = reviewEntityIntegration;
48
+ exports.compileEntityIntegrationSteps = compileEntityIntegrationSteps;
49
+ exports.planEntityIntegrationCandidate = planEntityIntegrationCandidate;
50
+ // @implements A-SPEC-629
51
+ // @implements A-SPEC-645
52
+ // @implements A-SPEC-641
53
+ // @implements A-SPEC-640
54
+ // @implements A-SPEC-639
55
+ // @implements A-SPEC-636
56
+ // @implements A-SPEC-637
57
+ // @implements A-SPEC-634
58
+ const entity_transaction_1 = require("./entity-transaction");
59
+ const entity_integration_1 = require("./entity-integration");
60
+ const entity_git_snapshot_1 = require("./entity-git-snapshot");
61
+ const entity_renumber_transaction_1 = require("./entity-renumber-transaction");
62
+ const entity_store_1 = require("./entity-store");
63
+ const node_util_1 = require("node:util");
64
+ const fs = __importStar(require("node:fs"));
65
+ const path = __importStar(require("node:path"));
66
+ const ignore_1 = require("../project/ignore");
67
+ const change_source_1 = require("../project/change-source");
68
+ const cpg_scanner_1 = require("../cpg/cpg-scanner");
69
+ const entity_renumber_1 = require("./entity-renumber");
70
+ const spec_parser_1 = require("./spec-parser");
71
+ const spec_digest_1 = require("./spec-digest");
72
+ const transition_policy_1 = require("./transition-policy");
73
+ const node_crypto_1 = require("node:crypto");
74
+ const workspace_identity_1 = require("../project/workspace-identity");
75
+ /** Produces document transitions only; authority and durable application belong to the executor. */
76
+ function prepareEntityIntegrationTransitions(current, desired, readFile) {
77
+ const graph = (entries) => {
78
+ const validation = (0, entity_renumber_1.planEntityRenumber)(entries, []);
79
+ if (!validation.ok)
80
+ throw new entity_transaction_1.EntityStoreError('plan-conflict', validation.conflicts.join('\n'));
81
+ const byEntity = new Map(entries.map(entry => [entry.entityId, { ...entry }]));
82
+ const specs = new Map(entries.map(entry => [entry.entityId, (0, spec_parser_1.parseSpec)(entry.text)]));
83
+ const byId = new Map([...specs].map(([id, spec]) => [spec.id, id]));
84
+ const order = [], visited = new Set();
85
+ const visit = (id) => {
86
+ if (visited.has(id))
87
+ return;
88
+ visited.add(id);
89
+ for (const parent of specs.get(id).dependsOn.map(parent => byId.get(parent)).sort())
90
+ visit(parent);
91
+ order.push(id);
92
+ };
93
+ for (const id of [...byEntity.keys()].sort())
94
+ visit(id);
95
+ return { byEntity, specs, byId, order };
96
+ };
97
+ const before = graph(current), after = graph(desired), affected = new Set();
98
+ for (const id of new Set([...before.byEntity.keys(), ...after.byEntity.keys()])) {
99
+ const old = before.byEntity.get(id), next = after.byEntity.get(id);
100
+ if (!old || !next || old.text !== next.text || old.documentLocator !== next.documentLocator)
101
+ affected.add(id);
102
+ }
103
+ for (const [id, spec] of after.specs)
104
+ if (spec.status === 'approved') {
105
+ const parents = {};
106
+ let validParents = true;
107
+ for (const parentId of spec.dependsOn) {
108
+ const parent = after.specs.get(after.byId.get(parentId));
109
+ if (parent.status !== 'approved' || typeof parent.frontmatter.approved_digest !== 'string' || parent.frontmatter.approved_digest !== (0, spec_digest_1.specDigest)(parent))
110
+ validParents = false;
111
+ else
112
+ parents[parentId] = parent.frontmatter.approved_digest;
113
+ }
114
+ if (spec.frontmatter.approved_digest !== (0, spec_digest_1.specDigest)(spec) || !validParents || !(0, node_util_1.isDeepStrictEqual)(spec.frontmatter.parent_digests, parents))
115
+ affected.add(id);
116
+ }
117
+ // Current dependents may have different parents after integration. Close both graphs to a fixed point.
118
+ let previousSize = -1;
119
+ while (previousSize !== affected.size) {
120
+ previousSize = affected.size;
121
+ for (const g of [before, after])
122
+ for (const id of g.order) {
123
+ if (g.specs.get(id).dependsOn.some(parent => affected.has(g.byId.get(parent))))
124
+ affected.add(id);
125
+ }
126
+ }
127
+ const result = { unseal: [], publication: [], approve: [], completed: [] };
128
+ const all = (g) => [...g.specs.values()];
129
+ const withdraw = (g, id, record) => {
130
+ const entry = g.byEntity.get(id), spec = g.specs.get(id);
131
+ const prepared = (0, transition_policy_1.prepareLifecycleSpec)(spec, all(g), 'unseal');
132
+ if (!prepared.ok)
133
+ throw new entity_transaction_1.EntityStoreError('spec-policy-refused', prepared.reason);
134
+ const text = (0, entity_renumber_1.replaceFrontmatter)(entry.text, prepared.candidate);
135
+ if (record)
136
+ result.unseal.push({ entityId: id, displayId: spec.id, documentLocator: entry.documentLocator, before: entry.text, after: text });
137
+ entry.text = text;
138
+ g.specs.set(id, prepared.candidate);
139
+ };
140
+ for (const id of [...before.order].reverse())
141
+ if (affected.has(id) && before.specs.get(id).status === 'approved')
142
+ withdraw(before, id, true);
143
+ const approve = after.order.filter(id => affected.has(id) && after.specs.get(id).status === 'approved');
144
+ // Imported historical seals are not the seal of a new publication. Stage these documents as drafts.
145
+ for (const id of [...approve].reverse())
146
+ withdraw(after, id, false);
147
+ result.publication = [...after.byEntity.values()].map(entry => ({ ...entry }));
148
+ for (const id of approve) {
149
+ const entry = after.byEntity.get(id), spec = after.specs.get(id);
150
+ const prepared = (0, transition_policy_1.prepareApprovedSpec)(spec, all(after), specs => parent => specs.find(s => s.id === parent) ?? null, readFile);
151
+ // A-SPEC-641: a multi-document act names the document it refuses on.
152
+ if (!prepared.ok)
153
+ throw new entity_transaction_1.EntityStoreError('spec-policy-refused', spec.id + ': ' + prepared.reason);
154
+ const text = (0, entity_renumber_1.replaceFrontmatter)(entry.text, prepared.candidate);
155
+ result.approve.push({ entityId: id, displayId: spec.id, documentLocator: entry.documentLocator, before: entry.text, after: text });
156
+ entry.text = text;
157
+ after.specs.set(id, prepared.candidate);
158
+ }
159
+ result.completed = [...after.byEntity.values()].map(entry => ({ ...entry }));
160
+ return result;
161
+ }
162
+ /** A closed transport contract. Application must additionally regenerate it from current authorized inputs. */
163
+ function validateEntityIntegrationPlan(value) {
164
+ const fail = () => { throw new entity_transaction_1.EntityStoreError('invalid-plan', 'Invalid or unsupported integration plan fields or step continuity.'); };
165
+ const object = (input, keys) => {
166
+ if (!input || typeof input !== 'object' || Array.isArray(input) || Object.keys(input).length !== keys.length || keys.some(key => !Object.prototype.hasOwnProperty.call(input, key)))
167
+ fail();
168
+ };
169
+ const uuid = (input) => typeof input === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(input);
170
+ const oid = (input) => typeof input === 'string' && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(input);
171
+ const digest = (input) => typeof input === 'string' && /^sha256:[0-9a-f]{64}$/.test(input);
172
+ const locator = (input) => typeof input === 'string' && !!input && !/[\\:\x00]/.test(input) && input.split('/').every(part => !!part && part !== '.' && part !== '..' && part.toLowerCase() !== '.git');
173
+ const mode = (input) => input === '100644' || input === '100755';
174
+ const bytes = (input) => input === null || typeof input === 'string' && Buffer.from(input, 'base64').toString('base64') === input;
175
+ object(value, ['schema', 'operationId', 'workspaceId', 'storeId', 'storeLocator', 'commits', 'destination', 'configVersion', 'checks', 'policyInputs', 'steps']);
176
+ const plan = value;
177
+ object(plan.commits, ['base', 'left', 'right']);
178
+ object(plan.destination, ['head', 'indexVersion', 'membershipVersion']);
179
+ if (plan.schema !== 'holmes-entity-integration/1' || !uuid(plan.operationId) || !uuid(plan.workspaceId) || !uuid(plan.storeId) || !locator(plan.storeLocator) ||
180
+ Object.values(plan.commits).some(commit => !oid(commit)) || !oid(plan.destination.head) || !Object.values(plan.commits).includes(plan.destination.head) ||
181
+ !digest(plan.destination.indexVersion) || !digest(plan.destination.membershipVersion) || (plan.configVersion !== null && !digest(plan.configVersion)) ||
182
+ !Array.isArray(plan.checks) || !Array.isArray(plan.policyInputs) || !Array.isArray(plan.steps))
183
+ fail();
184
+ const states = new Map();
185
+ const checks = plan.checks.map(check => {
186
+ object(check, ['locator', 'version', 'mode']);
187
+ if (!locator(check.locator) || states.has(check.locator) || (check.version === null ? check.mode !== null : !digest(check.version) || !mode(check.mode)))
188
+ fail();
189
+ states.set(check.locator, { version: check.version, mode: check.mode });
190
+ return { locator: check.locator, version: check.version, mode: check.mode };
191
+ });
192
+ if (states.has('.ax/config.json') && states.get('.ax/config.json').version !== plan.configVersion)
193
+ fail();
194
+ const policySeen = new Set();
195
+ const policyInputs = plan.policyInputs.map(input => {
196
+ object(input, ['locator', 'version', 'effectiveVersion']);
197
+ if (!locator(input.locator) || policySeen.has(input.locator) || (input.version !== null && !digest(input.version)) || (input.effectiveVersion !== null && !digest(input.effectiveVersion)))
198
+ fail();
199
+ if (states.has(input.locator) && states.get(input.locator).version !== input.version)
200
+ fail();
201
+ policySeen.add(input.locator);
202
+ return { locator: input.locator, version: input.version, effectiveVersion: input.effectiveVersion };
203
+ });
204
+ let stage = 0;
205
+ const steps = plan.steps.map(step => {
206
+ object(step, ['phase', 'change', ...(step?.entityId === undefined ? [] : ['entityId']), ...(step?.specId === undefined ? [] : ['specId'])]);
207
+ object(step.change, ['locator', 'before', 'after', 'beforeMode', 'afterMode']);
208
+ const change = step.change;
209
+ if (!['unseal', 'document', 'mapping', 'source', 'approve'].includes(step.phase) || !locator(change.locator) || !bytes(change.before) || !bytes(change.after) ||
210
+ (change.before === null ? change.beforeMode !== null : !mode(change.beforeMode)) || (change.after === null ? change.afterMode !== null : !mode(change.afterMode)) ||
211
+ (step.entityId !== undefined && !uuid(step.entityId)) || (step.specId !== undefined && (typeof step.specId !== 'string' || !/^(?:REQ|H-SPEC|A-SPEC|C-SPEC|T-SPEC|ADR)-\d{3,}(?:\.\d+)?$/.test(step.specId))))
212
+ fail();
213
+ const nextStage = step.phase === 'unseal' ? 0 : step.phase === 'approve' ? 2 : 1;
214
+ if (nextStage < stage || (change.before === change.after && change.beforeMode === change.afterMode))
215
+ fail();
216
+ stage = nextStage;
217
+ if (step.phase === 'unseal' || step.phase === 'approve') {
218
+ if (!step.entityId || !step.specId || change.before === null || change.after === null || !under(change.locator, plan.storeLocator))
219
+ fail();
220
+ }
221
+ else if (step.phase === 'document') {
222
+ if (!under(change.locator, plan.storeLocator) || !change.locator.endsWith('.md'))
223
+ fail();
224
+ }
225
+ else if (step.phase === 'mapping') {
226
+ if (!/^\.ax\/entities\/records\/[0-9a-f-]+\.json$/.test(change.locator))
227
+ fail();
228
+ }
229
+ else if (under(change.locator, '.ax') || under(change.locator, plan.storeLocator))
230
+ fail();
231
+ const prior = states.get(change.locator), beforeVersion = change.before === null ? null : (0, entity_transaction_1.entityContentVersion)(Buffer.from(change.before, 'base64'));
232
+ if (!prior || prior.version !== beforeVersion || prior.mode !== change.beforeMode)
233
+ fail();
234
+ states.set(change.locator, { version: change.after === null ? null : (0, entity_transaction_1.entityContentVersion)(Buffer.from(change.after, 'base64')), mode: change.afterMode });
235
+ return { phase: step.phase, ...(step.entityId === undefined ? {} : { entityId: step.entityId }), ...(step.specId === undefined ? {} : { specId: step.specId }),
236
+ change: { locator: change.locator, before: change.before, after: change.after, beforeMode: change.beforeMode, afterMode: change.afterMode } };
237
+ });
238
+ return { schema: plan.schema, operationId: plan.operationId, workspaceId: plan.workspaceId, storeId: plan.storeId, storeLocator: plan.storeLocator,
239
+ commits: { base: plan.commits.base, left: plan.commits.left, right: plan.commits.right }, destination: { head: plan.destination.head, indexVersion: plan.destination.indexVersion, membershipVersion: plan.destination.membershipVersion },
240
+ configVersion: plan.configVersion, checks, policyInputs, steps };
241
+ }
242
+ function entityIntegrationTarget(plan) { return 'entity-store:integrate:' + (0, entity_transaction_1.entityContentVersion)(JSON.stringify(validateEntityIntegrationPlan(plan))); }
243
+ /** Internal execution core; the public adapter supplies current authority and durable audit. */
244
+ async function applyEntityIntegration(root, inspected, authorize, audit, beforePublish) {
245
+ if (typeof authorize !== 'function' || typeof audit !== 'function')
246
+ throw new entity_transaction_1.EntityStoreError('authority-required', 'Integration requires current authority and durable audit adapters.');
247
+ root = fs.realpathSync(root);
248
+ const requested = validateEntityIntegrationPlan(inspected), completed = await retryCompletedEntityIntegration(root, requested, authorize, audit);
249
+ if (completed)
250
+ return completed;
251
+ return withValidatedEntityIntegrationPlan(root, requested, (plan, candidate) => {
252
+ const storeOwner = (0, entity_transaction_1.entityLockOwner)(root, plan.storeLocator), sourceOwner = (0, entity_renumber_transaction_1.entitySourcePublicationOwner)(root);
253
+ if (!storeOwner || !sourceOwner)
254
+ throw new entity_transaction_1.EntityStoreError('unknown-owner', 'Integration publication requires both owners.');
255
+ const pending = '.ax/state/entity-transactions/' + plan.operationId, journalFile = pending + '/journal.json';
256
+ if (integrationRead(root, '.ax/state/entity-completed/' + plan.operationId + '/journal.json'))
257
+ throw new entity_transaction_1.EntityStoreError('recovery-required', 'This operation already has a completion receipt; verify it through recovery.');
258
+ const program = JSON.stringify(plan), journal = JSON.stringify({ schema: 'holmes-entity-integration-journal/1', plan, storeOwner, sourceOwner });
259
+ const progress = integrationProgress(root, plan), targets = [...new Set(plan.steps.flatMap(step => step.specId ? [step.specId] : []))].sort();
260
+ const authority = () => { authorize(); for (const id of targets)
261
+ authorize(id); };
262
+ let persisted = false;
263
+ const verify = () => {
264
+ if (JSON.stringify(plan) !== program)
265
+ throw new entity_transaction_1.EntityStoreError('plan-changed', 'Integration program changed after regeneration.');
266
+ if (!(0, node_util_1.isDeepStrictEqual)((0, entity_transaction_1.entityLockOwner)(root, plan.storeLocator), storeOwner) || !(0, node_util_1.isDeepStrictEqual)((0, entity_renumber_transaction_1.entitySourcePublicationOwner)(root), sourceOwner))
267
+ throw new entity_transaction_1.EntityStoreError('unknown-owner', 'Integration publication ownership changed.');
268
+ if (persisted && integrationRead(root, journalFile)?.toString('utf8') !== journal)
269
+ throw new entity_transaction_1.EntityStoreError('plan-changed', 'Integration journal changed before publication.');
270
+ progress.verify();
271
+ };
272
+ authority();
273
+ verify();
274
+ (0, entity_transaction_1.publishEntityRecord)(root, journalFile, journal);
275
+ persisted = true;
276
+ for (let index = 0; index < plan.steps.length; index++) {
277
+ verify();
278
+ authority();
279
+ beforePublish?.(plan, index);
280
+ verify();
281
+ authority();
282
+ verify();
283
+ (0, entity_transaction_1.applyEntityByteChange)(root, plan.steps[index].change);
284
+ progress.advance(plan.steps[index].change);
285
+ }
286
+ verify();
287
+ authority();
288
+ (0, entity_transaction_1.publishEntityRecord)(root, pending + '/derived.json', JSON.stringify({ schema: 'holmes-integration-derived-obligations/1', operationId: plan.operationId,
289
+ workspaceId: plan.workspaceId, storeId: plan.storeId, target: entityIntegrationTarget(plan), graph: 'pending', semantic: 'pending',
290
+ changedPaths: [...new Set(plan.steps.map(step => step.change.locator))].sort() }));
291
+ audit(plan, undefined, candidate.renumbered ?? []);
292
+ verify();
293
+ authority(); // A-SPEC-645
294
+ (0, entity_transaction_1.retireEntityOperation)(root, plan.operationId, plan.storeLocator, (0, entity_transaction_1.entityContentVersion)(journal));
295
+ return { ok: true, state: 'completed', operationId: plan.operationId };
296
+ });
297
+ }
298
+ /**
299
+ * @implements A-SPEC-639 — abandon a PENDING integration a third state blocked: claim both dead
300
+ * generations exactly as recovery does, revert the operation's own publications, preserve every
301
+ * foreign byte, audit, and retire the operation as abandoned. A completed operation is refused.
302
+ */
303
+ async function abandonEntityIntegration(root, inspected, authorize, audit) {
304
+ if (typeof authorize !== 'function' || typeof audit !== 'function')
305
+ throw new entity_transaction_1.EntityStoreError('authority-required', 'Abandonment requires current authority and durable audit adapters.');
306
+ root = fs.realpathSync(root);
307
+ authorize();
308
+ const request = validateEntityIntegrationRecoveryRequest(root, inspected);
309
+ const observed = inspectEntityIntegrationRecovery(root, request.operationId);
310
+ if (observed.state !== 'pending')
311
+ throw new entity_transaction_1.EntityStoreError('unsupported-recovery', 'Only a pending operation can be abandoned; a completed operation is already retired.');
312
+ if (!(0, node_util_1.isDeepStrictEqual)(observed, request))
313
+ throw new entity_transaction_1.EntityStoreError('plan-changed', 'Inspect the current integration recovery state before authorizing its abandonment.');
314
+ const plan = observed.plan, directory = '.ax/state/entity-transactions/' + plan.operationId, journalFile = directory + '/journal.json';
315
+ const identity = (0, workspace_identity_1.workspaceIdentity)(root);
316
+ if (identity.state !== 'registered' || identity.workspaceId !== plan.workspaceId)
317
+ throw new entity_transaction_1.EntityStoreError('workspace-mismatch', 'Abandonment requires the registered destination workspace.');
318
+ const ownStore = observed.storeOwner ? entity_transaction_1.withEntityRecoveryLock : entity_transaction_1.withEntityStoreLock;
319
+ return ownStore(root, plan.storeLocator, () => {
320
+ const storeOwner = (0, entity_transaction_1.entityLockOwner)(root, plan.storeLocator);
321
+ if (!storeOwner || (observed.storeOwner && !(0, node_util_1.isDeepStrictEqual)(observed.storeOwner, storeOwner)))
322
+ throw new entity_transaction_1.EntityStoreError('unknown-owner', 'Abandonment store ownership changed before claim.');
323
+ if ((0, entity_transaction_1.entityContentVersion)(integrationRead(root, journalFile) ?? Buffer.alloc(0)) !== observed.journalVersion)
324
+ throw new entity_transaction_1.EntityStoreError('plan-changed', 'Recovery journal changed before claim.');
325
+ if (!observed.storeOwner)
326
+ (0, entity_transaction_1.publishEntityRecord)(root, directory + '/store-owners/' + storeOwner.token + '.json', JSON.stringify({ schema: 'holmes-integration-store-owner/1', operationId: plan.operationId, journalVersion: observed.journalVersion, owner: storeOwner }));
327
+ return (0, entity_renumber_transaction_1.withRecoveredSourcePublication)(root, plan.operationId, observed.sourceOwner, () => {
328
+ const sourceOwner = (0, entity_renumber_transaction_1.entitySourcePublicationOwner)(root);
329
+ if (!sourceOwner || (observed.sourceOwner && !(0, node_util_1.isDeepStrictEqual)(sourceOwner, observed.sourceOwner)))
330
+ throw new entity_transaction_1.EntityStoreError('unknown-owner', 'Abandonment requires unchanged shared source ownership.');
331
+ const current = inspectEntityIntegrationRecovery(root, plan.operationId);
332
+ if (current.journalVersion !== observed.journalVersion || current.state !== 'pending' || current.completionVersion !== null)
333
+ throw new entity_transaction_1.EntityStoreError('plan-changed', 'The pending operation changed before abandonment.');
334
+ authorize();
335
+ (0, entity_transaction_1.settleEntityTransients)(root, plan.steps.map(step => step.change)); // @implements A-SPEC-640 — after every refusal point, before the revert
336
+ const outcome = (0, entity_transaction_1.abandonPublishedSteps)(root, plan.steps.map(step => step.change));
337
+ audit(plan, outcome);
338
+ (0, entity_transaction_1.abandonEntityOperation)(root, plan.operationId, plan.storeLocator, observed.journalVersion, outcome);
339
+ return { ok: true, state: 'abandoned', operationId: plan.operationId, ...outcome };
340
+ }, { file: journalFile, version: observed.journalVersion });
341
+ });
342
+ }
343
+ async function recoverEntityIntegration(root, inspected, authorize, audit, beforePublish) {
344
+ if (typeof authorize !== 'function' || typeof audit !== 'function')
345
+ throw new entity_transaction_1.EntityStoreError('authority-required', 'Recovery requires current authority and durable audit adapters.');
346
+ root = fs.realpathSync(root);
347
+ authorize();
348
+ const request = validateEntityIntegrationRecoveryRequest(root, inspected);
349
+ const current = inspectEntityIntegrationRecovery(root, request.operationId);
350
+ if (current.state === 'completed') {
351
+ const completed = await retryCompletedEntityIntegration(root, validateEntityIntegrationPlan(request.plan), authorize, audit, current);
352
+ if (!completed)
353
+ throw new entity_transaction_1.EntityStoreError('plan-changed', 'Inspected completion is no longer present.');
354
+ return completed;
355
+ }
356
+ const observed = await validateEntityIntegrationRecoveryProgram(root, request.operationId);
357
+ if (!(0, node_util_1.isDeepStrictEqual)(observed, request))
358
+ throw new entity_transaction_1.EntityStoreError('plan-changed', 'Inspect the current integration recovery state before authorization.');
359
+ const plan = observed.plan, program = JSON.stringify(plan), directory = '.ax/state/entity-transactions/' + plan.operationId, journalFile = directory + '/journal.json';
360
+ const journalBytes = integrationRead(root, journalFile);
361
+ const journal = JSON.parse(journalBytes.toString('utf8'));
362
+ const targets = observed.completionVersion ? [] : [...new Set(plan.steps.flatMap(step => step.specId ? [step.specId] : []))].sort();
363
+ const authority = () => { authorize(); for (const id of targets)
364
+ authorize(id); };
365
+ const identity = () => {
366
+ const current = (0, workspace_identity_1.workspaceIdentity)(root);
367
+ if (current.state !== 'registered' || current.workspaceId !== plan.workspaceId)
368
+ throw new entity_transaction_1.EntityStoreError('workspace-mismatch', 'Recovery requires the registered destination workspace.');
369
+ };
370
+ identity();
371
+ authority();
372
+ const ownStore = observed.storeOwner ? entity_transaction_1.withEntityRecoveryLock : entity_transaction_1.withEntityStoreLock;
373
+ return ownStore(root, plan.storeLocator, () => {
374
+ const storeOwner = (0, entity_transaction_1.entityLockOwner)(root, plan.storeLocator);
375
+ if (!storeOwner || (observed.storeOwner && !(0, node_util_1.isDeepStrictEqual)(observed.storeOwner, storeOwner)))
376
+ throw new entity_transaction_1.EntityStoreError('unknown-owner', 'Recovery store ownership changed before claim.');
377
+ if ((0, entity_transaction_1.entityContentVersion)(integrationRead(root, journalFile) ?? Buffer.alloc(0)) !== observed.journalVersion)
378
+ throw new entity_transaction_1.EntityStoreError('plan-changed', 'Recovery journal changed before claim.');
379
+ if (!observed.storeOwner)
380
+ (0, entity_transaction_1.publishEntityRecord)(root, directory + '/store-owners/' + storeOwner.token + '.json', JSON.stringify({ schema: 'holmes-integration-store-owner/1', operationId: plan.operationId, journalVersion: observed.journalVersion, owner: storeOwner }));
381
+ return (0, entity_renumber_transaction_1.withRecoveredSourcePublication)(root, plan.operationId, observed.sourceOwner, () => {
382
+ const sourceOwner = (0, entity_renumber_transaction_1.entitySourcePublicationOwner)(root);
383
+ if (!sourceOwner || (observed.sourceOwner && !(0, node_util_1.isDeepStrictEqual)(sourceOwner, observed.sourceOwner)))
384
+ throw new entity_transaction_1.EntityStoreError('unknown-owner', 'Recovery requires unchanged shared source ownership.');
385
+ const receipts = [integrationOwnerBinding(root, directory, 'store', journal.storeOwner, storeOwner, plan.operationId, observed.journalVersion),
386
+ integrationOwnerBinding(root, directory, 'source', journal.sourceOwner, sourceOwner, plan.operationId, observed.journalVersion)].filter((receipt) => !!receipt);
387
+ const current = inspectEntityIntegrationRecovery(root, plan.operationId);
388
+ if (current.journalVersion !== observed.journalVersion || current.completionVersion !== observed.completionVersion || current.conflicts.length || !(0, node_util_1.isDeepStrictEqual)(current.observedPrefixes, observed.observedPrefixes))
389
+ throw new entity_transaction_1.EntityStoreError('plan-changed', 'Recovery prefix or completion receipt changed before publication.');
390
+ const progress = integrationProgress(root, plan), prefix = observed.observedPrefixes[0];
391
+ for (const step of plan.steps.slice(0, prefix))
392
+ progress.advance(step.change);
393
+ const verify = () => {
394
+ identity();
395
+ if (JSON.stringify(plan) !== program || !integrationRead(root, journalFile)?.equals(journalBytes))
396
+ throw new entity_transaction_1.EntityStoreError('plan-changed', 'Recovery program or journal changed.');
397
+ const completion = integrationRead(root, directory + '/completion.json');
398
+ if ((completion ? (0, entity_transaction_1.entityContentVersion)(completion) : null) !== observed.completionVersion)
399
+ throw new entity_transaction_1.EntityStoreError('plan-changed', 'Recovery completion receipt changed.');
400
+ if (!(0, node_util_1.isDeepStrictEqual)((0, entity_transaction_1.entityLockOwner)(root, plan.storeLocator), storeOwner) || !(0, node_util_1.isDeepStrictEqual)((0, entity_renumber_transaction_1.entitySourcePublicationOwner)(root), sourceOwner))
401
+ throw new entity_transaction_1.EntityStoreError('unknown-owner', 'Recovery ownership changed.');
402
+ for (const receipt of receipts)
403
+ if (!integrationRead(root, receipt.file)?.equals(receipt.bytes))
404
+ throw new entity_transaction_1.EntityStoreError('plan-changed', 'Recovery ownership receipt changed.');
405
+ progress.verify();
406
+ };
407
+ (0, entity_transaction_1.settleEntityTransients)(root, plan.steps.map(step => step.change)); // @implements A-SPEC-640 — after every refusal point, before the replay
408
+ for (let index = prefix; index < plan.steps.length; index++) {
409
+ verify();
410
+ authority();
411
+ beforePublish?.(plan, index);
412
+ verify();
413
+ authority();
414
+ verify();
415
+ (0, entity_transaction_1.applyEntityByteChange)(root, plan.steps[index].change);
416
+ progress.advance(plan.steps[index].change);
417
+ }
418
+ verify();
419
+ authority();
420
+ const derivedFile = directory + '/derived.json', derived = JSON.stringify({ schema: 'holmes-integration-derived-obligations/1', operationId: plan.operationId,
421
+ workspaceId: plan.workspaceId, storeId: plan.storeId, target: entityIntegrationTarget(plan), graph: 'pending', semantic: 'pending',
422
+ changedPaths: [...new Set(plan.steps.map(step => step.change.locator))].sort() });
423
+ const existing = integrationRead(root, derivedFile);
424
+ if (existing && existing.toString('utf8') !== derived)
425
+ throw new entity_transaction_1.EntityStoreError('plan-changed', 'Derived integration obligations changed.');
426
+ if (!existing && observed.completionVersion)
427
+ throw new entity_transaction_1.EntityStoreError('completion-conflict', 'A completed publication is missing its derived obligations.');
428
+ if (!existing)
429
+ (0, entity_transaction_1.publishEntityRecord)(root, derivedFile, derived);
430
+ audit(plan, observed.completionVersion !== null);
431
+ verify();
432
+ authority();
433
+ if (observed.completionVersion)
434
+ retireExistingIntegrationCompletion(root, observed);
435
+ else
436
+ (0, entity_transaction_1.retireEntityOperation)(root, plan.operationId, plan.storeLocator, observed.journalVersion);
437
+ return { ok: true, state: 'completed', operationId: plan.operationId };
438
+ }, { file: journalFile, version: observed.journalVersion });
439
+ });
440
+ }
441
+ /** Archive an already certified publication; its historical owner and receipt remain immutable. */
442
+ function retireExistingIntegrationCompletion(root, observed) {
443
+ const current = inspectEntityIntegrationRecovery(root, observed.operationId);
444
+ if (!observed.completionVersion || current.state !== 'pending' || current.completionVersion !== observed.completionVersion || current.journalVersion !== observed.journalVersion ||
445
+ !(0, node_util_1.isDeepStrictEqual)(current.plan, observed.plan) || current.conflicts.length || !(0, node_util_1.isDeepStrictEqual)(current.observedPrefixes, [observed.plan.steps.length]) || !current.storeOwner || !current.sourceOwner) {
446
+ throw new entity_transaction_1.EntityStoreError('completion-conflict', 'Retirement requires an unchanged, owned and verified completion receipt.');
447
+ }
448
+ const pending = (0, entity_transaction_1.entityDirectory)(root, '.ax/state/entity-transactions/' + observed.operationId);
449
+ const destination = path.join((0, entity_transaction_1.entityDirectory)(root, '.ax/state/entity-completed', true), observed.operationId);
450
+ try {
451
+ fs.lstatSync(destination);
452
+ throw new entity_transaction_1.EntityStoreError('retirement-conflict', 'Another completed operation occupies the retirement destination.');
453
+ }
454
+ catch (error) {
455
+ if (error.code !== 'ENOENT')
456
+ throw error;
457
+ }
458
+ const journal = (0, entity_transaction_1.readEntityBytes)(path.join(pending, 'journal.json')), receipt = (0, entity_transaction_1.readEntityBytes)(path.join(pending, 'completion.json'));
459
+ if (!journal || !receipt || (0, entity_transaction_1.entityContentVersion)(journal) !== observed.journalVersion || (0, entity_transaction_1.entityContentVersion)(receipt) !== observed.completionVersion ||
460
+ !(0, node_util_1.isDeepStrictEqual)((0, entity_transaction_1.entityLockOwner)(root, observed.plan.storeLocator), current.storeOwner) || !(0, node_util_1.isDeepStrictEqual)((0, entity_renumber_transaction_1.entitySourcePublicationOwner)(root), current.sourceOwner)) {
461
+ throw new entity_transaction_1.EntityStoreError('plan-changed', 'Completion or ownership changed before retirement.');
462
+ }
463
+ fs.renameSync(pending, destination);
464
+ }
465
+ /** Completed retries verify existing evidence; they never publish files or repair missing audit. */
466
+ async function retryCompletedEntityIntegration(root, plan, authorize, audit, request) {
467
+ const directory = '.ax/state/entity-completed/' + plan.operationId;
468
+ const journalBytes = integrationRead(root, directory + '/journal.json'), receiptBytes = integrationRead(root, directory + '/completion.json');
469
+ if (!journalBytes && !receiptBytes)
470
+ return undefined;
471
+ if (!journalBytes || !receiptBytes)
472
+ throw new entity_transaction_1.EntityStoreError('completion-conflict', 'Completed retry requires both journal and receipt.');
473
+ authorize();
474
+ const observed = inspectEntityIntegrationRecovery(root, plan.operationId);
475
+ if (observed.state !== 'completed' || observed.conflicts.length || !(0, node_util_1.isDeepStrictEqual)(observed.plan, plan) || (request && !(0, node_util_1.isDeepStrictEqual)(request, observed)))
476
+ throw new entity_transaction_1.EntityStoreError('completion-conflict', 'The completed operation differs from the requested plan or current files.');
477
+ const derivedBytes = integrationRead(root, directory + '/derived.json');
478
+ const derived = { schema: 'holmes-integration-derived-obligations/1', operationId: plan.operationId, workspaceId: plan.workspaceId, storeId: plan.storeId,
479
+ target: entityIntegrationTarget(plan), graph: 'pending', semantic: 'pending', changedPaths: [...new Set(plan.steps.map(step => step.change.locator))].sort() };
480
+ if (!derivedBytes || !(0, node_util_1.isDeepStrictEqual)(JSON.parse(derivedBytes.toString('utf8')), derived))
481
+ throw new entity_transaction_1.EntityStoreError('completion-conflict', 'Completed derived-state obligations are missing or changed.');
482
+ const identity = () => {
483
+ const current = (0, workspace_identity_1.workspaceIdentity)(root);
484
+ if (current.state !== 'registered' || current.workspaceId !== plan.workspaceId)
485
+ throw new entity_transaction_1.EntityStoreError('workspace-mismatch', 'Completed retry requires the registered destination workspace.');
486
+ };
487
+ identity();
488
+ const journal = JSON.parse(journalBytes.toString('utf8')), receipt = JSON.parse(receiptBytes.toString('utf8'));
489
+ if (observed.storeOwner && !(0, node_util_1.isDeepStrictEqual)(observed.storeOwner, receipt.lockOwner))
490
+ throw new entity_transaction_1.EntityStoreError('unknown-owner', 'Current store ownership belongs to another operation.');
491
+ const bindings = [];
492
+ const storeBinding = integrationOwnerBinding(root, directory, 'store', journal.storeOwner, receipt.lockOwner, plan.operationId, observed.journalVersion);
493
+ if (storeBinding)
494
+ bindings.push(storeBinding);
495
+ if (observed.sourceOwner) {
496
+ const sourceBinding = integrationOwnerBinding(root, directory, 'source', journal.sourceOwner, observed.sourceOwner, plan.operationId, observed.journalVersion);
497
+ if (sourceBinding)
498
+ bindings.push(sourceBinding);
499
+ }
500
+ const gitBasis = { head: (0, entity_git_snapshot_1.readEntityGitSnapshot)(root, 'HEAD', plan.storeLocator).commit, indexVersion: (0, entity_git_snapshot_1.readEntityGitIndex)(root).version };
501
+ const candidate = await compileEntityIntegrationCandidate(root, (0, entity_git_snapshot_1.readEntityGitInputs)(root, plan.commits, plan.storeLocator), plan.storeLocator, plan, true);
502
+ if (!(0, node_util_1.isDeepStrictEqual)(integrationPlanFromCandidate(candidate, root, plan.operationId), plan))
503
+ throw new entity_transaction_1.EntityStoreError('plan-changed', 'Completed journal differs from its original program.');
504
+ if (!(0, node_util_1.isDeepStrictEqual)(inspectEntityIntegrationRecovery(root, plan.operationId), observed))
505
+ throw new entity_transaction_1.EntityStoreError('plan-changed', 'Completion changed while regenerating the original program.');
506
+ const ownStore = observed.storeOwner ? entity_transaction_1.withEntityRecoveryLock : entity_transaction_1.withEntityStoreLock;
507
+ return ownStore(root, plan.storeLocator, () => (0, entity_renumber_transaction_1.withRecoveredSourcePublication)(root, plan.operationId, observed.sourceOwner, () => {
508
+ const storeOwner = (0, entity_transaction_1.entityLockOwner)(root, plan.storeLocator), sourceOwner = (0, entity_renumber_transaction_1.entitySourcePublicationOwner)(root), program = JSON.stringify(plan);
509
+ if (!storeOwner || (observed.storeOwner && !(0, node_util_1.isDeepStrictEqual)(storeOwner, observed.storeOwner)) || !(0, node_util_1.isDeepStrictEqual)(sourceOwner, observed.sourceOwner))
510
+ throw new entity_transaction_1.EntityStoreError('unknown-owner', 'Completed retry ownership changed.');
511
+ const progress = integrationProgress(root, plan, gitBasis);
512
+ for (const step of plan.steps)
513
+ progress.advance(step.change);
514
+ const verify = () => {
515
+ identity();
516
+ if (JSON.stringify(plan) !== program || !integrationRead(root, directory + '/journal.json')?.equals(journalBytes) || !integrationRead(root, directory + '/completion.json')?.equals(receiptBytes) ||
517
+ integrationRead(root, '.ax/state/entity-transactions/' + plan.operationId + '/journal.json'))
518
+ throw new entity_transaction_1.EntityStoreError('completion-conflict', 'Completion evidence changed.');
519
+ if (!integrationRead(root, directory + '/derived.json')?.equals(derivedBytes))
520
+ throw new entity_transaction_1.EntityStoreError('completion-conflict', 'Completed derived-state obligations changed.');
521
+ for (const binding of bindings)
522
+ if (!integrationRead(root, binding.file)?.equals(binding.bytes))
523
+ throw new entity_transaction_1.EntityStoreError('unknown-owner', 'Completed ownership binding changed.');
524
+ if (!(0, node_util_1.isDeepStrictEqual)((0, entity_transaction_1.entityLockOwner)(root, plan.storeLocator), storeOwner) || !(0, node_util_1.isDeepStrictEqual)((0, entity_renumber_transaction_1.entitySourcePublicationOwner)(root), sourceOwner))
525
+ throw new entity_transaction_1.EntityStoreError('unknown-owner', 'Completed retry ownership changed.');
526
+ progress.verify();
527
+ };
528
+ verify();
529
+ authorize();
530
+ audit(plan, true);
531
+ verify();
532
+ authorize();
533
+ return { ok: true, state: 'completed', operationId: plan.operationId };
534
+ }));
535
+ }
536
+ /** Bind replacement owners to this immutable journal; old dead-owner generations remain unchanged. */
537
+ function integrationOwnerBinding(root, directory, kind, original, current, operationId, journalVersion) {
538
+ if ((0, node_util_1.isDeepStrictEqual)(original, current))
539
+ return undefined;
540
+ const file = directory + '/' + kind + '-owners/' + current.token + '.json', bytes = integrationRead(root, file);
541
+ const expected = { schema: kind === 'store' ? 'holmes-integration-store-owner/1' : 'holmes-renumber-source-owner/1', operationId, journalVersion, owner: current };
542
+ if (!bytes || !(0, node_util_1.isDeepStrictEqual)(JSON.parse(bytes.toString('utf8')), expected))
543
+ throw new entity_transaction_1.EntityStoreError('unknown-owner', 'Recovery owner is not bound to this integration journal.');
544
+ return { file, bytes };
545
+ }
546
+ function integrationRead(root, locator) {
547
+ if (!locator || /[\\:\x00]/.test(locator) || locator.split('/').some(part => !part || part === '.' || part === '..'))
548
+ throw new entity_transaction_1.EntityStoreError('unsafe-path', 'Integration input must be a workspace-relative file.');
549
+ return (0, entity_transaction_1.readEntityBytes)(path.join((0, entity_transaction_1.entityDirectory)(root, path.posix.dirname(locator)), path.posix.basename(locator)));
550
+ }
551
+ /** Keep the original approval target while binding a lost-response retry to durable completion. */
552
+ function validateEntityIntegrationRecoveryRequest(root, inspected) {
553
+ const fail = () => { throw new entity_transaction_1.EntityStoreError('plan-changed', 'Provide an unchanged recovery request bound to the current integration journal.'); };
554
+ if (!inspected || typeof inspected !== 'object' || Array.isArray(inspected))
555
+ return fail();
556
+ const request = JSON.parse(JSON.stringify(inspected));
557
+ if (typeof request.operationId !== 'string')
558
+ return fail();
559
+ const current = inspectEntityIntegrationRecovery(root, request.operationId);
560
+ if ((0, node_util_1.isDeepStrictEqual)(request, current))
561
+ return request;
562
+ // Only completed verification tolerates obsolete prefix/ownership observations.
563
+ // A pending writer must still match the complete current inspection exactly.
564
+ if (current.state !== 'completed' || request.state !== 'pending' || current.conflicts.length ||
565
+ !Array.isArray(request.conflicts) || request.conflicts.length || !Array.isArray(request.observedPrefixes) || request.observedPrefixes.length !== 1 ||
566
+ !Number.isSafeInteger(request.observedPrefixes[0]) || request.observedPrefixes[0] < 0 || request.observedPrefixes[0] > current.plan.steps.length ||
567
+ (request.completionVersion !== null && request.completionVersion !== current.completionVersion))
568
+ return fail();
569
+ if (!(0, node_util_1.isDeepStrictEqual)({ ...request, state: current.state, observedPrefixes: current.observedPrefixes,
570
+ storeOwner: current.storeOwner, sourceOwner: current.sourceOwner, completionVersion: current.completionVersion }, current))
571
+ return fail();
572
+ const directory = '.ax/state/entity-completed/' + current.operationId;
573
+ const journalBytes = integrationRead(root, directory + '/journal.json');
574
+ if (!journalBytes || (0, entity_transaction_1.entityContentVersion)(journalBytes) !== current.journalVersion)
575
+ return fail();
576
+ const journal = JSON.parse(journalBytes.toString('utf8'));
577
+ for (const [kind, original, observed] of [['store', journal.storeOwner, request.storeOwner], ['source', journal.sourceOwner, request.sourceOwner]]) {
578
+ if (observed === null)
579
+ continue;
580
+ if (!observed || typeof observed !== 'object' || Array.isArray(observed) ||
581
+ !(0, node_util_1.isDeepStrictEqual)(Object.keys(observed).sort(), ['token', 'version']) ||
582
+ typeof observed.token !== 'string' || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(observed.token) ||
583
+ typeof observed.version !== 'string' || !/^sha256:[0-9a-f]{64}$/.test(observed.version))
584
+ return fail();
585
+ integrationOwnerBinding(root, directory, kind, original, observed, current.operationId, current.journalVersion);
586
+ }
587
+ if (!(0, node_util_1.isDeepStrictEqual)(inspectEntityIntegrationRecovery(root, request.operationId), current))
588
+ return fail();
589
+ return request;
590
+ }
591
+ /** Observation only; matching bytes do not authorize replay of a journal's program. */
592
+ function inspectEntityIntegrationRecovery(root, operationId) {
593
+ const invalid = () => { throw new entity_transaction_1.EntityStoreError('invalid-plan', 'Invalid integration recovery journal or completion receipt.'); };
594
+ const uuid = (value) => typeof value === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(value);
595
+ const exact = (value, keys) => !!value && typeof value === 'object' && !Array.isArray(value) && (0, node_util_1.isDeepStrictEqual)(Object.keys(value).sort(), [...keys].sort());
596
+ const owner = (value) => exact(value, ['token', 'version']) && uuid(value.token) && typeof value.version === 'string' && /^sha256:[0-9a-f]{64}$/.test(value.version);
597
+ if (!uuid(operationId))
598
+ invalid();
599
+ root = fs.realpathSync(root);
600
+ const pendingFile = '.ax/state/entity-transactions/' + operationId + '/journal.json';
601
+ const completedDirectory = '.ax/state/entity-completed/' + operationId;
602
+ const completedFile = completedDirectory + '/journal.json';
603
+ const pending = integrationRead(root, pendingFile), completed = integrationRead(root, completedFile);
604
+ // @implements A-SPEC-634 — a pending directory without a journal is an operation that died between
605
+ // its locks and its journal; it is recovered through entity_store, not through this inspection.
606
+ if (!pending && !completed && (0, entity_transaction_1.entityStat)(path.join((0, entity_transaction_1.entityDirectory)(root, '.ax/state/entity-transactions'), operationId)))
607
+ throw new entity_transaction_1.EntityStoreError('no-journal', 'Operation ' + operationId + ' was interrupted before its journal existed; inspect entity_store recovery-plan and recover it there.');
608
+ if ((!pending && !completed) || (pending && completed))
609
+ invalid();
610
+ const state = pending ? 'pending' : 'completed', bytes = (pending ?? completed);
611
+ const journal = JSON.parse(bytes.toString('utf8'));
612
+ if (!exact(journal, ['schema', 'plan', 'storeOwner', 'sourceOwner']))
613
+ return invalid();
614
+ if (journal.schema !== 'holmes-entity-integration-journal/1' || !owner(journal.storeOwner) || !owner(journal.sourceOwner))
615
+ invalid();
616
+ const plan = validateEntityIntegrationPlan(journal.plan), journalVersion = (0, entity_transaction_1.entityContentVersion)(bytes);
617
+ if (plan.operationId !== operationId)
618
+ invalid();
619
+ const bindings = [];
620
+ const receiptDirectory = state === 'completed' ? completedDirectory : path.posix.dirname(pendingFile);
621
+ const receiptBytes = integrationRead(root, receiptDirectory + '/completion.json');
622
+ if (state === 'completed' && !receiptBytes)
623
+ return invalid();
624
+ if (receiptBytes) {
625
+ const receipt = JSON.parse(receiptBytes.toString('utf8'));
626
+ if (!exact(receipt, ['schema', 'operationId', 'storeLocator', 'lockOwner', 'journalVersion']))
627
+ return invalid();
628
+ if (receipt.schema !== 'holmes-entity-completion/1' || receipt.operationId !== operationId || receipt.storeLocator !== plan.storeLocator ||
629
+ receipt.journalVersion !== journalVersion || !owner(receipt.lockOwner))
630
+ invalid();
631
+ const binding = integrationOwnerBinding(root, receiptDirectory, 'store', journal.storeOwner, receipt.lockOwner, operationId, journalVersion);
632
+ if (binding)
633
+ bindings.push(binding);
634
+ }
635
+ const destination = (0, entity_git_snapshot_1.readEntityGitSnapshot)(root, plan.destination.head, plan.storeLocator);
636
+ if (destination.workspaceId !== plan.workspaceId || destination.storeId !== plan.storeId)
637
+ invalid();
638
+ const storeOwner = (0, entity_transaction_1.entityLockOwner)(root, plan.storeLocator), sourceOwner = (0, entity_renumber_transaction_1.entitySourcePublicationOwner)(root);
639
+ if (state === 'pending') {
640
+ for (const [kind, original, current] of [['store', journal.storeOwner, storeOwner], ['source', journal.sourceOwner, sourceOwner]])
641
+ if (current) {
642
+ const binding = integrationOwnerBinding(root, path.posix.dirname(pendingFile), kind, original, current, operationId, journalVersion);
643
+ if (binding)
644
+ bindings.push(binding);
645
+ }
646
+ }
647
+ const asideConflicts = [];
648
+ const observe = (locator) => {
649
+ // @implements A-SPEC-640 — a lone aside beside an absent target IS the target's current state.
650
+ if (!locator || /[\\:\x00]/.test(locator) || locator.split('/').some(part => !part || part === '.' || part === '..'))
651
+ throw new entity_transaction_1.EntityStoreError('unsafe-path', 'Integration input must be a workspace-relative file.');
652
+ const observed = (0, entity_transaction_1.observeEntityTarget)(root, locator);
653
+ if (observed.asides.length > 1)
654
+ asideConflicts.push(locator + ': multiple aside files');
655
+ return { version: observed.bytes ? (0, entity_transaction_1.entityContentVersion)(observed.bytes) : null, mode: observed.mode };
656
+ };
657
+ const actual = new Map(), expected = new Map(), mismatches = new Set();
658
+ for (const check of plan.checks) {
659
+ actual.set(check.locator, observe(check.locator));
660
+ expected.set(check.locator, { version: check.version, mode: check.mode });
661
+ if (!(0, node_util_1.isDeepStrictEqual)(actual.get(check.locator), expected.get(check.locator)))
662
+ mismatches.add(check.locator);
663
+ }
664
+ const observedPrefixes = [], conflicts = [...asideConflicts];
665
+ if (!mismatches.size)
666
+ observedPrefixes.push(0);
667
+ plan.steps.forEach(({ change }, index) => {
668
+ const next = { version: change.after === null ? null : (0, entity_transaction_1.entityContentVersion)(Buffer.from(change.after, 'base64')), mode: change.afterMode };
669
+ expected.set(change.locator, next);
670
+ if ((0, node_util_1.isDeepStrictEqual)(actual.get(change.locator), next))
671
+ mismatches.delete(change.locator);
672
+ else
673
+ mismatches.add(change.locator);
674
+ if (!mismatches.size)
675
+ observedPrefixes.push(index + 1);
676
+ });
677
+ if (!observedPrefixes.length)
678
+ conflicts.push('Actual bytes or modes match no integration prefix.');
679
+ if (observedPrefixes.length > 1)
680
+ conflicts.push('Actual bytes and modes match multiple integration prefixes; progress is ambiguous.');
681
+ if (state === 'completed' && !observedPrefixes.includes(plan.steps.length))
682
+ conflicts.push('Completed receipt exists, but current files differ from its final prefix.');
683
+ if (state === 'pending' && receiptBytes && !observedPrefixes.includes(plan.steps.length))
684
+ conflicts.push('Pending completion receipt differs from the actual final prefix.');
685
+ // Pending observations also expose Git, configuration, policy and membership changes.
686
+ if (state === 'pending' && observedPrefixes.length) {
687
+ const progress = integrationProgress(root, plan);
688
+ let advanced = 0;
689
+ for (const prefix of observedPrefixes) {
690
+ while (advanced < prefix)
691
+ progress.advance(plan.steps[advanced++].change);
692
+ try {
693
+ progress.verify();
694
+ }
695
+ catch (error) {
696
+ conflicts.push('prefix ' + prefix + ': ' + (error instanceof Error ? error.message : 'Input verification failed.'));
697
+ }
698
+ }
699
+ }
700
+ for (const [locator, value] of actual)
701
+ if (!(0, node_util_1.isDeepStrictEqual)(observe(locator), value))
702
+ conflicts.push('File changed during recovery inspection: ' + locator);
703
+ if (!(0, node_util_1.isDeepStrictEqual)(integrationRead(root, pendingFile), pending) || !(0, node_util_1.isDeepStrictEqual)(integrationRead(root, completedFile), completed) ||
704
+ bindings.some(binding => !integrationRead(root, binding.file)?.equals(binding.bytes)) ||
705
+ !(0, node_util_1.isDeepStrictEqual)(integrationRead(root, receiptDirectory + '/completion.json'), receiptBytes) ||
706
+ !(0, node_util_1.isDeepStrictEqual)((0, entity_transaction_1.entityLockOwner)(root, plan.storeLocator), storeOwner) || !(0, node_util_1.isDeepStrictEqual)((0, entity_renumber_transaction_1.entitySourcePublicationOwner)(root), sourceOwner)) {
707
+ throw new entity_transaction_1.EntityStoreError('plan-changed', 'Recovery journal, receipt or ownership changed during inspection.');
708
+ }
709
+ return { schema: 'holmes-entity-integration-recovery-inspection/2', state, operationId, journalVersion, completionVersion: receiptBytes ? (0, entity_transaction_1.entityContentVersion)(receiptBytes) : null, plan, observedPrefixes, conflicts, storeOwner, sourceOwner };
710
+ }
711
+ /** Re-derive the pending journal program before any recovery writer may use it. */
712
+ async function validateEntityIntegrationRecoveryProgram(root, operationId) {
713
+ root = fs.realpathSync(root);
714
+ const observed = inspectEntityIntegrationRecovery(root, operationId);
715
+ if (observed.state !== 'pending' || observed.conflicts.length || observed.observedPrefixes.length !== 1)
716
+ throw new entity_transaction_1.EntityStoreError('recovery-required', 'A pending, unambiguous and unchanged integration prefix is required.');
717
+ const plan = observed.plan, inputs = (0, entity_git_snapshot_1.readEntityGitInputs)(root, plan.commits, plan.storeLocator);
718
+ const candidate = await compileEntityIntegrationCandidate(root, inputs, plan.storeLocator, plan);
719
+ const regenerated = integrationPlanFromCandidate(candidate, root, plan.operationId);
720
+ if (!(0, node_util_1.isDeepStrictEqual)(regenerated, plan))
721
+ throw new entity_transaction_1.EntityStoreError('plan-changed', 'Recovery program differs from its original Git and current policy inputs.');
722
+ const after = inspectEntityIntegrationRecovery(root, operationId);
723
+ if (!(0, node_util_1.isDeepStrictEqual)(after, observed))
724
+ throw new entity_transaction_1.EntityStoreError('plan-changed', 'Integration recovery state changed during program regeneration.');
725
+ return after;
726
+ }
727
+ /** Expected state advances only after a successful byte publication. No persisted prefix counter is trusted. */
728
+ function integrationProgress(root, plan, gitBasis = plan.destination) {
729
+ const destination = (0, entity_git_snapshot_1.readEntityGitSnapshot)(root, plan.destination.head, plan.storeLocator);
730
+ const registrations = destination.files.filter(file => /^\.ax\/entities\/stores\/[^/]+\.json$/.test(file.path));
731
+ const blobs = (0, entity_git_snapshot_1.readEntityGitBlobs)(root, registrations.map(file => file.oid));
732
+ const canonicalRoots = ['.ax/entities', ...registrations.map(file => JSON.parse(blobs.get(file.oid).toString()).storeLocator)];
733
+ const expected = new Map(plan.checks.map(check => [check.locator, { version: check.version, mode: check.mode }]));
734
+ const version = (bytes) => bytes ? (0, entity_transaction_1.entityContentVersion)(bytes) : null;
735
+ const verify = () => {
736
+ if ((0, entity_git_snapshot_1.readEntityGitSnapshot)(root, 'HEAD', plan.storeLocator).commit !== gitBasis.head || (0, entity_git_snapshot_1.readEntityGitIndex)(root).version !== gitBasis.indexVersion)
737
+ throw new entity_transaction_1.EntityStoreError('plan-changed', 'Git HEAD or index changed during integration.');
738
+ if (version(integrationRead(root, '.ax/config.json')) !== plan.configVersion)
739
+ throw new entity_transaction_1.EntityStoreError('plan-changed', 'Integration config changed.');
740
+ for (const [locator, state] of expected) {
741
+ // @implements A-SPEC-640 — a lone aside beside an absent target is the target's current state.
742
+ const observed = (0, entity_transaction_1.observeEntityTarget)(root, locator);
743
+ if (version(observed.bytes) !== state.version || observed.mode !== state.mode)
744
+ throw new entity_transaction_1.EntityStoreError('external-change', 'Integration input changed outside the expected prefix: ' + locator);
745
+ }
746
+ for (const input of plan.policyInputs)
747
+ if (!expected.has(input.locator) && version(integrationRead(root, input.locator)) !== input.version)
748
+ throw new entity_transaction_1.EntityStoreError('plan-changed', 'Integration policy input changed: ' + input.locator);
749
+ const files = new Map(destination.files.map(file => [file.path, file.mode]));
750
+ for (const [locator, state] of expected) {
751
+ if (state.version === null)
752
+ files.delete(locator);
753
+ else
754
+ files.set(locator, state.mode ?? '100644');
755
+ }
756
+ const membership = destinationMembership(root, [...files].map(([file, mode]) => ({ path: file, mode })), canonicalRoots, integrationRead(root, '.gitignore')?.toString('utf8') ?? '');
757
+ if (membership.conflicts.length)
758
+ throw new entity_transaction_1.EntityStoreError('external-change', membership.conflicts.join('\n'));
759
+ };
760
+ return { verify, advance: (change) => expected.set(change.locator, { version: change.after === null ? null : (0, entity_transaction_1.entityContentVersion)(Buffer.from(change.after, 'base64')), mode: change.afterMode }) };
761
+ }
762
+ /** Executor boundary: a shape-valid plan still needs regeneration and current destination registration. */
763
+ async function withValidatedEntityIntegrationPlan(root, inspected, act) {
764
+ const plan = validateEntityIntegrationPlan(inspected);
765
+ return inspectEntityIntegration(root, plan.commits, plan.storeLocator, (candidate, actualRoot) => {
766
+ const fresh = integrationPlanFromCandidate(candidate, actualRoot, plan.operationId);
767
+ if (!(0, node_util_1.isDeepStrictEqual)(fresh, plan))
768
+ throw new entity_transaction_1.EntityStoreError('plan-changed', 'Integration plan differs from current Git, file or policy inputs.');
769
+ (0, entity_store_1.assertEntityRepositoryReadable)(path.join(actualRoot, plan.storeLocator));
770
+ return act(fresh, candidate); // A-SPEC-645 — the regenerated candidate rides along (renumbers live only there)
771
+ });
772
+ }
773
+ async function prepareEntityIntegrationPlan(root, revisions, storeLocator, operationId = (0, node_crypto_1.randomUUID)()) {
774
+ return inspectEntityIntegration(root, revisions, storeLocator, (candidate, actualRoot) => integrationPlanFromCandidate(candidate, actualRoot, operationId));
775
+ }
776
+ /** Public review keeps the candidate and executable plan within the same owned inspection. */
777
+ async function reviewEntityIntegration(root, revisions, storeLocator) {
778
+ return inspectEntityIntegration(root, revisions, storeLocator, (preview, actualRoot) => ({ preview,
779
+ ...(preview.ok ? { plan: integrationPlanFromCandidate(preview, actualRoot, (0, node_crypto_1.randomUUID)()) } : {}) }));
780
+ }
781
+ function integrationPlanFromCandidate(candidate, actualRoot, operationId) {
782
+ if (!candidate.ok || !candidate.steps)
783
+ throw new entity_transaction_1.EntityStoreError('plan-conflict', [...candidate.conflicts, ...candidate.pending.map(item => item.file + ': ' + item.reason)].join('\n'));
784
+ const config = (0, entity_transaction_1.readEntityBytes)(path.join((0, entity_transaction_1.entityDirectory)(actualRoot, '.ax'), 'config.json'));
785
+ return validateEntityIntegrationPlan({ schema: 'holmes-entity-integration/1', operationId, workspaceId: candidate.workspaceId, storeId: candidate.storeId, storeLocator: candidate.storeLocator,
786
+ commits: candidate.commits, destination: { head: candidate.destinationHead, indexVersion: candidate.indexVersion, membershipVersion: candidate.membershipVersion },
787
+ configVersion: config ? (0, entity_transaction_1.entityContentVersion)(config) : null, checks: candidate.checks, policyInputs: candidate.policyInputs, steps: candidate.steps });
788
+ }
789
+ /** Internal compilation, not validation or authorization of a caller-supplied execution payload. */
790
+ function compileEntityIntegrationSteps(candidate) {
791
+ const refuse = (reason) => { throw new entity_transaction_1.EntityStoreError('invalid-plan', reason); };
792
+ if (!candidate.ok || candidate.conflicts.length || candidate.pending.length || !candidate.transitions)
793
+ refuse('Resolve candidate conflicts and transition obligations before compiling steps.');
794
+ const transitions = candidate.transitions;
795
+ const state = (bytes, mode) => {
796
+ if (bytes === null ? mode !== null : typeof bytes !== 'string' || (mode !== '100644' && mode !== '100755') || Buffer.from(bytes, 'base64').toString('base64') !== bytes)
797
+ refuse('Invalid byte/mode state.');
798
+ return { bytes, mode: mode };
799
+ };
800
+ const encoded = (text) => Buffer.from(text).toString('base64');
801
+ const checks = new Map(candidate.checks.map(check => [check.locator, check]));
802
+ if (checks.size !== candidate.checks.length)
803
+ refuse('Duplicate candidate input checks.');
804
+ const initial = new Map(), current = new Map(), final = new Map();
805
+ const bind = (locator, value) => {
806
+ if (!locator || /[\\:\x00]/.test(locator) || locator.split('/').some(part => !part || part === '.' || part === '..'))
807
+ refuse('Unsafe integration step locator.');
808
+ const check = checks.get(locator), version = value.bytes === null ? null : (0, entity_transaction_1.entityContentVersion)(Buffer.from(value.bytes, 'base64'));
809
+ if (!check || check.version !== version || check.mode !== value.mode)
810
+ refuse('Step input differs from inspected bytes/mode: ' + locator);
811
+ if (initial.has(locator) && !(0, node_util_1.isDeepStrictEqual)(initial.get(locator), value))
812
+ refuse('Conflicting initial state: ' + locator);
813
+ initial.set(locator, value);
814
+ current.set(locator, value);
815
+ };
816
+ for (const change of candidate.changes) {
817
+ if (final.has(change.locator))
818
+ refuse('Duplicate publication destination: ' + change.locator);
819
+ bind(change.locator, state(change.before, change.beforeMode));
820
+ final.set(change.locator, state(change.after, change.afterMode));
821
+ }
822
+ for (const step of transitions.unseal) {
823
+ const locator = candidate.storeLocator + '/' + step.documentLocator;
824
+ bind(locator, state(encoded(step.before), checks.get(locator)?.mode));
825
+ }
826
+ const documentIds = new Map();
827
+ for (const entry of transitions.publication) {
828
+ const locator = candidate.storeLocator + '/' + entry.documentLocator;
829
+ if (documentIds.has(locator))
830
+ refuse('Duplicate transition document path: ' + locator);
831
+ documentIds.set(locator, entry.entityId);
832
+ if (!initial.has(locator))
833
+ bind(locator, state(encoded(entry.text), checks.get(locator)?.mode));
834
+ const mode = final.get(locator)?.mode ?? initial.get(locator).mode;
835
+ final.set(locator, state(encoded(entry.text), mode));
836
+ }
837
+ for (const [oldPath, value] of initial)
838
+ if (value.bytes !== null)
839
+ for (const [newPath, next] of final) {
840
+ if (next.bytes !== null && oldPath !== newPath && (under(oldPath, newPath) || under(newPath, oldPath)))
841
+ refuse('Directory/file transition requires explicit preparation: ' + oldPath + ' -> ' + newPath);
842
+ }
843
+ const steps = [];
844
+ const add = (phase, locator, next, meta = {}, expected) => {
845
+ const prior = current.get(locator);
846
+ if (!prior || (expected !== undefined && prior.bytes !== encoded(expected)))
847
+ refuse('Discontinuous integration step: ' + locator);
848
+ if ((0, node_util_1.isDeepStrictEqual)(prior, next))
849
+ return;
850
+ steps.push({ phase, ...meta, change: { locator, before: prior.bytes, after: next.bytes, beforeMode: prior.mode, afterMode: next.mode } });
851
+ current.set(locator, next);
852
+ };
853
+ for (const step of transitions.unseal) {
854
+ const locator = candidate.storeLocator + '/' + step.documentLocator;
855
+ add('unseal', locator, state(encoded(step.after), current.get(locator).mode), { entityId: step.entityId, specId: step.displayId }, step.before);
856
+ }
857
+ // Retire old paths before publishing replacements. No directory mutation is implicit in this plan.
858
+ const publication = [...final].sort(([a, av], [b, bv]) => Number(av.bytes !== null) - Number(bv.bytes !== null) || (a < b ? -1 : a > b ? 1 : 0));
859
+ for (const [locator, next] of publication) {
860
+ const phase = under(locator, candidate.storeLocator) && locator.endsWith('.md') ? 'document' : /^\.ax\/entities\/records\/[^/]+\.json$/.test(locator) ? 'mapping' : 'source';
861
+ const entityId = documentIds.get(locator);
862
+ add(phase, locator, next, entityId ? { entityId } : {});
863
+ }
864
+ for (const step of transitions.approve) {
865
+ const locator = candidate.storeLocator + '/' + step.documentLocator;
866
+ add('approve', locator, state(encoded(step.after), current.get(locator)?.mode), { entityId: step.entityId, specId: step.displayId }, step.before);
867
+ }
868
+ for (const entry of transitions.completed) {
869
+ const locator = candidate.storeLocator + '/' + entry.documentLocator;
870
+ if (current.get(locator)?.bytes !== encoded(entry.text))
871
+ refuse('Final document differs from transition output: ' + locator);
872
+ }
873
+ return steps;
874
+ }
875
+ /** A reviewable candidate, not an authorized executable journal or a source of replacement authority. */
876
+ async function planEntityIntegrationCandidate(root, revisions, storeLocator) {
877
+ return inspectEntityIntegration(root, revisions, storeLocator, candidate => candidate);
878
+ }
879
+ async function inspectEntityIntegration(root, revisions, storeLocator, inspect) {
880
+ root = fs.realpathSync(root);
881
+ const inputs = (0, entity_git_snapshot_1.readEntityGitInputs)(root, revisions, storeLocator);
882
+ return (0, entity_transaction_1.withEntityStoreLockAsync)(root, storeLocator, () => (0, entity_renumber_transaction_1.withEntitySourceInspection)(root, async () => {
883
+ const owner = (0, entity_transaction_1.entityLockOwner)(root, storeLocator);
884
+ (0, entity_store_1.assertNoPendingEntityPublication)(path.join(root, storeLocator));
885
+ const candidate = await compileEntityIntegrationCandidate(root, inputs, storeLocator);
886
+ const result = await inspect(candidate, root);
887
+ if (!owner || !(0, node_util_1.isDeepStrictEqual)((0, entity_transaction_1.entityLockOwner)(root, storeLocator), owner))
888
+ throw new entity_transaction_1.EntityStoreError('unknown-owner', 'Destination inspection ownership changed.');
889
+ (0, entity_store_1.assertNoPendingEntityPublication)(path.join(root, storeLocator));
890
+ return result;
891
+ }));
892
+ }
893
+ async function compileEntityIntegrationCandidate(root, inputs, storeLocator, recovery, completed = false) {
894
+ if (completed && !recovery)
895
+ throw new entity_transaction_1.EntityStoreError('invalid-plan', 'Completed program regeneration requires its original plan.');
896
+ const sides = ['base', 'left', 'right'];
897
+ const commits = { base: inputs.base.commit, left: inputs.left.commit, right: inputs.right.commit };
898
+ const destination = (0, entity_git_snapshot_1.readEntityGitSnapshot)(root, recovery?.destination.head ?? 'HEAD', storeLocator);
899
+ const index = (0, entity_git_snapshot_1.readEntityGitIndex)(root);
900
+ const documents = (0, entity_integration_1.planEntityGitDocuments)(inputs);
901
+ const registrations = destination.files.filter(file => /^\.ax\/entities\/stores\/[^/]+\.json$/.test(file.path));
902
+ const controlBlobs = (0, entity_git_snapshot_1.readEntityGitBlobs)(root, [...registrations.map(file => file.oid), ...destination.files.filter(file => file.path === '.gitignore').map(file => file.oid)]);
903
+ const canonicalRoots = ['.ax/entities', ...registrations.map(file => JSON.parse(controlBlobs.get(file.oid).toString('utf8')).storeLocator)];
904
+ const ignoreEntry = destination.files.find(file => file.path === '.gitignore');
905
+ const ignoreText = ignoreEntry ? controlBlobs.get(ignoreEntry.oid).toString('utf8') : '';
906
+ // Recovery verifies present membership against the observed prefix separately. The historical
907
+ // inventory digest is retained as an envelope field, never used to derive or authorize writes.
908
+ const membership = recovery ? { version: recovery.destination.membershipVersion, conflicts: [] }
909
+ : destinationMembership(root, destination.files, canonicalRoots, ignoreText);
910
+ const result = { schema: 'holmes-entity-integration-candidate/1', stage: 'candidate', ok: false,
911
+ workspaceId: inputs.base.workspaceId, storeId: inputs.base.storeId, storeLocator, commits, destinationHead: destination.commit,
912
+ indexVersion: completed ? recovery.destination.indexVersion : index.version, membershipVersion: membership.version, changes: [], checks: [], conflicts: [...documents.conflicts, ...membership.conflicts], pending: [], approval: documents.approval, policyInputs: [] };
913
+ const conflictEvidence = [];
914
+ result.conflictEvidence = conflictEvidence;
915
+ for (const decision of (0, entity_integration_1.classifyEntityGitChanges)(inputs, 'meaning').decisions)
916
+ if (decision.outcome === 'conflict') {
917
+ const objects = {};
918
+ for (const side of sides) {
919
+ const entity = decision[side], file = entity ? storeLocator + '/' + entity.documentLocator : undefined;
920
+ const entry = file ? inputs[side].files.find(entry => entry.path === file) : undefined;
921
+ objects[side] = entry ? { commit: commits[side], oid: entry.oid, mode: entry.mode, path: entry.path } : null;
922
+ }
923
+ conflictEvidence.push({ kind: 'entity', entityId: decision.entityId, reason: decision.reason, objects });
924
+ }
925
+ // @implements A-SPEC-636 — numbering, document, reference and mirror conflicts carry the entity's
926
+ // document and record objects per side beside the planner's per-side facts.
927
+ const entityObject = (side, entityId, which) => {
928
+ const entity = inputs[side].entities.find(candidate => candidate.entityId === entityId);
929
+ const file = entity ? (which === 'document' ? storeLocator + '/' + entity.documentLocator : entity.recordLocator) : undefined;
930
+ const entry = file ? inputs[side].files.find(candidate => candidate.path === file) : undefined;
931
+ return entry ? { commit: commits[side], oid: entry.oid, mode: entry.mode, path: entry.path } : null;
932
+ };
933
+ for (const record of documents.evidence ?? []) {
934
+ const primary = record.kind === 'mirror' ? record.entityIds[0] : record.entityId;
935
+ const objects = {}, records = {};
936
+ for (const side of sides) {
937
+ objects[side] = entityObject(side, primary, 'document');
938
+ records[side] = entityObject(side, primary, 'record');
939
+ }
940
+ conflictEvidence.push({ ...record, objects, records });
941
+ }
942
+ if (!Object.values(commits).includes(destination.commit))
943
+ result.conflicts.push('Destination HEAD is not one of the inspected Git inputs.');
944
+ if (destination.workspaceId !== result.workspaceId || destination.storeId !== result.storeId)
945
+ result.conflicts.push('Destination workspace/store identity differs from the integration inputs.');
946
+ const indexed = new Map(index.entries.filter(entry => entry.stage === 0).map(entry => [entry.path, entry]));
947
+ const headFiles = new Map(destination.files.map(entry => [entry.path, entry]));
948
+ for (const entry of completed ? [] : index.entries) {
949
+ if (entry.stage !== 0)
950
+ result.conflicts.push(entry.path + ': unmerged index stage ' + entry.stage);
951
+ else {
952
+ const head = headFiles.get(entry.path);
953
+ if (!head || head.oid !== entry.oid || head.mode !== entry.mode)
954
+ result.conflicts.push(entry.path + ': index differs from destination HEAD');
955
+ }
956
+ }
957
+ if (!completed)
958
+ for (const file of destination.files)
959
+ if (!indexed.has(file.path))
960
+ result.conflicts.push(file.path + ': missing stage-zero index entry');
961
+ if (result.conflicts.length)
962
+ return result;
963
+ const sources = await (0, entity_integration_1.planEntityGitMergedSources)(root, commits, storeLocator);
964
+ result.conflicts.push(...sources.conflicts);
965
+ for (const source of sources.files)
966
+ if (!source.merge.ok)
967
+ conflictEvidence.push({ kind: 'source', file: source.file, reason: source.merge.reason, objects: source.objects });
968
+ for (const record of sources.evidence)
969
+ conflictEvidence.push(record);
970
+ result.excluded = sources.excluded.filter(entry => entry.reason.startsWith('unchanged-'));
971
+ if (!sources.ok)
972
+ return result;
973
+ const desired = new Map(), primary = new Set(), assigned = new Set();
974
+ const regularMode = (mode) => {
975
+ if (mode !== '100644' && mode !== '100755')
976
+ throw new Error('Unsupported canonical publication mode.');
977
+ return mode;
978
+ };
979
+ const put = (locator, state) => {
980
+ if (assigned.has(locator))
981
+ result.conflicts.push(locator + ': multiple publication candidates');
982
+ assigned.add(locator);
983
+ desired.set(locator, state);
984
+ };
985
+ for (const snapshot of [...sides.map(side => inputs[side]), destination])
986
+ for (const entity of snapshot.entities) {
987
+ for (const locator of [storeLocator + '/' + entity.documentLocator, entity.recordLocator]) {
988
+ primary.add(locator);
989
+ desired.set(locator, null);
990
+ }
991
+ }
992
+ const renumbered = [];
993
+ for (const document of documents.documents) {
994
+ const snapshot = inputs[document.source], original = snapshot.entities.find(entity => entity.entityId === document.entityId);
995
+ const mapping = JSON.parse(original.record);
996
+ if (typeof mapping.displayId === 'string' && mapping.displayId !== document.newId)
997
+ renumbered.push({ entityId: document.entityId, oldDisplayId: mapping.displayId, newDisplayId: document.newId }); // A-SPEC-645
998
+ const record = mapping.displayId === document.newId && mapping.documentLocator === document.newDocumentLocator ? original.record
999
+ : JSON.stringify({ ...mapping, displayId: document.newId, documentLocator: document.newDocumentLocator });
1000
+ put(storeLocator + '/' + document.newDocumentLocator, { bytes: Buffer.from(document.after), mode: regularMode(snapshot.files.find(file => file.path === storeLocator + '/' + original.documentLocator).mode) });
1001
+ put(original.recordLocator, { bytes: Buffer.from(record), mode: regularMode(snapshot.files.find(file => file.path === original.recordLocator).mode) });
1002
+ }
1003
+ for (const source of sources.files)
1004
+ put(source.file, source.merge.result ? { bytes: source.merge.result.bytes, mode: regularMode(source.merge.result.mode) } : null);
1005
+ if (renumbered.length)
1006
+ result.renumbered = renumbered.sort((a, b) => a.entityId < b.entityId ? -1 : a.entityId > b.entityId ? 1 : 0); // A-SPEC-645
1007
+ const inventories = sides.map(side => new Map(inputs[side].files.map(file => [file.path, file])));
1008
+ for (const excluded of sources.excluded) {
1009
+ if (primary.has(excluded.file) || excluded.file === '.ax/workspace.json')
1010
+ continue;
1011
+ const states = inventories.map(inventory => { const file = inventory.get(excluded.file); return file ? file.mode + ':' + file.oid : null; });
1012
+ if (states.some(state => state !== states[0]))
1013
+ result.pending.push(excluded);
1014
+ }
1015
+ // @implements A-SPEC-637 — an excluded non-regular entry is never a publication path.
1016
+ for (const excluded of result.excluded)
1017
+ for (const locator of desired.keys())
1018
+ if (under(locator, excluded.file))
1019
+ result.conflicts.push(locator + ': path passes through excluded entry ' + excluded.file);
1020
+ const inputObjects = (locator) => {
1021
+ const objects = {};
1022
+ for (const side of sides) {
1023
+ const entry = inputs[side].files.find(file => file.path === locator);
1024
+ objects[side] = entry ? { commit: commits[side], oid: entry.oid, mode: entry.mode, path: entry.path } : null;
1025
+ }
1026
+ return objects;
1027
+ };
1028
+ const current = new Map(destination.files.map(file => [file.path, file]));
1029
+ const checked = new Set([...desired.keys(), '.ax/workspace.json', '.gitignore', ...destination.files.filter(file => canonicalRoots.some(dir => under(file.path, dir))).map(file => file.path)]);
1030
+ const beforeBlobs = (0, entity_git_snapshot_1.readEntityGitBlobs)(root, [...checked].flatMap(locator => current.has(locator) ? [current.get(locator).oid] : []));
1031
+ for (const locator of [...checked].sort()) {
1032
+ const entry = current.get(locator), beforeBytes = entry ? beforeBlobs.get(entry.oid) : null;
1033
+ const before = beforeBytes?.toString('base64') ?? null, beforeMode = entry ? regularMode(entry.mode) : null;
1034
+ result.checks.push({ locator, version: beforeBytes ? (0, entity_transaction_1.entityContentVersion)(beforeBytes) : null, mode: beforeMode });
1035
+ if (!recovery) {
1036
+ try {
1037
+ (0, entity_transaction_1.assertEntityByteInput)(root, { locator, before, after: before, beforeMode, afterMode: beforeMode });
1038
+ }
1039
+ catch (error) {
1040
+ const collision = classifyDestinationCollision(root, locator, before, error);
1041
+ if (collision) {
1042
+ result.conflicts.push(collision.message);
1043
+ conflictEvidence.push({ kind: 'destination', file: locator, reason: collision.reason, existing: collision.existing, objects: inputObjects(locator) });
1044
+ }
1045
+ else
1046
+ result.conflicts.push(locator + ': ' + (error instanceof Error ? error.message : 'Unsafe destination state.'));
1047
+ }
1048
+ }
1049
+ if (!desired.has(locator))
1050
+ continue;
1051
+ const state = desired.get(locator), after = state?.bytes.toString('base64') ?? null, afterMode = state?.mode ?? null;
1052
+ if (before !== after || beforeMode !== afterMode)
1053
+ result.changes.push({ locator, before, after, beforeMode, afterMode });
1054
+ }
1055
+ const surviving = new Set([...current.keys()].filter(locator => !desired.has(locator) || desired.get(locator) !== null));
1056
+ for (const [locator, state] of desired)
1057
+ if (state)
1058
+ surviving.add(locator);
1059
+ for (const locator of surviving) {
1060
+ const parts = locator.split('/');
1061
+ for (let length = 1; length < parts.length; length++) {
1062
+ const ancestor = parts.slice(0, length).join('/');
1063
+ if (surviving.has(ancestor))
1064
+ result.conflicts.push(ancestor + ': final file-directory conflict with ' + locator);
1065
+ }
1066
+ }
1067
+ if (!completed && (0, entity_git_snapshot_1.readEntityGitSnapshot)(root, 'HEAD', storeLocator).commit !== destination.commit)
1068
+ result.conflicts.push('Destination HEAD changed during candidate planning.');
1069
+ if ((0, entity_git_snapshot_1.readEntityGitIndex)(root).version !== index.version)
1070
+ result.conflicts.push('Destination index changed during candidate planning.');
1071
+ const finalMembership = recovery ? membership : destinationMembership(root, destination.files, canonicalRoots, ignoreText);
1072
+ result.conflicts.push(...finalMembership.conflicts);
1073
+ if (finalMembership.version !== membership.version)
1074
+ result.conflicts.push('Destination membership changed during candidate planning.');
1075
+ result.ok = result.conflicts.length === 0 && result.pending.length === 0;
1076
+ if (result.ok) {
1077
+ const policyInputs = new Map();
1078
+ try {
1079
+ result.transitions = prepareEntityIntegrationTransitions(destination.entities.map(entry => ({ entityId: entry.entityId, documentLocator: entry.documentLocator, text: entry.document })), documents.documents.map(entry => ({ entityId: entry.entityId, documentLocator: entry.newDocumentLocator, text: entry.after })), file => {
1080
+ if (!file || /[\\:\x00]/.test(file) || file.split('/').some(part => !part || part === '.' || part === '..'))
1081
+ throw new entity_transaction_1.EntityStoreError('unsafe-path', 'Policy input must be a workspace-relative file.');
1082
+ let actual;
1083
+ const original = recovery ? current.get(file) : undefined;
1084
+ if (original) {
1085
+ if (original.mode !== '100644' && original.mode !== '100755')
1086
+ throw new entity_transaction_1.EntityStoreError('unsafe-path', 'Recovery policy input must be a regular Git blob.');
1087
+ actual = (0, entity_git_snapshot_1.readEntityGitBlobs)(root, [original.oid]).get(original.oid);
1088
+ }
1089
+ else if (recovery && checked.has(file)) {
1090
+ // This path was absent in the original tree but may already have been published.
1091
+ actual = undefined;
1092
+ }
1093
+ else {
1094
+ actual = (0, entity_transaction_1.readEntityBytes)(path.join((0, entity_transaction_1.entityDirectory)(root, path.posix.dirname(file)), path.posix.basename(file)));
1095
+ if (recovery) {
1096
+ // Non-Git policy files are not reconstructed from caller bytes. Their original
1097
+ // observed digest must still match the local file before current policy is evaluated.
1098
+ const bound = recovery.policyInputs.find(input => input.locator === file);
1099
+ if (!bound || bound.version !== (actual ? (0, entity_transaction_1.entityContentVersion)(actual) : null))
1100
+ throw new entity_transaction_1.EntityStoreError('plan-changed', 'Recovery local policy input changed: ' + file);
1101
+ }
1102
+ }
1103
+ const effective = desired.has(file) ? desired.get(file)?.bytes : actual;
1104
+ policyInputs.set(file, { locator: file, version: actual ? (0, entity_transaction_1.entityContentVersion)(actual) : null, effectiveVersion: effective ? (0, entity_transaction_1.entityContentVersion)(effective) : null });
1105
+ return effective?.toString('utf8') ?? null;
1106
+ });
1107
+ }
1108
+ catch (error) {
1109
+ result.conflicts.push('Integration transition policy: ' + (error instanceof Error ? error.message : 'Transition preparation failed.'));
1110
+ result.ok = false;
1111
+ }
1112
+ result.policyInputs = [...policyInputs.values()].sort((a, b) => a.locator < b.locator ? -1 : a.locator > b.locator ? 1 : 0);
1113
+ if (result.ok) {
1114
+ try {
1115
+ result.steps = compileEntityIntegrationSteps(result);
1116
+ }
1117
+ catch (error) {
1118
+ result.conflicts.push(error instanceof Error ? error.message : 'Step compilation failed.');
1119
+ result.ok = false;
1120
+ }
1121
+ }
1122
+ }
1123
+ return result;
1124
+ }
1125
+ function under(file, dir) { return file === dir || file.startsWith(dir + '/'); }
1126
+ /** Read local membership without following links; canonical roots take precedence over source exclusions. */
1127
+ /**
1128
+ * @implements A-SPEC-637 — classify a failed destination pre-check as a shape collision the plan itself
1129
+ * would run into: a file where a directory is needed, a directory where a file is needed, or a path
1130
+ * that differs from a tracked one only by case on a case-insensitive filesystem.
1131
+ */
1132
+ function classifyDestinationCollision(root, locator, before, error) {
1133
+ if (!(error instanceof entity_transaction_1.EntityStoreError))
1134
+ return undefined;
1135
+ const stat = (relative) => { try {
1136
+ return fs.lstatSync(path.join(root, relative));
1137
+ }
1138
+ catch {
1139
+ return undefined;
1140
+ } };
1141
+ if (error.code === 'unsafe-path' && /directories/.test(error.message)) {
1142
+ const parts = locator.split('/');
1143
+ for (let depth = 1; depth < parts.length; depth++) {
1144
+ const ancestor = parts.slice(0, depth).join('/'), s = stat(ancestor);
1145
+ if (s && !s.isDirectory())
1146
+ return { reason: 'file-directory-collision', existing: ancestor, message: locator + ': destination has file ' + ancestor + ' where the plan needs a directory' };
1147
+ }
1148
+ return undefined;
1149
+ }
1150
+ if (error.code === 'unsafe-path' && /regular files/.test(error.message) && stat(locator)?.isDirectory()) {
1151
+ return { reason: 'directory-file-collision', existing: locator, message: locator + ': destination has directory ' + locator + ' where the plan needs a file' };
1152
+ }
1153
+ if (error.code === 'external-change' && before === null && stat(locator)?.isFile()) {
1154
+ let real;
1155
+ try {
1156
+ real = path.relative(fs.realpathSync.native(root), fs.realpathSync.native(path.join(root, locator))).split(path.sep).join('/');
1157
+ }
1158
+ catch {
1159
+ return undefined;
1160
+ }
1161
+ if (real !== locator && real.toLowerCase() === locator.toLowerCase())
1162
+ return { reason: 'case-collision', existing: real, message: locator + ': destination path ' + real + ' differs only by case on a case-insensitive filesystem' };
1163
+ }
1164
+ return undefined;
1165
+ }
1166
+ function destinationMembership(root, files, canonicalRoots, ignoreText) {
1167
+ const tracked = new Set(files.map(file => file.path)), inventory = [], conflicts = [];
1168
+ // @implements A-SPEC-637 — tracked symlinks are members; tracked gitlink directories are boundaries.
1169
+ const symlinks = new Set(files.filter(file => file.mode === '120000').map(file => file.path)), gitlinks = new Set(files.filter(file => file.mode === '160000').map(file => file.path));
1170
+ const ignore = (0, ignore_1.parseIgnore)(ignoreText);
1171
+ for (const pattern of ignore.unsupportedPatterns())
1172
+ conflicts.push('.gitignore: unsupported destination ignore pattern: ' + pattern);
1173
+ const walk = (relative) => {
1174
+ let entries;
1175
+ try {
1176
+ entries = fs.readdirSync(path.join(root, relative), { withFileTypes: true });
1177
+ }
1178
+ catch {
1179
+ conflicts.push((relative || '.') + ': unreadable destination directory');
1180
+ return;
1181
+ }
1182
+ for (const entry of entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
1183
+ const file = relative ? relative + '/' + entry.name : entry.name;
1184
+ // Git itself resolves this administrative entry; linked worktrees use a file here.
1185
+ if (file === '.git')
1186
+ continue;
1187
+ // @implements A-SPEC-640 — the publication primitive's own transients are never destination members.
1188
+ if ((0, entity_transaction_1.isEntityTransientName)(entry.name))
1189
+ continue;
1190
+ const canonical = canonicalRoots.some(dir => under(file, dir) || under(dir, file));
1191
+ if (!canonical && (under(file, '.ax/approvals') || (0, change_source_1.isDerivedState)(file + (entry.isDirectory() ? '/' : '')) ||
1192
+ (entry.isDirectory() && cpg_scanner_1.SKIP.has(entry.name)) || ignore.isIgnored(file)))
1193
+ continue;
1194
+ if (entry.isSymbolicLink()) {
1195
+ if (symlinks.has(file)) {
1196
+ inventory.push(file);
1197
+ continue;
1198
+ }
1199
+ conflicts.push(file + ': symlinked destination membership is unsupported');
1200
+ continue;
1201
+ }
1202
+ if (entry.isDirectory()) {
1203
+ if (gitlinks.has(file))
1204
+ continue;
1205
+ if (!canonical && fs.existsSync(path.join(root, file, 'pyvenv.cfg')))
1206
+ continue;
1207
+ walk(file);
1208
+ continue;
1209
+ }
1210
+ if (!entry.isFile()) {
1211
+ conflicts.push(file + ': unsupported destination file type');
1212
+ continue;
1213
+ }
1214
+ inventory.push(file);
1215
+ if (!tracked.has(file))
1216
+ conflicts.push(file + ': untracked destination file');
1217
+ }
1218
+ };
1219
+ walk('');
1220
+ return { version: (0, entity_transaction_1.entityContentVersion)(JSON.stringify(inventory)), conflicts };
1221
+ }