openyida 2026.7.19 → 2026.7.21

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 (139) hide show
  1. package/README.md +14 -0
  2. package/bin/yida.js +107 -18
  3. package/lib/app/create-app.js +12 -1
  4. package/lib/app/create-form/args.js +13 -0
  5. package/lib/app/create-form.js +154 -67
  6. package/lib/app/create-page.js +45 -9
  7. package/lib/app/get-schema.js +65 -14
  8. package/lib/app/import-app.js +92 -41
  9. package/lib/app/publish.js +289 -556
  10. package/lib/app/schema-field-resolution.js +370 -0
  11. package/lib/app/services/app-reader.js +85 -0
  12. package/lib/app/services/app-service.js +235 -0
  13. package/lib/app/services/canvas-page-compiler.js +91 -0
  14. package/lib/app/services/canvas-page-schema-builder.js +149 -0
  15. package/lib/app/services/field-bindings.js +68 -0
  16. package/lib/app/services/form-compiler.js +1922 -0
  17. package/lib/app/services/form-mode-service.js +182 -0
  18. package/lib/app/services/form-schema-patcher.js +333 -0
  19. package/lib/app/services/form-schema-reader.js +59 -0
  20. package/lib/app/services/form-service.js +379 -0
  21. package/lib/app/services/form-validation.js +562 -0
  22. package/lib/app/services/native-page-compiler.js +92 -0
  23. package/lib/app/services/native-page-schema-builder.js +428 -0
  24. package/lib/app/services/page-resource-service.js +185 -0
  25. package/lib/app/update-app.js +18 -6
  26. package/lib/auth/token-auth.js +34 -3
  27. package/lib/auth/token-store.js +22 -4
  28. package/lib/bridge/bridge.js +37 -5
  29. package/lib/core/agent-capabilities.js +5 -3
  30. package/lib/core/command-manifest.js +40 -0
  31. package/lib/core/doctor.js +15 -4
  32. package/lib/core/env-cmd.js +1 -1
  33. package/lib/core/env.js +7 -6
  34. package/lib/core/legacy-schema-guard.js +408 -0
  35. package/lib/core/locales/en.js +8 -46
  36. package/lib/core/locales/zh.js +8 -46
  37. package/lib/core/sample.js +1 -1
  38. package/lib/core/utils.js +334 -66
  39. package/lib/core/yida-client.js +104 -3
  40. package/lib/formula/evaluate.js +8 -66
  41. package/lib/formula/field-refs.js +83 -0
  42. package/lib/process/configure-process.js +44 -1992
  43. package/lib/process/create-process.js +54 -87
  44. package/lib/process/services/process-compiler.js +2056 -0
  45. package/lib/process/services/process-reader.js +232 -0
  46. package/lib/process/services/process-resource-service.js +571 -0
  47. package/lib/process/services/process-service.js +122 -0
  48. package/lib/process/services/process-stage-checkpoint.js +246 -0
  49. package/lib/report/append.js +7 -26
  50. package/lib/report/http.js +31 -5
  51. package/lib/report/index.js +10 -2
  52. package/lib/samples/yida-canvas-custom-page/business-list.canvas.jsx +63 -14
  53. package/lib/samples/yida-canvas-custom-page/dashboard-overview.canvas.jsx +176 -32
  54. package/lib/samples/yida-canvas-custom-page/data-management.canvas.jsx +228 -14
  55. package/lib/samples/yida-canvas-custom-page/split-pane-detail.canvas.jsx +204 -8
  56. package/lib/schema/adapters/app-adapter.js +210 -0
  57. package/lib/schema/adapters/form-adapter.js +1362 -0
  58. package/lib/schema/adapters/keys.js +19 -0
  59. package/lib/schema/adapters/page-adapter.js +566 -0
  60. package/lib/schema/adapters/process-adapter.js +519 -0
  61. package/lib/schema/applier.js +1878 -0
  62. package/lib/schema/apply-store.js +1083 -0
  63. package/lib/schema/command.js +645 -0
  64. package/lib/schema/dependency-graph.js +83 -0
  65. package/lib/schema/errors.js +481 -0
  66. package/lib/schema/hash.js +9 -0
  67. package/lib/schema/manifest-limits.js +177 -0
  68. package/lib/schema/manifest-loader.js +309 -0
  69. package/lib/schema/manifest-schema-v1.json +193 -0
  70. package/lib/schema/normalize-manifest.js +281 -0
  71. package/lib/schema/page-canvas-foundation.js +447 -0
  72. package/lib/schema/page-data-source-builder.js +561 -0
  73. package/lib/schema/page-foundation.js +556 -0
  74. package/lib/schema/page-source-loader.js +314 -0
  75. package/lib/schema/planner.js +691 -0
  76. package/lib/schema/remote-dispatch-boundary.js +39 -0
  77. package/lib/schema/remote-missing.js +21 -0
  78. package/lib/schema/remote-reader.js +189 -0
  79. package/lib/schema/resource-registry.js +157 -0
  80. package/lib/schema/server-revision.js +71 -0
  81. package/lib/schema/sort.js +28 -0
  82. package/lib/schema/state-store.js +589 -0
  83. package/package.json +2 -2
  84. package/scripts/postinstall.js +8 -12
  85. package/scripts/validate-package-size.js +1 -1
  86. package/yida-skills/SKILL.md +114 -29
  87. package/yida-skills/references/schema-as-code-phase1.md +99 -0
  88. package/yida-skills/references/setup-and-env.md +59 -59
  89. package/yida-skills/references/task-retrospective.md +6 -6
  90. package/yida-skills/skills/yida-app/SKILL.md +60 -14
  91. package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +18 -7
  92. package/yida-skills/skills/yida-canvas-custom-page/references/data-bridge-guide.md +3 -3
  93. package/yida-skills/skills/yida-canvas-custom-page/references/page-generation-guide.md +7 -1
  94. package/yida-skills/skills/yida-create-app/SKILL.md +21 -3
  95. package/yida-skills/skills/yida-create-form-page/SKILL.md +19 -1
  96. package/yida-skills/skills/yida-create-page/SKILL.md +16 -3
  97. package/yida-skills/skills/yida-create-process/SKILL.md +100 -4
  98. package/yida-skills/skills/yida-custom-page/SKILL.md +19 -4
  99. package/yida-skills/skills/yida-dashboard/SKILL.md +1 -1
  100. package/yida-skills/skills/yida-dashboard/references/pitfalls.md +1 -1
  101. package/yida-skills/skills/yida-get-schema/SKILL.md +49 -5
  102. package/yida-skills/skills/yida-i18n/SKILL.md +1 -1
  103. package/yida-skills/skills/yida-integration/SKILL.md +7 -1
  104. package/yida-skills/skills/yida-login/SKILL.md +50 -99
  105. package/yida-skills/skills/yida-page-config/SKILL.md +7 -1
  106. package/yida-skills/skills/yida-page-uiux/references/app/blueprint.md +0 -1
  107. package/yida-skills/skills/yida-page-uiux/references/app/navigation-patterns.md +0 -1
  108. package/yida-skills/skills/yida-page-uiux/references/app/role-journey.md +0 -1
  109. package/yida-skills/skills/yida-page-uiux/references/dashboard/app-blueprint.md +0 -1
  110. package/yida-skills/skills/yida-page-uiux/references/dashboard/chart-contracts.md +0 -1
  111. package/yida-skills/skills/yida-page-uiux/references/dashboard/component-contracts.md +0 -1
  112. package/yida-skills/skills/yida-page-uiux/references/dashboard/layout-patterns.md +0 -1
  113. package/yida-skills/skills/yida-page-uiux/references/dashboard/theme-recipes.md +0 -1
  114. package/yida-skills/skills/yida-page-uiux/references/detail/object-narrative.md +0 -1
  115. package/yida-skills/skills/yida-page-uiux/references/detail/section-patterns.md +0 -1
  116. package/yida-skills/skills/yida-page-uiux/references/detail/timeline-and-related.md +0 -1
  117. package/yida-skills/skills/yida-page-uiux/references/landing/assets-workflow.md +0 -1
  118. package/yida-skills/skills/yida-page-uiux/references/landing/industry-playbooks.md +0 -1
  119. package/yida-skills/skills/yida-page-uiux/references/landing/research-levels.md +0 -1
  120. package/yida-skills/skills/yida-page-uiux/references/landing/section-patterns.md +0 -1
  121. package/yida-skills/skills/yida-page-uiux/references/list/drawer-detail.md +0 -1
  122. package/yida-skills/skills/yida-page-uiux/references/list/empty-loading-error.md +0 -1
  123. package/yida-skills/skills/yida-page-uiux/references/list/filter-patterns.md +0 -1
  124. package/yida-skills/skills/yida-page-uiux/references/list/table-patterns.md +0 -1
  125. package/yida-skills/skills/yida-page-uiux/references/scenes/screen.md +0 -1
  126. package/yida-skills/skills/yida-page-uiux/references/workbench/entry-patterns.md +0 -1
  127. package/yida-skills/skills/yida-page-uiux/references/workbench/portal-layouts.md +0 -1
  128. package/yida-skills/skills/yida-page-uiux/references/workbench/task-feed.md +0 -1
  129. package/yida-skills/skills/yida-page-uiux/workflow/output-decision-block.md +1 -1
  130. package/yida-skills/skills/yida-process-rule/SKILL.md +35 -1
  131. package/yida-skills/skills/yida-publish-page/SKILL.md +46 -4
  132. package/yida-skills/skills/yida-report/SKILL.md +7 -1
  133. package/yida-skills/skills-index.json +14 -46
  134. package/yida-skills/skills/large-file-write/SKILL.md +0 -93
  135. package/yida-skills/skills/large-file-write/references/write-patterns.md +0 -147
  136. package/yida-skills/skills/large-file-write/scripts/write.js +0 -157
  137. package/yida-skills/skills/sls-log-workbench/SKILL.md +0 -134
  138. package/yida-skills/skills/sls-log-workbench/references/workbench-reference.md +0 -130
  139. package/yida-skills/skills/sls-log-workbench/sls-query.js +0 -295
@@ -0,0 +1,1878 @@
1
+ 'use strict';
2
+
3
+ const { schemaError } = require('./errors');
4
+ const { createPlan, hashManagedIdentity, selectObservableResources } = require('./planner');
5
+ const { resourceId } = require('./dependency-graph');
6
+ const { isSha256 } = require('./hash');
7
+ const { readObservedResources, hashObservedManaged, isRemoteMissingError } = require('./remote-reader');
8
+ const { createDefaultRegistry, isStagedAdapter } = require('./resource-registry');
9
+ const { compareCodePoints } = require('./sort');
10
+ const {
11
+ getResourceState,
12
+ hashStable,
13
+ readState,
14
+ upsertResourceState,
15
+ writeStateAtomic,
16
+ } = require('./state-store');
17
+ const {
18
+ acquireApplyLock,
19
+ assertApplyLockHandleOwned,
20
+ assertSafeApplyPath,
21
+ createApplyJournal,
22
+ createApplyOperationId,
23
+ readApplyJournal,
24
+ removeJournalOperation,
25
+ resolveSafeApplyPaths,
26
+ updateJournalOperation,
27
+ writeApplyJournalAtomic,
28
+ } = require('./apply-store');
29
+ const { isServerRevisionConflict } = require('./server-revision');
30
+
31
+ const APPLY_KIND = 'openyida_schema_apply';
32
+ const APPLY_CONTRACT_VERSION = 1;
33
+ const FORM_POST_WRITE_READBACK_MAX_ATTEMPTS = 10;
34
+ const FORM_POST_WRITE_READBACK_DELAY_MS = 1000;
35
+ const BLOCKING_OPERATIONS = new Set(['conflict', 'unmanaged', 'orphan']);
36
+ const CREATE_IDENTITY_BOUNDARY_FAILURES = new WeakSet();
37
+ const POST_DISPATCH_LOCK_FAILURES = new WeakSet();
38
+ const MAX_SUPPRESSED_OPERATION_ERRORS = 10;
39
+
40
+ async function applySchema(input, options = {}) {
41
+ const registry = options.registry || createDefaultRegistry();
42
+ const safePaths = resolveSafeApplyPaths(input.statePath, input.workspaceRoot, {
43
+ fsImpl: options.fsImpl,
44
+ paths: options.paths,
45
+ });
46
+ input = {
47
+ ...input,
48
+ statePath: safePaths.statePath,
49
+ };
50
+ const paths = safePaths.paths;
51
+ const lock = acquireApplyLock(paths.lockPath, {
52
+ fsImpl: options.fsImpl,
53
+ workspaceRoot: input.workspaceRoot,
54
+ });
55
+ let applyResult;
56
+ let primaryError;
57
+ try {
58
+ const desiredResult = await input.loadDesired();
59
+ input = {
60
+ ...input,
61
+ manifestHash: desiredResult.manifestHash,
62
+ };
63
+ const desiredResources = desiredResult.normalized.resources.map(resource => ({
64
+ ...resource,
65
+ adapterVersion: resource.adapterVersion || registry.get(resource.resourceType).adapterVersion,
66
+ }));
67
+ assertApplyAdapters(desiredResources, registry, {
68
+ ...createAdapterContext(input, options),
69
+ workspaceRoot: input.workspaceRoot,
70
+ });
71
+ let state = readState(input.statePath, {
72
+ environment: input.environment,
73
+ fsImpl: options.fsImpl,
74
+ manifestHash: desiredResult.manifestHash,
75
+ registry,
76
+ });
77
+ let journal = readApplyJournal(paths.journalPath, {
78
+ environment: input.environment,
79
+ fsImpl: options.fsImpl,
80
+ registry,
81
+ workspaceRoot: input.workspaceRoot,
82
+ });
83
+ const reviewedJournalOperations = validateJournalBeforeObserved({
84
+ desiredResources,
85
+ input,
86
+ journal,
87
+ registry,
88
+ state,
89
+ });
90
+ const deferredStagedOperations = getIncompleteStagedOperations(
91
+ reviewedJournalOperations,
92
+ registry
93
+ );
94
+ const deferredResourceIds = new Set(deferredStagedOperations.keys());
95
+ let observed;
96
+ if (deferredResourceIds.size > 0) {
97
+ const reviewedRecovery = await readObservedForReviewedRecovery({
98
+ deferredResourceIds,
99
+ desiredResources,
100
+ input,
101
+ journal,
102
+ paths,
103
+ options,
104
+ registry,
105
+ state,
106
+ lock,
107
+ });
108
+ observed = reviewedRecovery.observed;
109
+ journal = reviewedRecovery.journal;
110
+ } else {
111
+ observed = await readInitialObserved(desiredResources, state, input, options, registry, lock);
112
+ }
113
+ let reviewedObservedIdentities = indexObservedReviewIdentities(observed.resources);
114
+ let plan = createPlan({
115
+ desiredResources,
116
+ manifestHash: desiredResult.manifestHash,
117
+ observedResources: observed.resources,
118
+ state,
119
+ }, { registry });
120
+
121
+ const resumeFromReviewedJournal = plan.planId !== input.expectedPlanId && canResumeReviewedJournal(
122
+ journal,
123
+ input.expectedPlanId,
124
+ desiredResult.manifestHash
125
+ );
126
+ if (plan.planId !== input.expectedPlanId && !resumeFromReviewedJournal) {
127
+ throw schemaError('SCHEMA_APPLY_PLAN_STALE', 'Schema apply planId is stale.');
128
+ }
129
+ assertPlanExecutable(plan, { deferredResourceIds });
130
+ const internalStagedRecoveryEvidence = await collectInternalStagedRecoveryEvidence({
131
+ deferredStagedOperations,
132
+ input,
133
+ options,
134
+ registry,
135
+ state,
136
+ lock,
137
+ });
138
+ const preparedJournal = await prepareJournalForPlan({
139
+ desiredResources,
140
+ desiredResult,
141
+ input,
142
+ journal,
143
+ options,
144
+ paths,
145
+ plan,
146
+ registry,
147
+ state,
148
+ internalStagedRecoveryEvidence,
149
+ lock,
150
+ });
151
+ journal = preparedJournal.journal;
152
+ state = preparedJournal.state;
153
+
154
+ if (resumeFromReviewedJournal || preparedJournal.recoveredRemote) {
155
+ const resumedObserved = await readInitialObserved(desiredResources, state, input, options, registry, lock);
156
+ reviewedObservedIdentities = indexObservedReviewIdentities(resumedObserved.resources);
157
+ plan = createPlan({
158
+ desiredResources,
159
+ manifestHash: desiredResult.manifestHash,
160
+ observedResources: resumedObserved.resources,
161
+ state,
162
+ }, { registry });
163
+ assertPlanExecutable(plan);
164
+ }
165
+
166
+ const resourcesById = new Map(desiredResources.map(resource => [resourceId(resource), resource]));
167
+ const counts = { create: 0, update: 0, noop: 0, stateRepair: 0 };
168
+
169
+ for (const change of plan.changes) {
170
+ if (change.operation === 'noop' && !change.stateRepair) {
171
+ counts.noop += 1;
172
+ continue;
173
+ }
174
+ const resource = resourcesById.get(`${change.resourceType}:${change.key}`);
175
+ if (!resource) {
176
+ continue;
177
+ }
178
+ if (stateAlreadyCheckpointed(resource, state)) {
179
+ counts.noop += 1;
180
+ continue;
181
+ }
182
+ const result = await applyResourceChange({
183
+ change,
184
+ input,
185
+ journal,
186
+ lock,
187
+ options,
188
+ paths,
189
+ registry,
190
+ resource,
191
+ reviewedObservedIdentities,
192
+ state,
193
+ });
194
+ journal = result.journal;
195
+ state = result.state;
196
+ if (change.stateRepair) {
197
+ counts.stateRepair += 1;
198
+ } else {
199
+ counts[change.operation] += 1;
200
+ }
201
+ }
202
+
203
+ applyResult = {
204
+ kind: APPLY_KIND,
205
+ contractVersion: APPLY_CONTRACT_VERSION,
206
+ success: true,
207
+ planId: input.expectedPlanId,
208
+ manifestHash: desiredResult.manifestHash,
209
+ stateRevision: state.revision,
210
+ counts,
211
+ };
212
+ } catch (error) {
213
+ primaryError = error;
214
+ }
215
+ let releaseError;
216
+ try {
217
+ lock.release();
218
+ } catch (error) {
219
+ releaseError = error;
220
+ }
221
+ if (primaryError) {
222
+ if (releaseError) {
223
+ attachSuppressedLockReleaseError(primaryError, releaseError);
224
+ }
225
+ throw primaryError;
226
+ }
227
+ if (releaseError) {
228
+ throw releaseError;
229
+ }
230
+ return applyResult;
231
+ }
232
+
233
+ function attachSuppressedLockReleaseError(primaryError, releaseError) {
234
+ if (!primaryError || (typeof primaryError !== 'object' && typeof primaryError !== 'function')) {
235
+ return;
236
+ }
237
+ const property = Object.prototype.hasOwnProperty.call(primaryError, 'cause')
238
+ ? 'suppressedLockReleaseError'
239
+ : 'cause';
240
+ try {
241
+ Object.defineProperty(primaryError, property, {
242
+ configurable: true,
243
+ enumerable: false,
244
+ value: releaseError,
245
+ writable: false,
246
+ });
247
+ } catch {
248
+ // Preserve the primary apply error even when the error object cannot carry diagnostics.
249
+ }
250
+ }
251
+
252
+ async function readInitialObserved(desiredResources, state, input, options, registry, lock) {
253
+ const observable = selectObservableResources(desiredResources, state, { registry });
254
+ return readObservedSubset(observable, state, input, options, registry, lock);
255
+ }
256
+
257
+ async function readObservedSubset(observable, state, input, options, registry, lock) {
258
+ const reader = options.readObservedResources || readObservedResources;
259
+ if (observable.length === 0) {
260
+ return { missingResources: [], resources: [] };
261
+ }
262
+ assertApplyLockHandleOwned(lock);
263
+ const result = await reader(observable, state, {
264
+ assertDispatchBoundary: () => assertApplyLockHandleOwned(lock),
265
+ context: {
266
+ ...createAdapterContext(input, options, state),
267
+ assertRemoteDispatchBoundary: phase => assertRemoteDispatchBoundary({ lock }, phase),
268
+ },
269
+ registry,
270
+ services: options.services,
271
+ });
272
+ assertApplyLockHandleOwned(lock);
273
+ return result;
274
+ }
275
+
276
+ async function readObservedForReviewedRecovery(context) {
277
+ const observable = selectObservableResources(context.desiredResources, context.state, {
278
+ registry: context.registry,
279
+ });
280
+ const ordinary = observable.filter(resource => !context.deferredResourceIds.has(resourceId(resource)));
281
+ const deferred = observable.filter(resource => context.deferredResourceIds.has(resourceId(resource)));
282
+ const ordinaryObserved = await readObservedSubset(
283
+ ordinary,
284
+ context.state,
285
+ context.input,
286
+ context.options,
287
+ context.registry,
288
+ context.lock
289
+ );
290
+ const preliminaryPlan = createPlan({
291
+ desiredResources: context.desiredResources,
292
+ manifestHash: context.input.manifestHash,
293
+ observedResources: ordinaryObserved.resources,
294
+ state: context.state,
295
+ }, { registry: context.registry });
296
+ assertPlanExecutable(preliminaryPlan, {
297
+ deferredResourceIds: context.deferredResourceIds,
298
+ });
299
+
300
+ const recoveredObserved = [];
301
+ const recoveredMissing = [];
302
+ let journal = context.journal;
303
+ for (const resource of deferred) {
304
+ try {
305
+ const result = await readObservedSubset(
306
+ [resource],
307
+ context.state,
308
+ context.input,
309
+ context.options,
310
+ context.registry,
311
+ context.lock
312
+ );
313
+ recoveredObserved.push(...(result.resources || []));
314
+ recoveredMissing.push(...(result.missingResources || []));
315
+ } catch (error) {
316
+ journal = recordDeferredRecoveryReadFailure(journal, resource, error, context);
317
+ // Only the exact reviewed incomplete staged operation is deferred to reconciliation.
318
+ }
319
+ }
320
+ return {
321
+ observed: {
322
+ missingResources: (ordinaryObserved.missingResources || []).concat(recoveredMissing),
323
+ resources: (ordinaryObserved.resources || []).concat(recoveredObserved),
324
+ },
325
+ journal,
326
+ };
327
+ }
328
+
329
+ function recordDeferredRecoveryReadFailure(journal, resource, error, context) {
330
+ if (!journal || !journal.operations) {
331
+ return journal;
332
+ }
333
+ const id = resourceId(resource);
334
+ const operation = journal.operations[id];
335
+ if (!operation) {
336
+ return journal;
337
+ }
338
+ const suppressedErrors = Array.isArray(operation.suppressedErrors)
339
+ ? operation.suppressedErrors.slice(-(MAX_SUPPRESSED_OPERATION_ERRORS - 1))
340
+ : [];
341
+ const nextOperation = {
342
+ ...operation,
343
+ suppressedErrors: suppressedErrors.concat(createDeferredRecoveryReadDiagnostic(resource, error)),
344
+ };
345
+ try {
346
+ return persistJournalOperation(journal, nextOperation, context);
347
+ } catch {
348
+ try {
349
+ return updateJournalOperation(journal, nextOperation, {
350
+ environment: context.input.environment,
351
+ registry: context.registry,
352
+ });
353
+ } catch {
354
+ return journal;
355
+ }
356
+ }
357
+ }
358
+
359
+ function createDeferredRecoveryReadDiagnostic(resource, error) {
360
+ const diagnostic = {
361
+ code: 'SCHEMA_DEFERRED_RECOVERY_READ_FAILED',
362
+ phase: 'deferred_recovery_observed_read',
363
+ resourceType: resource.resourceType,
364
+ key: resource.key,
365
+ };
366
+ const errorCode = safeErrorCode(error);
367
+ if (errorCode) {
368
+ diagnostic.errorCode = errorCode;
369
+ }
370
+ const adapterCode = safeAdapterCode(error);
371
+ if (adapterCode) {
372
+ diagnostic.adapterCode = adapterCode;
373
+ }
374
+ const errorName = safeErrorName(error);
375
+ if (errorName) {
376
+ diagnostic.errorName = errorName;
377
+ }
378
+ return diagnostic;
379
+ }
380
+
381
+ function safeErrorCode(error) {
382
+ const code = error && error.code;
383
+ return typeof code === 'string' && /^[A-Z0-9_:-]{1,120}$/.test(code) ? code : undefined;
384
+ }
385
+
386
+ function safeAdapterCode(error) {
387
+ const code = error && error.details && error.details.adapterCode;
388
+ return typeof code === 'string' && /^[A-Z0-9_:-]{1,120}$/.test(code) ? code : undefined;
389
+ }
390
+
391
+ function safeErrorName(error) {
392
+ const name = (error && error.name) || (error && error.constructor && error.constructor.name);
393
+ return typeof name === 'string' && /^[A-Za-z0-9_.:-]{1,80}$/.test(name) ? name : undefined;
394
+ }
395
+
396
+ function assertPlanExecutable(plan, options = {}) {
397
+ const deferredResourceIds = options.deferredResourceIds || new Set();
398
+ const blocking = plan.changes.filter(change => (
399
+ BLOCKING_OPERATIONS.has(change.operation) &&
400
+ !deferredResourceIds.has(`${change.resourceType}:${change.key}`)
401
+ ));
402
+ if (blocking.length > 0) {
403
+ throw schemaError('SCHEMA_APPLY_BLOCKED', 'Schema apply plan contains blocked changes.', {
404
+ details: {
405
+ operation: blocking[0].operation,
406
+ resourceType: blocking[0].resourceType,
407
+ key: blocking[0].key,
408
+ },
409
+ });
410
+ }
411
+ }
412
+
413
+ function validateJournalBeforeObserved(context) {
414
+ if (!context.journal) {
415
+ return new Map();
416
+ }
417
+ if (!journalMatchesReviewedInput(context.journal, context.input)) {
418
+ if (!journalIsFullyCheckpointed(context.journal, context.state)) {
419
+ throw schemaError('SCHEMA_RECONCILIATION_REQUIRED', 'A previous schema apply requires reconciliation.');
420
+ }
421
+ return new Map();
422
+ }
423
+ return validateJournalOperationsAgainstDesired(
424
+ context.journal,
425
+ context.desiredResources
426
+ );
427
+ }
428
+
429
+ function validateJournalOperationsAgainstDesired(journal, desiredResources) {
430
+ const resourcesById = new Map(desiredResources.map(resource => [resourceId(resource), resource]));
431
+ for (const id of Object.keys(journal.operations).sort(compareCodePoints)) {
432
+ const operation = journal.operations[id];
433
+ const resource = resourcesById.get(id);
434
+ if (!resource || operation.desiredHash !== desiredHash(resource)) {
435
+ throw schemaError('SCHEMA_RECONCILIATION_REQUIRED', 'A previous schema apply requires reconciliation.');
436
+ }
437
+ }
438
+ return new Map(Object.keys(journal.operations).sort(compareCodePoints).map(id => [
439
+ id,
440
+ {
441
+ operation: journal.operations[id],
442
+ resource: resourcesById.get(id),
443
+ },
444
+ ]));
445
+ }
446
+
447
+ function getIncompleteStagedOperations(reviewedJournalOperations, registry) {
448
+ const result = new Map();
449
+ for (const [id, entry] of reviewedJournalOperations.entries()) {
450
+ const adapter = registry.get(entry.resource.resourceType);
451
+ if (entry.operation.status !== 'completed' && isStagedRemoteOperation(entry.operation, adapter)) {
452
+ result.set(id, entry);
453
+ }
454
+ }
455
+ return result;
456
+ }
457
+
458
+ async function collectInternalStagedRecoveryEvidence(context) {
459
+ const evidence = new Map();
460
+ for (const [id, entry] of context.deferredStagedOperations.entries()) {
461
+ const adapter = context.registry.get(entry.resource.resourceType);
462
+ const stateResource = getResourceState(
463
+ context.state,
464
+ entry.resource.resourceType,
465
+ entry.resource.key
466
+ );
467
+ const adapterContext = {
468
+ ...createAdapterContext(context.input, context.options, context.state),
469
+ resource: entry.resource,
470
+ stateResource,
471
+ };
472
+ const decision = await collectAdapterRecoveryEvidence(
473
+ adapter,
474
+ entry.operation,
475
+ adapterContext,
476
+ context
477
+ );
478
+ evidence.set(id, decision);
479
+ }
480
+ return evidence;
481
+ }
482
+
483
+ function journalMatchesReviewedInput(journal, input) {
484
+ return !!(
485
+ journal &&
486
+ journal.planId === input.expectedPlanId &&
487
+ journal.manifestHash === input.manifestHash
488
+ );
489
+ }
490
+
491
+ async function prepareJournalForPlan(context) {
492
+ let journal = context.journal;
493
+ let state = context.state;
494
+ let recoveredRemote = false;
495
+ if (journal && (journal.planId !== context.input.expectedPlanId || journal.manifestHash !== context.desiredResult.manifestHash)) {
496
+ if (!journalIsFullyCheckpointed(journal, state)) {
497
+ throw schemaError('SCHEMA_RECONCILIATION_REQUIRED', 'A previous schema apply requires reconciliation.');
498
+ }
499
+ journal = null;
500
+ }
501
+
502
+ if (!journal) {
503
+ journal = createApplyJournal({
504
+ environment: context.input.environment,
505
+ manifestHash: context.desiredResult.manifestHash,
506
+ planId: context.plan.planId,
507
+ registry: context.registry,
508
+ });
509
+ assertApplyLockHandleOwned(context.lock);
510
+ writeApplyJournalAtomic(context.paths.journalPath, journal, {
511
+ environment: context.input.environment,
512
+ fsImpl: context.options.fsImpl,
513
+ registry: context.registry,
514
+ workspaceRoot: context.input.workspaceRoot,
515
+ });
516
+ assertApplyLockHandleOwned(context.lock);
517
+ return { journal, recoveredRemote, state };
518
+ }
519
+
520
+ const resourcesById = validateJournalOperationsAgainstDesired(journal, context.desiredResources);
521
+ for (const id of Object.keys(journal.operations).sort(compareCodePoints)) {
522
+ const operation = journal.operations[id];
523
+ const resource = resourcesById.get(id).resource;
524
+ const adapter = context.registry.get(resource.resourceType);
525
+ if (isStagedRemoteOperation(operation, adapter) && operation.status !== 'completed') {
526
+ if (!context.internalStagedRecoveryEvidence.has(id)) {
527
+ throw schemaError('SCHEMA_RECONCILIATION_REQUIRED', 'A staged schema operation requires reconciliation evidence.');
528
+ }
529
+ context.journal = journal;
530
+ const recovered = await reconcileStagedOperation(
531
+ operation,
532
+ resource,
533
+ state,
534
+ context,
535
+ context.internalStagedRecoveryEvidence.get(id)
536
+ );
537
+ journal = recovered.journal;
538
+ state = recovered.state;
539
+ recoveredRemote = true;
540
+ continue;
541
+ }
542
+ if (operation.status === 'uncertain' && isResumableCreateIdentityOperation(operation, adapter)) {
543
+ context.journal = journal;
544
+ const recovered = await resumeCreateIdentityOperation(operation, resource, state, context);
545
+ journal = recovered.journal;
546
+ state = recovered.state;
547
+ recoveredRemote = recoveredRemote || recovered.remoteWork === true;
548
+ continue;
549
+ }
550
+ if (operation.status === 'uncertain') {
551
+ throw schemaError('SCHEMA_RECONCILIATION_REQUIRED', 'A previous schema apply requires reconciliation.');
552
+ }
553
+ if (operation.status === 'completed') {
554
+ if (!stateMatchesCheckpoint(state, operation)) {
555
+ const recovered = await recoverCompletedOperation(operation, resource, state, context);
556
+ state = recovered.state;
557
+ }
558
+ continue;
559
+ }
560
+ if (operation.status === 'pending') {
561
+ context.journal = journal;
562
+ const recovered = isResumableCreateIdentityOperation(operation, adapter)
563
+ ? await resumeCreateIdentityOperation(operation, resource, state, context)
564
+ : await reconcilePendingOperation(operation, resource, state, context);
565
+ journal = recovered.journal;
566
+ state = recovered.state;
567
+ recoveredRemote = recoveredRemote || recovered.remoteWork === true;
568
+ }
569
+ }
570
+ return { journal, recoveredRemote, state };
571
+ }
572
+
573
+ function isResumableCreateIdentityOperation(operation, adapter) {
574
+ return !!(
575
+ operation &&
576
+ operation.operation === 'create' &&
577
+ operation.createIdentity !== undefined &&
578
+ adapter &&
579
+ typeof adapter.resumeCreate === 'function'
580
+ );
581
+ }
582
+
583
+ function canResumeReviewedJournal(journal, expectedPlanId, manifestHash) {
584
+ if (!journal || journal.planId !== expectedPlanId || journal.manifestHash !== manifestHash) {
585
+ return false;
586
+ }
587
+ const operations = Object.values(journal.operations || {});
588
+ return operations.length > 0;
589
+ }
590
+
591
+ async function recoverCompletedOperation(operation, resource, state, context) {
592
+ const stateResource = operation.checkpoint;
593
+ const observed = await readAndProjectResource(
594
+ resource,
595
+ stateResource,
596
+ state,
597
+ context.input,
598
+ context.options,
599
+ context.registry,
600
+ context.lock
601
+ );
602
+ const adapter = context.registry.get(resource.resourceType);
603
+ await adapter.verify(observed.projection, {
604
+ ...createAdapterContext(context.input, context.options, state),
605
+ resource,
606
+ recovery: 'completed',
607
+ operation: operation.operation,
608
+ expectedRemoteSchemaHash: stateResource.remoteSchemaHash,
609
+ stateResource,
610
+ });
611
+ return checkpointState(resource, observed.projection, state, context);
612
+ }
613
+
614
+ async function reconcilePendingOperation(operation, resource, state, context) {
615
+ const stateResource = getResourceState(state, resource.resourceType, resource.key);
616
+ if (!stateResource) {
617
+ const uncertain = { ...operation, status: 'uncertain' };
618
+ persistJournalOperation(context.journal, uncertain, context);
619
+ throw schemaError('SCHEMA_RECONCILIATION_REQUIRED', 'A pending create operation cannot be identified without bindings.');
620
+ }
621
+
622
+ let observed;
623
+ try {
624
+ observed = await readAndProjectResource(
625
+ resource,
626
+ stateResource,
627
+ state,
628
+ context.input,
629
+ context.options,
630
+ context.registry,
631
+ context.lock
632
+ );
633
+ const adapter = context.registry.get(resource.resourceType);
634
+ await adapter.verify(observed.projection, {
635
+ ...createAdapterContext(context.input, context.options, state),
636
+ operation: operation.operation,
637
+ recovery: 'pending',
638
+ resource,
639
+ stateResource,
640
+ });
641
+ } catch (error) {
642
+ const uncertain = { ...operation, status: 'uncertain' };
643
+ persistJournalOperation(context.journal, uncertain, context);
644
+ throw schemaError('SCHEMA_RECONCILIATION_REQUIRED', 'A pending schema operation could not be reconciled.');
645
+ }
646
+
647
+ const checkpoint = buildCheckpoint(resource, observed.projection);
648
+ const completed = { ...operation, status: 'completed', checkpoint };
649
+ const journal = persistJournalOperation(context.journal, completed, context);
650
+ const checkpointed = checkpointState(resource, observed.projection, state, context);
651
+ return { journal, state: checkpointed.state };
652
+ }
653
+
654
+ async function resumeCreateIdentityOperation(operation, resource, state, context) {
655
+ const adapter = context.registry.get(resource.resourceType);
656
+ if (
657
+ typeof adapter.resumeCreate !== 'function' ||
658
+ getResourceState(state, resource.resourceType, resource.key)
659
+ ) {
660
+ throw schemaError('SCHEMA_RECONCILIATION_REQUIRED', 'A create operation cannot be resumed safely.');
661
+ }
662
+ const journal = assertCreateOperationCurrent(operation, context.journal, state, context);
663
+ const adapterContext = {
664
+ ...createAdapterContext(context.input, context.options, state),
665
+ resource,
666
+ stateResource: null,
667
+ };
668
+ const prepared = await prepareAdapterOperation(adapter, {
669
+ operation: 'create',
670
+ resource,
671
+ stateResource: null,
672
+ recovery: { createIdentity: clonePlain(operation.createIdentity) },
673
+ }, adapterContext, context);
674
+
675
+ let result;
676
+ try {
677
+ assertCreateOperationCurrent(operation, journal, state, context);
678
+ assertRemoteDispatchBoundary(context, 'before');
679
+ result = await adapter.resumeCreate(
680
+ resource.desired,
681
+ clonePlain(operation.createIdentity),
682
+ {
683
+ ...adapterContext,
684
+ assertRemoteDispatchBoundary: phase => assertRemoteDispatchBoundary(context, phase),
685
+ operationId: operation.operationId,
686
+ prepared,
687
+ recovery: 'createIdentity',
688
+ }
689
+ );
690
+ assertRemoteDispatchBoundary(context, 'after');
691
+ } catch (error) {
692
+ rethrowApplyLockBoundaryFailure(error);
693
+ if (isServerRevisionConflict(error)) {
694
+ throw error;
695
+ }
696
+ const uncertain = { ...operation, status: 'uncertain' };
697
+ persistJournalOperation(journal, uncertain, context);
698
+ throw schemaError('SCHEMA_RECONCILIATION_REQUIRED', 'Schema create resume result is uncertain.');
699
+ }
700
+
701
+ const finalized = await finalizeOrdinaryResult({
702
+ adapter,
703
+ adapterContext: { ...adapterContext, recovery: 'createIdentity' },
704
+ context: { ...context, journal, resource, state },
705
+ journal,
706
+ operation: 'create',
707
+ operationRecord: operation,
708
+ prepared,
709
+ result,
710
+ });
711
+ return { ...finalized, remoteWork: finalized.usedOperationResultProjection !== true };
712
+ }
713
+
714
+ async function reconcileStagedOperation(operation, resource, state, context, decision) {
715
+ const adapter = context.registry.get(resource.resourceType);
716
+ const stateResource = getResourceState(state, resource.resourceType, resource.key);
717
+ const operationContext = { ...context, resource, state };
718
+ const adapterContext = {
719
+ ...createAdapterContext(context.input, context.options, state),
720
+ resource,
721
+ stateResource,
722
+ };
723
+ const controller = createStageController(operation, context.journal, operationContext, adapter);
724
+ controller.markPending();
725
+ const prepared = await prepareAdapterOperation(adapter, {
726
+ operation: operation.operation,
727
+ resource,
728
+ stateResource,
729
+ recovery: { stageCheckpoint: controller.getStageCheckpoint() },
730
+ }, adapterContext, operationContext);
731
+
732
+ if (decision.action === 'complete') {
733
+ controller.advanceReconciledRemoteComplete();
734
+ return finalizeStagedWithRecovery({
735
+ adapter,
736
+ adapterContext,
737
+ context: operationContext,
738
+ controller,
739
+ result: decision.result,
740
+ allowReconciliation: false,
741
+ });
742
+ }
743
+
744
+ return executeStagedWrite({
745
+ adapter,
746
+ adapterContext,
747
+ context: operationContext,
748
+ controller,
749
+ currentStateResource: stateResource,
750
+ jitObserved: undefined,
751
+ operation: operation.operation,
752
+ prepared,
753
+ allowReconciliation: false,
754
+ recovery: true,
755
+ });
756
+ }
757
+
758
+ async function executeStagedWrite(runtime) {
759
+ let result;
760
+ try {
761
+ const writeContext = {
762
+ ...runtime.adapterContext,
763
+ assertRemoteDispatchBoundary: phase => assertRemoteDispatchBoundary(runtime.context, phase),
764
+ checkpointStage: runtime.controller.checkpointAdapterStage,
765
+ prepared: runtime.prepared,
766
+ };
767
+ if (runtime.recovery) {
768
+ writeContext.recovery = {
769
+ stageCheckpoint: runtime.controller.getStageCheckpoint(),
770
+ };
771
+ }
772
+ assertRemoteDispatchBoundary(runtime.context, 'before');
773
+ result = runtime.operation === 'create'
774
+ ? await runtime.adapter.create(runtime.context.resource.desired, writeContext)
775
+ : await runtime.adapter.update(
776
+ runtime.context.resource.desired,
777
+ runtime.jitObserved && runtime.jitObserved.raw,
778
+ runtime.currentStateResource,
779
+ writeContext
780
+ );
781
+ assertRemoteDispatchBoundary(runtime.context, 'after');
782
+ } catch (error) {
783
+ rethrowApplyLockBoundaryFailure(error);
784
+ runtime.controller.markUncertain();
785
+ if (error && error.code === 'SCHEMA_APPLY_JOURNAL_INVALID') {
786
+ throw error;
787
+ }
788
+ if (!runtime.allowReconciliation) {
789
+ reconciliationRequired();
790
+ }
791
+ const decision = await collectAdapterRecoveryEvidence(
792
+ runtime.adapter,
793
+ runtime.controller.getOperation(),
794
+ runtime.adapterContext,
795
+ runtime.context
796
+ );
797
+ return continueAfterStagedReconciliation(runtime, decision);
798
+ }
799
+
800
+ if (!runtime.controller.isRemoteComplete()) {
801
+ runtime.controller.markUncertain();
802
+ if (!runtime.allowReconciliation) {
803
+ reconciliationRequired();
804
+ }
805
+ const decision = await collectAdapterRecoveryEvidence(
806
+ runtime.adapter,
807
+ runtime.controller.getOperation(),
808
+ runtime.adapterContext,
809
+ runtime.context
810
+ );
811
+ return continueAfterStagedReconciliation(runtime, decision);
812
+ }
813
+
814
+ return finalizeStagedWithRecovery({
815
+ adapter: runtime.adapter,
816
+ adapterContext: runtime.adapterContext,
817
+ context: runtime.context,
818
+ controller: runtime.controller,
819
+ result,
820
+ allowReconciliation: runtime.allowReconciliation,
821
+ });
822
+ }
823
+
824
+ async function continueAfterStagedReconciliation(runtime, decision) {
825
+ runtime.controller.markPending();
826
+ if (decision.action === 'complete') {
827
+ runtime.controller.advanceReconciledRemoteComplete();
828
+ return finalizeStagedWithRecovery({
829
+ adapter: runtime.adapter,
830
+ adapterContext: runtime.adapterContext,
831
+ context: runtime.context,
832
+ controller: runtime.controller,
833
+ result: decision.result,
834
+ allowReconciliation: false,
835
+ });
836
+ }
837
+ return executeStagedWrite({
838
+ ...runtime,
839
+ allowReconciliation: false,
840
+ recovery: true,
841
+ });
842
+ }
843
+
844
+ async function finalizeStagedWithRecovery(runtime) {
845
+ try {
846
+ return await finalizeStagedResult(runtime);
847
+ } catch (error) {
848
+ rethrowApplyLockBoundaryFailure(error);
849
+ if (runtime.controller.isCompleted()) {
850
+ throw error;
851
+ }
852
+ runtime.controller.markUncertain();
853
+ if (
854
+ error &&
855
+ (error.code === 'SCHEMA_APPLY_VERIFY_FAILED' || error.code === 'SCHEMA_APPLY_JOURNAL_INVALID')
856
+ ) {
857
+ throw error;
858
+ }
859
+ if (!runtime.allowReconciliation) {
860
+ reconciliationRequired();
861
+ }
862
+ const decision = await collectAdapterRecoveryEvidence(
863
+ runtime.adapter,
864
+ runtime.controller.getOperation(),
865
+ runtime.adapterContext,
866
+ runtime.context
867
+ );
868
+ if (decision.action !== 'complete') {
869
+ reconciliationRequired();
870
+ }
871
+ runtime.controller.markPending();
872
+ runtime.controller.advanceReconciledRemoteComplete();
873
+ try {
874
+ return await finalizeStagedResult({
875
+ ...runtime,
876
+ result: decision.result,
877
+ allowReconciliation: false,
878
+ });
879
+ } catch (retryError) {
880
+ rethrowApplyLockBoundaryFailure(retryError);
881
+ if (runtime.controller.isCompleted()) {
882
+ throw retryError;
883
+ }
884
+ runtime.controller.markUncertain();
885
+ if (
886
+ retryError &&
887
+ (retryError.code === 'SCHEMA_APPLY_VERIFY_FAILED' || retryError.code === 'SCHEMA_APPLY_JOURNAL_INVALID')
888
+ ) {
889
+ throw retryError;
890
+ }
891
+ reconciliationRequired();
892
+ }
893
+ }
894
+ }
895
+
896
+ async function finalizeStagedResult(runtime) {
897
+ const proposedBindings = runtime.adapter.buildBindings(runtime.result, {
898
+ ...runtime.adapterContext,
899
+ stageCheckpoint: runtime.controller.getStageCheckpoint(),
900
+ });
901
+ const proposedStateResource = {
902
+ adapterVersion: runtime.adapter.adapterVersion,
903
+ bindings: proposedBindings,
904
+ };
905
+ const postRead = await readAndProjectResource(
906
+ runtime.context.resource,
907
+ proposedStateResource,
908
+ runtime.context.state,
909
+ runtime.context.input,
910
+ runtime.context.options,
911
+ runtime.context.registry,
912
+ runtime.context.lock
913
+ );
914
+ await runtime.adapter.verify(postRead.projection, {
915
+ ...runtime.adapterContext,
916
+ stateResource: proposedStateResource,
917
+ });
918
+
919
+ runtime.controller.recordReadback(postRead.projection.observedManagedHash);
920
+ const checkpoint = buildCheckpoint(runtime.context.resource, postRead.projection);
921
+ runtime.controller.complete(checkpoint);
922
+ const checkpointed = checkpointState(
923
+ runtime.context.resource,
924
+ postRead.projection,
925
+ runtime.context.state,
926
+ runtime.context
927
+ );
928
+ return {
929
+ journal: runtime.controller.getJournal(),
930
+ state: checkpointed.state,
931
+ };
932
+ }
933
+
934
+ async function collectAdapterRecoveryEvidence(adapter, operation, adapterContext, applyContext) {
935
+ let decision;
936
+ try {
937
+ assertApplyLockHandleOwned(applyContext.lock);
938
+ decision = await adapter.reconcileStaged({
939
+ operation: clonePlain(operation),
940
+ resource: adapterContext.resource,
941
+ stageCheckpoint: clonePlain(operation.stageCheckpoint),
942
+ stateResource: adapterContext.stateResource,
943
+ }, {
944
+ ...adapterContext,
945
+ assertRemoteDispatchBoundary: phase => assertRemoteDispatchBoundary(applyContext, phase),
946
+ });
947
+ assertApplyLockHandleOwned(applyContext.lock);
948
+ } catch (error) {
949
+ rethrowApplyLockBoundaryFailure(error);
950
+ reconciliationRequired();
951
+ }
952
+ if (!decision || typeof decision !== 'object' || Array.isArray(decision)) {
953
+ reconciliationRequired();
954
+ }
955
+ const keys = Object.keys(decision).sort(compareCodePoints);
956
+ if (decision.action === 'resume' && keys.join(',') === 'action') {
957
+ return decision;
958
+ }
959
+ if (
960
+ decision.action === 'complete' &&
961
+ keys.join(',') === 'action,result' &&
962
+ Object.prototype.hasOwnProperty.call(decision, 'result')
963
+ ) {
964
+ return decision;
965
+ }
966
+ reconciliationRequired();
967
+ }
968
+
969
+ function createStageController(initialOperation, initialJournal, context, adapter) {
970
+ let operation = clonePlain(initialOperation);
971
+ let journal = initialJournal;
972
+
973
+ function persist(nextOperation) {
974
+ const nextJournal = persistJournalOperation(journal, nextOperation, context);
975
+ operation = nextOperation;
976
+ journal = nextJournal;
977
+ }
978
+
979
+ function advance(event, input = {}) {
980
+ const stageCheckpoint = adapter.advanceStagedCheckpoint(
981
+ clonePlain(operation.stageCheckpoint),
982
+ event,
983
+ {
984
+ ...input,
985
+ desiredManagedHash: operation.desiredHash,
986
+ }
987
+ );
988
+ adapter.validateStageCheckpoint(stageCheckpoint, {
989
+ desiredManagedHash: operation.desiredHash,
990
+ });
991
+ if (hashStable(stageCheckpoint) === hashStable(operation.stageCheckpoint)) {
992
+ return clonePlain(stageCheckpoint);
993
+ }
994
+ persist({ ...operation, stageCheckpoint });
995
+ return clonePlain(stageCheckpoint);
996
+ }
997
+
998
+ return {
999
+ checkpointAdapterStage(stage, partial = {}) {
1000
+ return advance('adapterCheckpoint', { partial, stage });
1001
+ },
1002
+ advanceReconciledRemoteComplete() {
1003
+ return advance('reconciledRemoteComplete');
1004
+ },
1005
+ complete(checkpoint) {
1006
+ const stageCheckpoint = adapter.advanceStagedCheckpoint(
1007
+ clonePlain(operation.stageCheckpoint),
1008
+ 'completed',
1009
+ { desiredManagedHash: operation.desiredHash }
1010
+ );
1011
+ adapter.validateStageCheckpoint(stageCheckpoint, {
1012
+ desiredManagedHash: operation.desiredHash,
1013
+ operation: operation.operation,
1014
+ operationStatus: 'completed',
1015
+ });
1016
+ persist({
1017
+ ...operation,
1018
+ status: 'completed',
1019
+ checkpoint,
1020
+ stageCheckpoint,
1021
+ });
1022
+ },
1023
+ getJournal() {
1024
+ return journal;
1025
+ },
1026
+ getOperation() {
1027
+ return clonePlain(operation);
1028
+ },
1029
+ getStageCheckpoint() {
1030
+ return clonePlain(operation.stageCheckpoint);
1031
+ },
1032
+ isCompleted() {
1033
+ return operation.status === 'completed';
1034
+ },
1035
+ isRemoteComplete() {
1036
+ return adapter.isStagedCheckpointRemoteComplete(
1037
+ clonePlain(operation.stageCheckpoint),
1038
+ { desiredManagedHash: operation.desiredHash }
1039
+ );
1040
+ },
1041
+ markPending() {
1042
+ persist({ ...operation, status: 'pending' });
1043
+ },
1044
+ markUncertain() {
1045
+ persist({ ...operation, status: 'uncertain' });
1046
+ },
1047
+ recordReadback(observedManagedHash) {
1048
+ return advance('readbackVerified', { observedManagedHash });
1049
+ },
1050
+ };
1051
+ }
1052
+
1053
+ function createCreateIdentityController(initialOperation, initialJournal, runtime) {
1054
+ let operation = clonePlain(initialOperation);
1055
+ let journal = initialJournal;
1056
+ let open = true;
1057
+
1058
+ function checkpoint(result) {
1059
+ if (
1060
+ !open ||
1061
+ operation.operation !== 'create' ||
1062
+ operation.status !== 'pending' ||
1063
+ operation.createIdentity !== undefined
1064
+ ) {
1065
+ throw schemaError('SCHEMA_APPLY_JOURNAL_INVALID', 'Schema create identity checkpoint is no longer available.');
1066
+ }
1067
+ let diskJournal;
1068
+ try {
1069
+ diskJournal = assertCreateOperationCurrent(
1070
+ operation,
1071
+ journal,
1072
+ runtime.context.state,
1073
+ runtime.context
1074
+ );
1075
+ } catch (error) {
1076
+ markCreateIdentityBoundaryFailure(error);
1077
+ throw error;
1078
+ }
1079
+ const bindings = runtime.adapter.buildBindings(result, {
1080
+ ...runtime.adapterContext,
1081
+ identityCheckpoint: true,
1082
+ });
1083
+ const nextOperation = {
1084
+ ...operation,
1085
+ createIdentity: { bindings: clonePlain(bindings) },
1086
+ };
1087
+ try {
1088
+ journal = persistJournalOperation(diskJournal, nextOperation, runtime.context);
1089
+ } catch (error) {
1090
+ markCreateIdentityBoundaryFailure(error);
1091
+ throw error;
1092
+ }
1093
+ operation = nextOperation;
1094
+ return clonePlain(operation.createIdentity);
1095
+ }
1096
+
1097
+ return {
1098
+ checkpoint,
1099
+ close() {
1100
+ open = false;
1101
+ },
1102
+ getJournal() {
1103
+ return journal;
1104
+ },
1105
+ getOperation() {
1106
+ return clonePlain(operation);
1107
+ },
1108
+ hasCheckpoint() {
1109
+ return operation.createIdentity !== undefined;
1110
+ },
1111
+ };
1112
+ }
1113
+
1114
+ function assertCreateOperationCurrent(operation, journal, state, context) {
1115
+ assertApplyLockHandleOwned(context.lock);
1116
+ if (
1117
+ !journal ||
1118
+ journal.planId !== context.input.expectedPlanId ||
1119
+ journal.manifestHash !== context.input.manifestHash ||
1120
+ operation.stateRevision !== state.revision
1121
+ ) {
1122
+ throw schemaError('SCHEMA_APPLY_JOURNAL_INVALID', 'Schema create checkpoint context is stale.');
1123
+ }
1124
+ const diskState = readState(context.input.statePath, {
1125
+ environment: context.input.environment,
1126
+ fsImpl: context.options.fsImpl,
1127
+ manifestHash: context.input.manifestHash,
1128
+ registry: context.registry,
1129
+ });
1130
+ if (diskState.revision !== state.revision) {
1131
+ throw schemaError('SCHEMA_APPLY_JOURNAL_INVALID', 'Schema create checkpoint State revision is stale.');
1132
+ }
1133
+ const diskJournal = readApplyJournal(context.paths.journalPath, {
1134
+ environment: context.input.environment,
1135
+ fsImpl: context.options.fsImpl,
1136
+ registry: context.registry,
1137
+ workspaceRoot: context.input.workspaceRoot,
1138
+ });
1139
+ const id = resourceId(operation);
1140
+ const diskOperation = diskJournal && diskJournal.operations && diskJournal.operations[id];
1141
+ if (
1142
+ !diskJournal ||
1143
+ diskJournal.planId !== journal.planId ||
1144
+ diskJournal.manifestHash !== journal.manifestHash ||
1145
+ !diskOperation ||
1146
+ hashStable(diskOperation) !== hashStable(operation)
1147
+ ) {
1148
+ throw schemaError('SCHEMA_APPLY_JOURNAL_INVALID', 'Schema create checkpoint operation is stale.');
1149
+ }
1150
+ assertApplyLockHandleOwned(context.lock);
1151
+ return diskJournal;
1152
+ }
1153
+
1154
+ function isStagedRemoteOperation(operation, adapter) {
1155
+ return isStagedAdapter(adapter) && (operation.operation === 'create' || operation.operation === 'update');
1156
+ }
1157
+
1158
+ function reconciliationRequired() {
1159
+ throw schemaError('SCHEMA_RECONCILIATION_REQUIRED', 'Schema staged apply requires reconciliation.');
1160
+ }
1161
+
1162
+ function assertRemoteDispatchBoundary(context, phase) {
1163
+ try {
1164
+ assertApplyLockHandleOwned(context.lock);
1165
+ } catch (error) {
1166
+ if (phase === 'after' && error && (typeof error === 'object' || typeof error === 'function')) {
1167
+ POST_DISPATCH_LOCK_FAILURES.add(error);
1168
+ }
1169
+ throw error;
1170
+ }
1171
+ }
1172
+
1173
+ function rethrowApplyLockBoundaryFailure(error) {
1174
+ if (!error || error.code !== 'SCHEMA_APPLY_LOCK_LOST') {
1175
+ return;
1176
+ }
1177
+ if (POST_DISPATCH_LOCK_FAILURES.has(error)) {
1178
+ throw schemaError('SCHEMA_RECONCILIATION_REQUIRED', 'Schema remote write may have completed after apply lock loss.');
1179
+ }
1180
+ throw error;
1181
+ }
1182
+
1183
+ async function prepareAdapterOperation(adapter, input, adapterContext, applyContext) {
1184
+ if (typeof adapter.prepareOperation !== 'function') {
1185
+ return undefined;
1186
+ }
1187
+ assertApplyLockHandleOwned(applyContext.lock);
1188
+ const prepared = await adapter.prepareOperation(input, {
1189
+ ...adapterContext,
1190
+ assertRemoteDispatchBoundary: phase => assertRemoteDispatchBoundary(applyContext, phase),
1191
+ });
1192
+ assertApplyLockHandleOwned(applyContext.lock);
1193
+ return prepared;
1194
+ }
1195
+
1196
+ async function applyResourceChange(context) {
1197
+ const adapter = context.registry.get(context.resource.resourceType);
1198
+ assertApplyAdapter(adapter);
1199
+ const currentStateResource = getResourceState(context.state, context.resource.resourceType, context.resource.key);
1200
+ const operation = context.change.stateRepair ? 'stateRepair' : context.change.operation;
1201
+ let jitObserved;
1202
+
1203
+ if (operation === 'create') {
1204
+ if (currentStateResource) {
1205
+ throw schemaError('SCHEMA_APPLY_JIT_CONFLICT', 'Create candidate received a state binding before execution.', {
1206
+ details: {
1207
+ operation,
1208
+ resourceType: context.resource.resourceType,
1209
+ key: context.resource.key,
1210
+ },
1211
+ });
1212
+ }
1213
+ } else {
1214
+ jitObserved = await readAndProjectResource(
1215
+ context.resource,
1216
+ currentStateResource,
1217
+ context.state,
1218
+ context.input,
1219
+ context.options,
1220
+ context.registry,
1221
+ context.lock
1222
+ );
1223
+ assertJitClassification(
1224
+ context.resource,
1225
+ currentStateResource,
1226
+ jitObserved.projection,
1227
+ context.change,
1228
+ context.registry,
1229
+ context.reviewedObservedIdentities
1230
+ );
1231
+ }
1232
+
1233
+ const adapterContext = {
1234
+ ...createAdapterContext(context.input, context.options, context.state),
1235
+ resource: context.resource,
1236
+ stateResource: currentStateResource,
1237
+ };
1238
+ const prepared = await prepareAdapterOperation(adapter, {
1239
+ operation,
1240
+ resource: context.resource,
1241
+ stateResource: currentStateResource,
1242
+ observed: jitObserved && jitObserved.raw,
1243
+ }, adapterContext, context);
1244
+
1245
+ if (operation === 'stateRepair') {
1246
+ const checkpoint = buildCheckpoint(context.resource, jitObserved.projection);
1247
+ const operationRecord = createOperationRecord(
1248
+ context.resource,
1249
+ operation,
1250
+ 'completed',
1251
+ context.input.expectedPlanId,
1252
+ checkpoint,
1253
+ context.state.revision
1254
+ );
1255
+ const journal = persistJournalOperation(context.journal, operationRecord, context);
1256
+ const checkpointed = checkpointState(
1257
+ context.resource,
1258
+ jitObserved.projection,
1259
+ context.state,
1260
+ context
1261
+ );
1262
+ return { journal, state: checkpointed.state };
1263
+ }
1264
+
1265
+ let operationRecord = createOperationRecord(
1266
+ context.resource,
1267
+ operation,
1268
+ 'pending',
1269
+ context.input.expectedPlanId,
1270
+ undefined,
1271
+ context.state.revision
1272
+ );
1273
+ let journal = persistJournalOperation(context.journal, operationRecord, context);
1274
+ if (isStagedRemoteOperation(operationRecord, adapter)) {
1275
+ const controller = createStageController(operationRecord, journal, context, adapter);
1276
+ return executeStagedWrite({
1277
+ adapter,
1278
+ adapterContext,
1279
+ context,
1280
+ controller,
1281
+ currentStateResource,
1282
+ jitObserved,
1283
+ operation,
1284
+ prepared,
1285
+ allowReconciliation: true,
1286
+ recovery: false,
1287
+ });
1288
+ }
1289
+ let result;
1290
+ const identityController = operation === 'create' && typeof adapter.resumeCreate === 'function'
1291
+ ? createCreateIdentityController(operationRecord, journal, {
1292
+ adapter,
1293
+ adapterContext,
1294
+ context,
1295
+ })
1296
+ : null;
1297
+ try {
1298
+ const writeContext = {
1299
+ ...adapterContext,
1300
+ assertRemoteDispatchBoundary: phase => assertRemoteDispatchBoundary(context, phase),
1301
+ operationId: operationRecord.operationId,
1302
+ prepared,
1303
+ };
1304
+ if (identityController) {
1305
+ writeContext.checkpointCreateIdentity = identityController.checkpoint;
1306
+ }
1307
+ assertRemoteDispatchBoundary(context, 'before');
1308
+ result = operation === 'create'
1309
+ ? await adapter.create(context.resource.desired, writeContext)
1310
+ : await adapter.update(context.resource.desired, jitObserved.raw, currentStateResource, writeContext);
1311
+ assertRemoteDispatchBoundary(context, 'after');
1312
+ if (identityController && !identityController.hasCheckpoint()) {
1313
+ throw schemaError('SCHEMA_APPLY_JOURNAL_INVALID', 'Resumable create did not checkpoint its remote identity.');
1314
+ }
1315
+ } catch (error) {
1316
+ if (identityController) {
1317
+ operationRecord = identityController.getOperation();
1318
+ journal = identityController.getJournal();
1319
+ }
1320
+ if (CREATE_IDENTITY_BOUNDARY_FAILURES.has(error)) {
1321
+ throw schemaError('SCHEMA_RECONCILIATION_REQUIRED', 'Schema create identity checkpoint requires reconciliation.');
1322
+ }
1323
+ rethrowApplyLockBoundaryFailure(error);
1324
+ if (isServerRevisionConflict(error)) {
1325
+ if (!identityController) {
1326
+ journal = persistJournalOperationRemoval(journal, operationRecord, context);
1327
+ }
1328
+ throw error;
1329
+ }
1330
+ operationRecord = { ...operationRecord, status: 'uncertain' };
1331
+ persistJournalOperation(journal, operationRecord, context);
1332
+ throw schemaError('SCHEMA_RECONCILIATION_REQUIRED', 'Schema remote write result is uncertain.');
1333
+ } finally {
1334
+ if (identityController) {
1335
+ identityController.close();
1336
+ }
1337
+ }
1338
+ if (identityController) {
1339
+ operationRecord = identityController.getOperation();
1340
+ journal = identityController.getJournal();
1341
+ }
1342
+ return finalizeOrdinaryResult({
1343
+ adapter,
1344
+ adapterContext,
1345
+ context,
1346
+ journal,
1347
+ operation,
1348
+ operationRecord,
1349
+ prepared,
1350
+ result,
1351
+ });
1352
+ }
1353
+
1354
+ function markCreateIdentityBoundaryFailure(error) {
1355
+ if (error && (typeof error === 'object' || typeof error === 'function')) {
1356
+ CREATE_IDENTITY_BOUNDARY_FAILURES.add(error);
1357
+ }
1358
+ }
1359
+
1360
+ async function finalizeOrdinaryResult(runtime) {
1361
+ let projection;
1362
+ let usedOperationResultProjection = false;
1363
+ try {
1364
+ const proposedBindings = runtime.adapter.buildBindings(runtime.result, runtime.adapterContext);
1365
+ const proposedStateResource = {
1366
+ adapterVersion: runtime.adapter.adapterVersion,
1367
+ bindings: proposedBindings,
1368
+ };
1369
+ const verificationContext = {
1370
+ ...runtime.adapterContext,
1371
+ operation: runtime.operation,
1372
+ operationResult: runtime.result,
1373
+ prepared: runtime.prepared,
1374
+ stateResource: proposedStateResource,
1375
+ };
1376
+ projection = await projectOperationResult(runtime, proposedStateResource, verificationContext);
1377
+ if (!projection) {
1378
+ const postRead = await readPostWriteResource(runtime, proposedStateResource);
1379
+ projection = postRead.projection;
1380
+ } else {
1381
+ usedOperationResultProjection = true;
1382
+ }
1383
+ await runtime.adapter.verify(projection, verificationContext);
1384
+ } catch (error) {
1385
+ rethrowApplyLockBoundaryFailure(error);
1386
+ const uncertain = { ...runtime.operationRecord, status: 'uncertain' };
1387
+ persistJournalOperation(runtime.journal, uncertain, runtime.context);
1388
+ if (error && error.code === 'SCHEMA_APPLY_VERIFY_FAILED') {
1389
+ throw error;
1390
+ }
1391
+ throw schemaError('SCHEMA_RECONCILIATION_REQUIRED', 'Schema post-write verification is uncertain.');
1392
+ }
1393
+
1394
+ const checkpoint = buildCheckpoint(runtime.context.resource, projection);
1395
+ const completed = { ...runtime.operationRecord, status: 'completed', checkpoint };
1396
+ const journal = persistJournalOperation(runtime.journal, completed, runtime.context);
1397
+ const checkpointed = checkpointState(
1398
+ runtime.context.resource,
1399
+ projection,
1400
+ runtime.context.state,
1401
+ runtime.context
1402
+ );
1403
+ return { journal, state: checkpointed.state, usedOperationResultProjection };
1404
+ }
1405
+
1406
+ async function projectOperationResult(runtime, proposedStateResource, verificationContext) {
1407
+ if (!runtime.adapter || typeof runtime.adapter.projectOperationResult !== 'function') {
1408
+ return null;
1409
+ }
1410
+ const projection = await runtime.adapter.projectOperationResult(
1411
+ runtime.result,
1412
+ proposedStateResource,
1413
+ verificationContext
1414
+ );
1415
+ if (!projection) {
1416
+ return null;
1417
+ }
1418
+ return attachObservedManagedHash(
1419
+ runtime.context.resource,
1420
+ projection,
1421
+ runtime.adapter.adapterVersion
1422
+ );
1423
+ }
1424
+
1425
+ async function readPostWriteResource(runtime, proposedStateResource) {
1426
+ const retry = postWriteReadbackRetry(runtime, proposedStateResource);
1427
+ if (!retry) {
1428
+ return readPostWriteResourceOnce(runtime, proposedStateResource);
1429
+ }
1430
+
1431
+ let lastError;
1432
+ for (let attempt = 1; attempt <= retry.maxAttempts; attempt++) {
1433
+ try {
1434
+ return await readPostWriteResourceOnce(runtime, proposedStateResource);
1435
+ } catch (error) {
1436
+ if (!retry.shouldRetry(error, runtime) || attempt === retry.maxAttempts) {
1437
+ throw error;
1438
+ }
1439
+ lastError = error;
1440
+ assertApplyLockHandleOwned(runtime.context.lock);
1441
+ await waitForPostWriteReadback(retry.delayMs);
1442
+ assertApplyLockHandleOwned(runtime.context.lock);
1443
+ }
1444
+ }
1445
+ throw lastError;
1446
+ }
1447
+
1448
+ function readPostWriteResourceOnce(runtime, proposedStateResource) {
1449
+ return readAndProjectResource(
1450
+ runtime.context.resource,
1451
+ proposedStateResource,
1452
+ runtime.context.state,
1453
+ runtime.context.input,
1454
+ runtime.context.options,
1455
+ runtime.context.registry,
1456
+ runtime.context.lock
1457
+ );
1458
+ }
1459
+
1460
+ function postWriteReadbackRetry(runtime, proposedStateResource) {
1461
+ if (shouldWaitForFormPostWriteReadback(runtime, proposedStateResource)) {
1462
+ return {
1463
+ ...formPostWriteReadbackRetryOptions(runtime.context.options),
1464
+ shouldRetry: isFormPostWriteReadbackReadFailure,
1465
+ };
1466
+ }
1467
+ return null;
1468
+ }
1469
+
1470
+ function shouldWaitForFormPostWriteReadback(runtime, proposedStateResource) {
1471
+ const resource = runtime && runtime.context && runtime.context.resource;
1472
+ const bindings = proposedStateResource && proposedStateResource.bindings || {};
1473
+ return !!(
1474
+ runtime &&
1475
+ resource &&
1476
+ resource.resourceType === 'form' &&
1477
+ typeof bindings.appType === 'string' &&
1478
+ bindings.appType &&
1479
+ typeof bindings.formUuid === 'string' &&
1480
+ bindings.formUuid
1481
+ );
1482
+ }
1483
+
1484
+ function isFormPostWriteReadbackReadFailure(error, runtime) {
1485
+ const resource = runtime && runtime.context && runtime.context.resource;
1486
+ return !!(
1487
+ error &&
1488
+ error.code === 'SCHEMA_REMOTE_READ_FAILED' &&
1489
+ error.details &&
1490
+ resource &&
1491
+ error.details.resourceType === 'form' &&
1492
+ error.details.key === resource.key
1493
+ );
1494
+ }
1495
+
1496
+ function formPostWriteReadbackRetryOptions(options = {}) {
1497
+ const config = options.formPostWriteReadbackRetry || {};
1498
+ return {
1499
+ maxAttempts: positiveIntegerOrDefault(
1500
+ config.maxAttempts,
1501
+ FORM_POST_WRITE_READBACK_MAX_ATTEMPTS
1502
+ ),
1503
+ delayMs: nonNegativeIntegerOrDefault(
1504
+ config.delayMs,
1505
+ FORM_POST_WRITE_READBACK_DELAY_MS
1506
+ ),
1507
+ };
1508
+ }
1509
+
1510
+ function positiveIntegerOrDefault(value, fallback) {
1511
+ return Number.isInteger(value) && value > 0 ? value : fallback;
1512
+ }
1513
+
1514
+ function nonNegativeIntegerOrDefault(value, fallback) {
1515
+ return Number.isInteger(value) && value >= 0 ? value : fallback;
1516
+ }
1517
+
1518
+ function waitForPostWriteReadback(delayMs) {
1519
+ if (delayMs === 0) {
1520
+ return Promise.resolve();
1521
+ }
1522
+ return new Promise(resolve => setTimeout(resolve, delayMs));
1523
+ }
1524
+
1525
+ function assertJitClassification(
1526
+ resource,
1527
+ stateResource,
1528
+ projection,
1529
+ originalChange,
1530
+ registry,
1531
+ reviewedObservedIdentities
1532
+ ) {
1533
+ const reviewedIdentity = reviewedObservedIdentities && reviewedObservedIdentities.get(resourceId(resource));
1534
+ const jitIdentity = createObservedReviewIdentity({
1535
+ resourceType: resource.resourceType,
1536
+ key: resource.key,
1537
+ adapterVersion: resource.adapterVersion,
1538
+ managed: projection.managed,
1539
+ observedManagedHash: projection.observedManagedHash,
1540
+ observedDraftCount: projection.observedDraftCount,
1541
+ observedDraftHash: projection.observedDraftHash,
1542
+ observedIdentityHash: projection.observedIdentityHash,
1543
+ observedIdentityMatchesBindings: projection.observedIdentityMatchesBindings,
1544
+ remoteSchemaHash: projection.remoteSchemaHash,
1545
+ });
1546
+ if (!reviewedIdentity || reviewedIdentity !== jitIdentity) {
1547
+ throw schemaError('SCHEMA_APPLY_JIT_CONFLICT', 'Resource changed after the reviewed plan.', {
1548
+ details: {
1549
+ resourceType: resource.resourceType,
1550
+ key: resource.key,
1551
+ },
1552
+ });
1553
+ }
1554
+ const isolatedState = {
1555
+ kind: 'openyida_resource_state',
1556
+ contractVersion: 1,
1557
+ revision: 0,
1558
+ environment: {
1559
+ environmentKey: hashStable('jit-environment'),
1560
+ corpIdHash: hashStable('jit-corp'),
1561
+ },
1562
+ resources: {
1563
+ [resource.resourceType]: {
1564
+ [resource.key]: stateResource,
1565
+ },
1566
+ },
1567
+ };
1568
+ const plan = createPlan({
1569
+ desiredResources: [resource],
1570
+ manifestHash: hashStable({ resource: resourceId(resource) }),
1571
+ observedResources: [{
1572
+ resourceType: resource.resourceType,
1573
+ key: resource.key,
1574
+ adapterVersion: resource.adapterVersion,
1575
+ managed: projection.managed,
1576
+ observedDraftCount: projection.observedDraftCount,
1577
+ observedDraftHash: projection.observedDraftHash,
1578
+ observedIdentityHash: projection.observedIdentityHash,
1579
+ observedIdentityMatchesBindings: projection.observedIdentityMatchesBindings,
1580
+ remoteSchemaHash: projection.remoteSchemaHash,
1581
+ }],
1582
+ state: isolatedState,
1583
+ }, { registry });
1584
+ const current = plan.changes[0];
1585
+ const matches = originalChange.stateRepair
1586
+ ? current && current.operation === 'noop' && current.stateRepair === true
1587
+ : current && current.operation === originalChange.operation;
1588
+ if (!matches) {
1589
+ throw schemaError('SCHEMA_APPLY_JIT_CONFLICT', 'Resource changed after the reviewed plan.', {
1590
+ details: {
1591
+ operation: current && current.operation,
1592
+ resourceType: resource.resourceType,
1593
+ key: resource.key,
1594
+ },
1595
+ });
1596
+ }
1597
+ }
1598
+
1599
+ function indexObservedReviewIdentities(resources) {
1600
+ const result = new Map();
1601
+ for (const resource of Array.isArray(resources) ? resources : []) {
1602
+ result.set(resourceId(resource), createObservedReviewIdentity(resource));
1603
+ }
1604
+ return result;
1605
+ }
1606
+
1607
+ function createObservedReviewIdentity(resource) {
1608
+ const observedManagedHash = resource.observedManagedHash || hashObservedManaged({
1609
+ resourceType: resource.resourceType,
1610
+ key: resource.key,
1611
+ adapterVersion: resource.adapterVersion,
1612
+ managed: resource.managed || {},
1613
+ });
1614
+ const identity = {
1615
+ adapterVersion: resource.adapterVersion,
1616
+ key: resource.key,
1617
+ observedManagedHash,
1618
+ resourceType: resource.resourceType,
1619
+ };
1620
+ if (resource.remoteSchemaHash !== undefined) {
1621
+ if (!isSha256(resource.remoteSchemaHash)) {
1622
+ throw schemaError('SCHEMA_INTERNAL_ERROR', 'Observed resource remoteSchemaHash must be a SHA-256 hash.');
1623
+ }
1624
+ identity.remoteSchemaHash = resource.remoteSchemaHash;
1625
+ }
1626
+ for (const property of [
1627
+ 'observedDraftCount',
1628
+ 'observedDraftHash',
1629
+ 'observedIdentityHash',
1630
+ 'observedIdentityMatchesBindings',
1631
+ ]) {
1632
+ if (resource[property] !== undefined) {
1633
+ identity[property] = resource[property];
1634
+ }
1635
+ }
1636
+ return hashStable(identity);
1637
+ }
1638
+
1639
+ async function readAndProjectResource(resource, stateResource, state, input, options, registry, lock) {
1640
+ if (!stateResource) {
1641
+ throw schemaError('SCHEMA_STATE_RESOURCE_BINDING_MISSING', 'State resource binding is missing.', {
1642
+ details: { resourceType: resource.resourceType, key: resource.key },
1643
+ });
1644
+ }
1645
+ const adapter = registry.get(resource.resourceType);
1646
+ const adapterContext = {
1647
+ ...createAdapterContext(input, options, state),
1648
+ assertRemoteDispatchBoundary: phase => assertRemoteDispatchBoundary({ lock }, phase),
1649
+ resource,
1650
+ stateResource,
1651
+ };
1652
+ let raw;
1653
+ try {
1654
+ assertApplyLockHandleOwned(lock);
1655
+ raw = await adapter.readObserved(stateResource, adapterContext);
1656
+ assertApplyLockHandleOwned(lock);
1657
+ } catch (error) {
1658
+ if (isRemoteMissingError(error)) {
1659
+ throw schemaError('SCHEMA_APPLY_JIT_CONFLICT', 'Bound remote resource is missing.', {
1660
+ details: { resourceType: resource.resourceType, key: resource.key },
1661
+ });
1662
+ }
1663
+ if (error && error.code === 'SCHEMA_REMOTE_RESOURCE_MISSING') {
1664
+ throw schemaError('SCHEMA_REMOTE_READ_FAILED', 'Remote observed read failed.', {
1665
+ details: { resourceType: resource.resourceType, key: resource.key },
1666
+ });
1667
+ }
1668
+ if (error && error.code && String(error.code).startsWith('SCHEMA_')) {
1669
+ throw error;
1670
+ }
1671
+ throw schemaError('SCHEMA_REMOTE_READ_FAILED', 'Remote observed read failed.', {
1672
+ details: { resourceType: resource.resourceType, key: resource.key },
1673
+ });
1674
+ }
1675
+ let projection;
1676
+ try {
1677
+ projection = adapter.projectObserved(raw, stateResource, adapterContext);
1678
+ } catch (error) {
1679
+ if (error && error.code && String(error.code).startsWith('SCHEMA_')) {
1680
+ throw error;
1681
+ }
1682
+ throw schemaError('SCHEMA_REMOTE_PROJECT_FAILED', 'Remote observed projection failed.', {
1683
+ details: { resourceType: resource.resourceType, key: resource.key },
1684
+ });
1685
+ }
1686
+ attachObservedManagedHash(resource, projection, adapter.adapterVersion);
1687
+ return { raw, projection };
1688
+ }
1689
+
1690
+ function attachObservedManagedHash(resource, projection, adapterVersion) {
1691
+ projection.observedManagedHash = projection.observedManagedHash || hashObservedManaged({
1692
+ resourceType: resource.resourceType,
1693
+ key: resource.key,
1694
+ adapterVersion,
1695
+ managed: projection.managed || {},
1696
+ });
1697
+ return projection;
1698
+ }
1699
+
1700
+ function checkpointState(resource, projection, state, context) {
1701
+ assertApplyLockHandleOwned(context.lock);
1702
+ const checkpoint = buildCheckpoint(resource, projection);
1703
+ let nextState = upsertResourceState(state, {
1704
+ resourceType: resource.resourceType,
1705
+ key: resource.key,
1706
+ ...checkpoint,
1707
+ }, { registry: context.registry });
1708
+ nextState.manifestHash = context.input.manifestHash || state.manifestHash;
1709
+ assertSafeApplyPath(context.input.statePath, context.input.workspaceRoot, {
1710
+ fsImpl: context.options.fsImpl,
1711
+ });
1712
+ assertApplyLockHandleOwned(context.lock);
1713
+ nextState = writeStateAtomic(context.input.statePath, nextState, {
1714
+ environment: context.input.environment,
1715
+ fsImpl: context.options.fsImpl,
1716
+ registry: context.registry,
1717
+ });
1718
+ assertApplyLockHandleOwned(context.lock);
1719
+ return { checkpoint, state: nextState };
1720
+ }
1721
+
1722
+ function buildCheckpoint(resource, projection) {
1723
+ const checkpoint = {
1724
+ adapterVersion: resource.adapterVersion,
1725
+ bindings: clonePlain(projection.bindings || {}),
1726
+ lastAppliedHash: hashStable(resource.desired || {}),
1727
+ observedManagedHash: projection.observedManagedHash,
1728
+ lastApplied: clonePlain(resource.desired || {}),
1729
+ };
1730
+ if (projection.remoteSchemaHash) {
1731
+ checkpoint.remoteSchemaHash = projection.remoteSchemaHash;
1732
+ }
1733
+ return checkpoint;
1734
+ }
1735
+
1736
+ function createOperationRecord(resource, operation, status, planId, checkpoint, stateRevision) {
1737
+ const record = {
1738
+ operationId: createApplyOperationId({
1739
+ planId,
1740
+ resourceType: resource.resourceType,
1741
+ key: resource.key,
1742
+ operation,
1743
+ }),
1744
+ resourceType: resource.resourceType,
1745
+ key: resource.key,
1746
+ operation,
1747
+ adapterVersion: resource.adapterVersion,
1748
+ desiredHash: desiredHash(resource),
1749
+ status,
1750
+ };
1751
+ if (stateRevision !== undefined) {
1752
+ record.stateRevision = stateRevision;
1753
+ }
1754
+ if (checkpoint) {
1755
+ record.checkpoint = checkpoint;
1756
+ }
1757
+ return record;
1758
+ }
1759
+
1760
+ function persistJournalOperation(journal, operation, context) {
1761
+ assertApplyLockHandleOwned(context.lock);
1762
+ const next = updateJournalOperation(journal, operation, {
1763
+ environment: context.input.environment,
1764
+ registry: context.registry,
1765
+ });
1766
+ writeApplyJournalAtomic(context.paths.journalPath, next, {
1767
+ environment: context.input.environment,
1768
+ fsImpl: context.options.fsImpl,
1769
+ registry: context.registry,
1770
+ workspaceRoot: context.input.workspaceRoot,
1771
+ });
1772
+ assertApplyLockHandleOwned(context.lock);
1773
+ return next;
1774
+ }
1775
+
1776
+ function persistJournalOperationRemoval(journal, operation, context) {
1777
+ assertApplyLockHandleOwned(context.lock);
1778
+ const next = removeJournalOperation(journal, operation, {
1779
+ environment: context.input.environment,
1780
+ registry: context.registry,
1781
+ });
1782
+ writeApplyJournalAtomic(context.paths.journalPath, next, {
1783
+ environment: context.input.environment,
1784
+ fsImpl: context.options.fsImpl,
1785
+ registry: context.registry,
1786
+ workspaceRoot: context.input.workspaceRoot,
1787
+ });
1788
+ assertApplyLockHandleOwned(context.lock);
1789
+ return next;
1790
+ }
1791
+
1792
+ function stateMatchesCheckpoint(state, operation) {
1793
+ const entry = getResourceState(state, operation.resourceType, operation.key);
1794
+ return !!(
1795
+ entry &&
1796
+ operation.checkpoint &&
1797
+ entry.adapterVersion === operation.checkpoint.adapterVersion &&
1798
+ entry.lastAppliedHash === operation.checkpoint.lastAppliedHash
1799
+ );
1800
+ }
1801
+
1802
+ function stateAlreadyCheckpointed(resource, state) {
1803
+ const entry = getResourceState(state, resource.resourceType, resource.key);
1804
+ return !!(
1805
+ entry &&
1806
+ entry.adapterVersion === resource.adapterVersion &&
1807
+ entry.lastAppliedHash === hashStable(resource.desired || {})
1808
+ );
1809
+ }
1810
+
1811
+ function journalIsFullyCheckpointed(journal, state) {
1812
+ return Object.values(journal.operations || {}).every(operation => (
1813
+ operation.status === 'completed' && stateMatchesCheckpoint(state, operation)
1814
+ ));
1815
+ }
1816
+
1817
+ function desiredHash(resource) {
1818
+ return hashManagedIdentity({
1819
+ adapterVersion: resource.adapterVersion,
1820
+ managed: resource.desired || {},
1821
+ resourceType: resource.resourceType,
1822
+ key: resource.key,
1823
+ });
1824
+ }
1825
+
1826
+ function createAdapterContext(input, options, state) {
1827
+ return {
1828
+ authRef: input.authRef,
1829
+ environment: input.environment,
1830
+ formPostWriteReadbackRetry: formPostWriteReadbackRetryOptions(options),
1831
+ fsImpl: options.fsImpl,
1832
+ services: options.services || {},
1833
+ state,
1834
+ workspaceRoot: input.workspaceRoot,
1835
+ };
1836
+ }
1837
+
1838
+ function assertApplyAdapter(adapter) {
1839
+ for (const method of [
1840
+ 'readObserved',
1841
+ 'projectObserved',
1842
+ 'create',
1843
+ 'update',
1844
+ 'verify',
1845
+ 'buildBindings',
1846
+ ]) {
1847
+ if (typeof adapter[method] !== 'function') {
1848
+ throw schemaError('SCHEMA_RESOURCE_TYPE_UNSUPPORTED', 'Resource adapter does not support schema apply.', {
1849
+ details: { resourceType: adapter.resourceType },
1850
+ });
1851
+ }
1852
+ }
1853
+ }
1854
+
1855
+ function assertApplyAdapters(resources, registry, context = {}) {
1856
+ const entries = (resources || []).map(resource => ({
1857
+ adapter: registry.get(resource.resourceType),
1858
+ resource,
1859
+ }));
1860
+ for (const entry of entries) {
1861
+ assertApplyAdapter(entry.adapter);
1862
+ }
1863
+ for (const entry of entries) {
1864
+ if (typeof entry.adapter.preflightApply === 'function') {
1865
+ entry.adapter.preflightApply(entry.resource, context);
1866
+ }
1867
+ }
1868
+ }
1869
+
1870
+ function clonePlain(value) {
1871
+ return value === undefined ? undefined : JSON.parse(JSON.stringify(value));
1872
+ }
1873
+
1874
+ module.exports = {
1875
+ APPLY_CONTRACT_VERSION,
1876
+ APPLY_KIND,
1877
+ applySchema,
1878
+ };