@younndai/lyt 0.13.5 → 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 (65) hide show
  1. package/README.md +27 -19
  2. package/dist/automator-run.d.ts +1 -0
  3. package/dist/automator-run.d.ts.map +1 -1
  4. package/dist/automator-run.js +5 -3
  5. package/dist/automator-run.js.map +1 -1
  6. package/dist/bench/pod-harness.d.ts.map +1 -1
  7. package/dist/bench/pod-harness.js +49 -1
  8. package/dist/bench/pod-harness.js.map +1 -1
  9. package/dist/build-program.d.ts +10 -1
  10. package/dist/build-program.d.ts.map +1 -1
  11. package/dist/build-program.js +23 -3
  12. package/dist/build-program.js.map +1 -1
  13. package/dist/cli.js +10 -4
  14. package/dist/cli.js.map +1 -1
  15. package/dist/commands/capture.d.ts.map +1 -1
  16. package/dist/commands/capture.js +1 -2
  17. package/dist/commands/capture.js.map +1 -1
  18. package/dist/commands/init.d.ts +2 -0
  19. package/dist/commands/init.d.ts.map +1 -1
  20. package/dist/commands/init.js +404 -244
  21. package/dist/commands/init.js.map +1 -1
  22. package/dist/commands/install.d.ts +4 -0
  23. package/dist/commands/install.d.ts.map +1 -0
  24. package/dist/commands/install.js +107 -0
  25. package/dist/commands/install.js.map +1 -0
  26. package/dist/commands/outdated.d.ts.map +1 -1
  27. package/dist/commands/outdated.js +19 -5
  28. package/dist/commands/outdated.js.map +1 -1
  29. package/dist/commands/update.d.ts +90 -2
  30. package/dist/commands/update.d.ts.map +1 -1
  31. package/dist/commands/update.js +883 -48
  32. package/dist/commands/update.js.map +1 -1
  33. package/dist/flows/init-bootstrap.d.ts +7 -1
  34. package/dist/flows/init-bootstrap.d.ts.map +1 -1
  35. package/dist/flows/init-bootstrap.js +240 -69
  36. package/dist/flows/init-bootstrap.js.map +1 -1
  37. package/dist/index.d.ts +6 -0
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +3 -0
  40. package/dist/index.js.map +1 -1
  41. package/dist/install/doctor-health.d.ts +16 -0
  42. package/dist/install/doctor-health.d.ts.map +1 -0
  43. package/dist/install/doctor-health.js +575 -0
  44. package/dist/install/doctor-health.js.map +1 -0
  45. package/dist/install/provider-inventory.d.ts +40 -0
  46. package/dist/install/provider-inventory.d.ts.map +1 -0
  47. package/dist/install/provider-inventory.js +73 -0
  48. package/dist/install/provider-inventory.js.map +1 -0
  49. package/dist/install/reconcile-engine.d.ts +87 -0
  50. package/dist/install/reconcile-engine.d.ts.map +1 -0
  51. package/dist/install/reconcile-engine.js +771 -0
  52. package/dist/install/reconcile-engine.js.map +1 -0
  53. package/dist/install/target-artifacts.d.ts +91 -0
  54. package/dist/install/target-artifacts.d.ts.map +1 -0
  55. package/dist/install/target-artifacts.js +597 -0
  56. package/dist/install/target-artifacts.js.map +1 -0
  57. package/dist/install/update-operation.d.ts +47 -0
  58. package/dist/install/update-operation.d.ts.map +1 -0
  59. package/dist/install/update-operation.js +318 -0
  60. package/dist/install/update-operation.js.map +1 -0
  61. package/dist/install/update-plan.d.ts +132 -0
  62. package/dist/install/update-plan.d.ts.map +1 -0
  63. package/dist/install/update-plan.js +418 -0
  64. package/dist/install/update-plan.js.map +1 -0
  65. package/package.json +16 -6
@@ -0,0 +1,771 @@
1
+ /*
2
+ * Copyright 2026 MARLINK TRADING SRL (YounndAI)
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import { createHash } from "node:crypto";
17
+ import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, readdirSync, renameSync, symlinkSync, writeFileSync, } from "node:fs";
18
+ import { homedir, platform } from "node:os";
19
+ import { basename, dirname, join, parse, relative, resolve } from "node:path";
20
+ import { createRequire } from "node:module";
21
+ import { composeManagedManualMarker, newUuidv7Bytes, openReceiptAttempt, parseReceiptV1ForEmission, reopenReceiptAttempt, uuid7BytesToDashedString, } from "@younndai/lyt-vault";
22
+ import { canonicalJson, digestCanonical, UPDATE_PLAN_PACKAGES } from "./update-plan.js";
23
+ export function defaultReconcileJournalRoot(homeDir = homedir()) {
24
+ return join(homeDir, "lyt", ".install-reconcile");
25
+ }
26
+ export function buildInstallReconcileFailureEnvelopeV1(error, options = {}) {
27
+ const journalRoot = resolve(options.journalRoot ?? defaultReconcileJournalRoot());
28
+ const payload = {
29
+ schema_id: "lyt.install-reconcile-plan",
30
+ schema_version: { major: 1, minor: 0 },
31
+ journal_root: journalRoot,
32
+ objects: Object.freeze([]),
33
+ };
34
+ const planDigest = digestCanonical(payload);
35
+ const plan = Object.freeze({
36
+ ...payload,
37
+ operation_id: digestToUuidV7(planDigest),
38
+ plan_digest: planDigest,
39
+ });
40
+ const now = options.now?.() ?? new Date();
41
+ return envelopeFromState({
42
+ plan,
43
+ attemptId: options.newAttemptId?.() ?? uuid7BytesToDashedString(newUuidv7Bytes()),
44
+ startedAt: now.toISOString(),
45
+ finishedAt: now.toISOString(),
46
+ disposition: "new",
47
+ completed: [],
48
+ pending: [],
49
+ refused: [],
50
+ mutations: 0,
51
+ dryRun: false,
52
+ failure: error instanceof Error ? error.message : "install-reconcile-failed",
53
+ nextActionOverride: "lyt doctor --json",
54
+ });
55
+ }
56
+ export function readPackageVersion(packageName) {
57
+ const require = createRequire(import.meta.url);
58
+ const packageJsonPath = (require.resolve.paths(packageName) ?? [])
59
+ .map((base) => join(base, ...packageName.split("/"), "package.json"))
60
+ .find((candidate) => existsSync(candidate));
61
+ if (packageJsonPath === undefined)
62
+ throw new Error("install-provider-package-root-not-found");
63
+ const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
64
+ if (typeof parsed.version !== "string")
65
+ throw new Error("install-provider-invalid-version");
66
+ return parsed.version;
67
+ }
68
+ export function prepareInstallReconcilePlanV1(providers, options = {}) {
69
+ assertProviderSet(providers);
70
+ const journalRoot = resolve(options.journalRoot ?? defaultReconcileJournalRoot());
71
+ assertPathChainHasNoLinks(journalRoot, true);
72
+ const objects = providers
73
+ .flatMap((provider) => provider.objects)
74
+ .map((object) => inspectProviderObject(object))
75
+ .sort((a, b) => a.object_id.localeCompare(b.object_id));
76
+ const ids = objects.map((object) => object.object_id);
77
+ if (new Set(ids).size !== ids.length)
78
+ throw new Error("install-provider-duplicate-object");
79
+ const payload = {
80
+ schema_id: "lyt.install-reconcile-plan",
81
+ schema_version: { major: 1, minor: 0 },
82
+ journal_root: journalRoot,
83
+ objects,
84
+ };
85
+ const planDigest = digestCanonical(payload);
86
+ const operationId = digestToUuidV7(planDigest);
87
+ return Object.freeze({ ...payload, operation_id: operationId, plan_digest: planDigest });
88
+ }
89
+ export async function reconcileInstallProvidersV1(providers, input = { apply: false }, options = {}) {
90
+ const now = options.now ?? (() => new Date());
91
+ const newAttemptId = options.newAttemptId ?? (() => uuid7BytesToDashedString(newUuidv7Bytes()));
92
+ const journalRoot = resolve(options.journalRoot ?? defaultReconcileJournalRoot());
93
+ const freshPlan = prepareInstallReconcilePlanV1(providers, { journalRoot });
94
+ let journal;
95
+ let plan;
96
+ if (input.resumeOperationId !== undefined) {
97
+ if (!input.apply)
98
+ throw new Error("install-reconcile-resume-requires-apply");
99
+ journal = readJournal(journalRoot, input.resumeOperationId);
100
+ plan = journal.plan;
101
+ journal = recoverAppliedObjects(plan, freshPlan, journal);
102
+ assertResumeEnvironment(plan, freshPlan, journal);
103
+ }
104
+ else {
105
+ plan = freshPlan;
106
+ journal = newJournal(plan);
107
+ }
108
+ const attemptId = journal.active_attempt_id ?? newAttemptId();
109
+ const startedAt = journal.active_started_at ?? now().toISOString();
110
+ if (!input.apply) {
111
+ return envelopeFromState({
112
+ plan,
113
+ attemptId,
114
+ startedAt,
115
+ finishedAt: now().toISOString(),
116
+ disposition: "new",
117
+ completed: plan.objects
118
+ .filter((object) => object.disposition === "already-current")
119
+ .map((object) => object.object_id),
120
+ pending: plan.objects
121
+ .filter((object) => object.disposition === "planned")
122
+ .map((object) => object.object_id),
123
+ refused: plan.objects
124
+ .filter((object) => object.disposition === "refused")
125
+ .map((object) => object.object_id),
126
+ mutations: 0,
127
+ dryRun: true,
128
+ });
129
+ }
130
+ const resumePendingAttempt = input.resumeOperationId !== undefined && journal.active_attempt_id !== null;
131
+ const pendingReceipt = baseReceipt({
132
+ plan,
133
+ attemptId,
134
+ startedAt,
135
+ finishedAt: startedAt,
136
+ disposition: input.resumeOperationId === undefined ? "new" : "resumed",
137
+ status: "failed",
138
+ mutations: 0,
139
+ errorCode: "install-reconcile-interrupted",
140
+ nextAction: "resume-install-reconcile",
141
+ });
142
+ const receiptResult = resumePendingAttempt
143
+ ? await (options.receiptReopen ?? reopenReceiptAttempt)(pendingReceipt)
144
+ : await (options.receiptOpen ?? openReceiptAttempt)(pendingReceipt);
145
+ const receiptSession = receiptResult.kind === "ready" ? receiptResult.session : null;
146
+ if (receiptSession === null) {
147
+ return envelopeFromState({
148
+ plan,
149
+ attemptId,
150
+ startedAt,
151
+ finishedAt: now().toISOString(),
152
+ disposition: input.resumeOperationId === undefined ? "new" : "resumed",
153
+ completed: journal.completed,
154
+ pending: journal.pending,
155
+ refused: journal.refused,
156
+ mutations: 0,
157
+ dryRun: false,
158
+ failure: "receipt-store-unavailable",
159
+ nextActionOverride: "lyt doctor --json",
160
+ });
161
+ }
162
+ journal = Object.freeze({
163
+ ...journal,
164
+ active_attempt_id: attemptId,
165
+ active_started_at: startedAt,
166
+ status: "interrupted",
167
+ });
168
+ writeJournal(journalRoot, journal);
169
+ const sourceObjects = new Map(providers.flatMap((provider) => provider.objects).map((object) => [object.object_id, object]));
170
+ const completed = new Set(journal.completed);
171
+ const refused = new Set(journal.refused);
172
+ let mutations = 0;
173
+ let failure = null;
174
+ for (const objectPlan of plan.objects) {
175
+ if (completed.has(objectPlan.object_id) || refused.has(objectPlan.object_id))
176
+ continue;
177
+ if (objectPlan.disposition === "already-current") {
178
+ completed.add(objectPlan.object_id);
179
+ continue;
180
+ }
181
+ if (objectPlan.disposition === "refused") {
182
+ refused.add(objectPlan.object_id);
183
+ continue;
184
+ }
185
+ const source = sourceObjects.get(objectPlan.object_id);
186
+ if (source === undefined) {
187
+ failure = "install-provider-object-missing";
188
+ break;
189
+ }
190
+ try {
191
+ revalidateObject(source, objectPlan);
192
+ const staged = stageObject(journalRoot, source);
193
+ applyOneObject(source, staged);
194
+ verifyAppliedObject(source, staged);
195
+ completed.add(objectPlan.object_id);
196
+ mutations += 1;
197
+ if (options.failBeforeJournalAfterObject === objectPlan.object_id) {
198
+ throw new Error("install-reconcile-injected-prejournal-interruption");
199
+ }
200
+ const currentPending = plan.objects
201
+ .map((entry) => entry.object_id)
202
+ .filter((id) => !completed.has(id) && !refused.has(id));
203
+ journal = Object.freeze({
204
+ ...journal,
205
+ completed: Object.freeze([...completed].sort()),
206
+ pending: Object.freeze(currentPending),
207
+ refused: Object.freeze([...refused].sort()),
208
+ status: "interrupted",
209
+ });
210
+ writeJournal(journalRoot, journal);
211
+ if (options.failAfterObject === objectPlan.object_id) {
212
+ throw new Error("install-reconcile-injected-interruption");
213
+ }
214
+ }
215
+ catch (error) {
216
+ if (error instanceof Error &&
217
+ error.message === "install-reconcile-injected-prejournal-interruption") {
218
+ throw error;
219
+ }
220
+ failure = error instanceof Error ? error.message : "install-reconcile-apply-failed";
221
+ break;
222
+ }
223
+ }
224
+ const pending = plan.objects
225
+ .map((entry) => entry.object_id)
226
+ .filter((id) => !completed.has(id) && !refused.has(id));
227
+ journal = Object.freeze({
228
+ ...journal,
229
+ completed: Object.freeze([...completed].sort()),
230
+ pending: Object.freeze(pending),
231
+ refused: Object.freeze([...refused].sort()),
232
+ active_attempt_id: failure === null ? null : attemptId,
233
+ active_started_at: failure === null ? null : startedAt,
234
+ status: failure === null ? terminalJournalStatus(pending, [...refused]) : "interrupted",
235
+ });
236
+ writeJournal(journalRoot, journal);
237
+ let envelope = envelopeFromState({
238
+ plan,
239
+ attemptId,
240
+ startedAt,
241
+ finishedAt: now().toISOString(),
242
+ disposition: input.resumeOperationId === undefined ? "new" : "resumed",
243
+ completed: [...completed].sort(),
244
+ pending,
245
+ refused: [...refused].sort(),
246
+ mutations,
247
+ dryRun: false,
248
+ failure,
249
+ });
250
+ if (receiptSession !== null) {
251
+ try {
252
+ const warnings = await receiptSession.finalize(envelope.receipt);
253
+ if (warnings.length > 0)
254
+ throw new Error("receipt-finalization-warning");
255
+ }
256
+ catch {
257
+ envelope = envelopeFromState({
258
+ plan,
259
+ attemptId,
260
+ startedAt,
261
+ finishedAt: now().toISOString(),
262
+ disposition: input.resumeOperationId === undefined ? "new" : "resumed",
263
+ completed: [...completed].sort(),
264
+ pending,
265
+ refused: [...refused].sort(),
266
+ mutations,
267
+ dryRun: false,
268
+ failure: "receipt-finalization-failed",
269
+ });
270
+ journal = Object.freeze({
271
+ ...journal,
272
+ active_attempt_id: attemptId,
273
+ active_started_at: startedAt,
274
+ status: "interrupted",
275
+ });
276
+ writeJournal(journalRoot, journal);
277
+ return envelope;
278
+ }
279
+ journal = Object.freeze({
280
+ ...journal,
281
+ active_attempt_id: null,
282
+ active_started_at: null,
283
+ status: terminalJournalStatus(pending, [...refused]),
284
+ });
285
+ writeJournal(journalRoot, journal);
286
+ }
287
+ return envelope;
288
+ }
289
+ function recoverAppliedObjects(sealed, observed, journal) {
290
+ const currentById = new Map(observed.objects.map((object) => [object.object_id, object]));
291
+ const completed = new Set(journal.completed);
292
+ for (const object of sealed.objects) {
293
+ const current = currentById.get(object.object_id);
294
+ if (journal.pending.includes(object.object_id) &&
295
+ current?.disposition === "already-current" &&
296
+ current.observed_digest === object.expected_applied_digest) {
297
+ completed.add(object.object_id);
298
+ }
299
+ }
300
+ if (completed.size === journal.completed.length)
301
+ return journal;
302
+ return Object.freeze({
303
+ ...journal,
304
+ completed: Object.freeze([...completed].sort()),
305
+ pending: Object.freeze(journal.pending.filter((id) => !completed.has(id))),
306
+ });
307
+ }
308
+ function assertResumeEnvironment(sealed, observed, journal) {
309
+ const observedById = new Map(observed.objects.map((object) => [object.object_id, object]));
310
+ if (observedById.size !== sealed.objects.length) {
311
+ throw new Error("install-reconcile-destination-drift");
312
+ }
313
+ const completed = new Set(journal.completed);
314
+ for (const expected of sealed.objects) {
315
+ const current = observedById.get(expected.object_id);
316
+ if (current === undefined ||
317
+ current.provider_package !== expected.provider_package ||
318
+ current.provider_version !== expected.provider_version ||
319
+ current.kind !== expected.kind ||
320
+ current.target_path !== expected.target_path ||
321
+ current.expected_digest !== expected.expected_digest ||
322
+ canonicalJson(current.trusted_legacy_digests) !==
323
+ canonicalJson(expected.trusted_legacy_digests) ||
324
+ current.expected_applied_digest !== expected.expected_applied_digest ||
325
+ (completed.has(expected.object_id) && current.disposition !== "already-current") ||
326
+ (completed.has(expected.object_id) &&
327
+ current.observed_digest !== expected.expected_applied_digest) ||
328
+ (!completed.has(expected.object_id) &&
329
+ (current.disposition !== expected.disposition ||
330
+ current.observed_digest !== expected.observed_digest))) {
331
+ throw new Error("install-reconcile-destination-drift");
332
+ }
333
+ }
334
+ }
335
+ function inspectProviderObject(object) {
336
+ assertPathChainHasNoLinks(dirname(object.target_path), true);
337
+ let disposition = "planned";
338
+ let refusalCode = null;
339
+ let observedDigest = null;
340
+ let expectedAppliedDigest = object.expected_digest;
341
+ try {
342
+ const stat = lstatSync(object.target_path);
343
+ if (stat.isSymbolicLink()) {
344
+ if (object.kind === "directory-link") {
345
+ const linked = resolve(dirname(object.target_path), readlinkSync(object.target_path));
346
+ observedDigest = digestPathTree(linked);
347
+ if (observedDigest === object.expected_digest)
348
+ disposition = "already-current";
349
+ else {
350
+ disposition = "refused";
351
+ refusalCode = "divergent-link";
352
+ }
353
+ }
354
+ else {
355
+ disposition = "refused";
356
+ refusalCode = "symlink-leaf";
357
+ }
358
+ }
359
+ else if (object.kind === "directory-link" && stat.isDirectory()) {
360
+ observedDigest = digestPathTree(object.target_path);
361
+ if (observedDigest === object.expected_digest) {
362
+ disposition = "already-current";
363
+ }
364
+ else {
365
+ disposition = "refused";
366
+ refusalCode = "handler-owned-skill-leaf";
367
+ }
368
+ }
369
+ else if (object.kind === "marker-file" && stat.isFile()) {
370
+ const current = readFileSync(object.target_path, "utf8");
371
+ const next = replaceOwnedMarker(current, object);
372
+ observedDigest = digestBytes(Buffer.from(current));
373
+ expectedAppliedDigest = digestBytes(Buffer.from(next));
374
+ disposition = next === current ? "already-current" : "planned";
375
+ }
376
+ else {
377
+ disposition = "refused";
378
+ refusalCode = "unsupported-leaf-type";
379
+ }
380
+ }
381
+ catch (error) {
382
+ if (error.code !== "ENOENT")
383
+ throw error;
384
+ }
385
+ return Object.freeze({
386
+ object_id: object.object_id,
387
+ provider_package: object.provider_package,
388
+ provider_version: object.provider_version,
389
+ kind: object.kind,
390
+ target_path: resolve(object.target_path),
391
+ expected_digest: object.expected_digest,
392
+ observed_digest: observedDigest,
393
+ expected_applied_digest: expectedAppliedDigest,
394
+ trusted_legacy_digests: Object.freeze([...object.trusted_legacy_digests].sort()),
395
+ disposition,
396
+ refusal_code: refusalCode,
397
+ });
398
+ }
399
+ function revalidateObject(source, sealed) {
400
+ const current = inspectProviderObject(source);
401
+ if (canonicalJson(current) !== canonicalJson(sealed)) {
402
+ throw new Error("install-reconcile-destination-drift");
403
+ }
404
+ if (source.kind === "directory-link" &&
405
+ digestPathTree(source.source_path) !== source.expected_digest) {
406
+ throw new Error("install-reconcile-provider-drift");
407
+ }
408
+ if (source.kind === "marker-file" &&
409
+ digestBytes(Buffer.from(source.content)) !== source.expected_digest) {
410
+ throw new Error("install-reconcile-provider-drift");
411
+ }
412
+ }
413
+ function stageObject(root, object) {
414
+ assertPathChainHasNoLinks(root, true);
415
+ const expectedAppliedDigest = inspectProviderObject(object).expected_applied_digest;
416
+ const objectRoot = join(root, "objects", expectedAppliedDigest);
417
+ assertPathChainHasNoLinks(objectRoot, true);
418
+ const primary = join(objectRoot, object.kind === "directory-link" ? "content" : "content.md");
419
+ mkdirSync(objectRoot, { recursive: true });
420
+ const candidates = [primary, `${primary}.recovery-${expectedAppliedDigest.slice(0, 16)}`];
421
+ for (const staged of candidates) {
422
+ assertPathChainHasNoLinks(staged, true);
423
+ if (!existsSync(staged)) {
424
+ if (object.kind === "directory-link")
425
+ cpSync(object.source_path, staged, { recursive: true });
426
+ else {
427
+ const existing = existsSync(object.target_path)
428
+ ? readFileSync(object.target_path, "utf8")
429
+ : "";
430
+ writeFileSync(staged, replaceOwnedMarker(existing, object), {
431
+ encoding: "utf8",
432
+ flag: "wx",
433
+ });
434
+ }
435
+ }
436
+ const actual = object.kind === "directory-link" ? digestPathTree(staged) : digestBytes(readFileSync(staged));
437
+ if (actual === expectedAppliedDigest)
438
+ return staged;
439
+ }
440
+ throw new Error("install-reconcile-staging-mismatch");
441
+ }
442
+ function applyOneObject(object, staged) {
443
+ assertPathChainHasNoLinks(dirname(object.target_path), true);
444
+ mkdirSync(dirname(object.target_path), { recursive: true });
445
+ if (object.kind === "directory-link") {
446
+ if (existsSync(object.target_path)) {
447
+ if (digestPathTree(object.target_path) === object.expected_digest)
448
+ return;
449
+ throw new Error("install-reconcile-refuses-existing-skill-leaf");
450
+ }
451
+ symlinkSync(staged, object.target_path, platform() === "win32" ? "junction" : "dir");
452
+ return;
453
+ }
454
+ atomicWriteFile(object.target_path, readFileSync(staged, "utf8"));
455
+ }
456
+ function verifyAppliedObject(object, staged) {
457
+ if (object.kind === "directory-link") {
458
+ const linked = resolve(dirname(object.target_path), readlinkSync(object.target_path));
459
+ if (resolve(linked) !== resolve(staged) || digestPathTree(linked) !== object.expected_digest) {
460
+ throw new Error("install-reconcile-verification-failed");
461
+ }
462
+ return;
463
+ }
464
+ if (digestBytes(readFileSync(object.target_path)) !== digestBytes(readFileSync(staged))) {
465
+ throw new Error("install-reconcile-verification-failed");
466
+ }
467
+ }
468
+ function assertProviderSet(providers) {
469
+ const names = providers.map((provider) => provider.package).sort();
470
+ const expected = [...UPDATE_PLAN_PACKAGES].sort();
471
+ if (canonicalJson(names) !== canonicalJson(expected)) {
472
+ throw new Error("install-provider-exact-seven-required");
473
+ }
474
+ if (new Set(providers.map((provider) => provider.version)).size !== 1) {
475
+ throw new Error("install-provider-version-skew");
476
+ }
477
+ for (const provider of providers) {
478
+ if (provider.schema_id !== "lyt.install-provider" ||
479
+ provider.schema_version.major !== 1 ||
480
+ provider.schema_version.minor !== 0 ||
481
+ provider.objects.some((object) => object.provider_package !== provider.package ||
482
+ object.provider_version !== provider.version) ||
483
+ provider.objects.some((object) => new Set(object.trusted_legacy_digests).size !== object.trusted_legacy_digests.length ||
484
+ object.trusted_legacy_digests.some((digest) => !/^[a-f0-9]{64}$/.test(digest) || digest === object.expected_digest))) {
485
+ throw new Error("install-provider-invalid-manifest");
486
+ }
487
+ }
488
+ }
489
+ function replaceOwnedMarker(existing, object) {
490
+ const composed = composeManagedManualMarker(existing, object.content);
491
+ if (composed.status === "malformed") {
492
+ throw new Error("install-reconcile-malformed-managed-marker");
493
+ }
494
+ return composed.result;
495
+ }
496
+ function newJournal(plan) {
497
+ const pending = plan.objects
498
+ .filter((object) => object.disposition === "planned")
499
+ .map((object) => object.object_id);
500
+ const refused = plan.objects
501
+ .filter((object) => object.disposition === "refused")
502
+ .map((object) => object.object_id);
503
+ return Object.freeze({
504
+ schema_id: "lyt.install-reconcile-journal",
505
+ schema_version: Object.freeze({ major: 1, minor: 0 }),
506
+ plan,
507
+ completed: Object.freeze(plan.objects
508
+ .filter((object) => object.disposition === "already-current")
509
+ .map((object) => object.object_id)),
510
+ pending: Object.freeze(pending),
511
+ refused: Object.freeze(refused),
512
+ active_attempt_id: null,
513
+ active_started_at: null,
514
+ status: terminalJournalStatus(pending, refused),
515
+ });
516
+ }
517
+ function terminalJournalStatus(pending, refused) {
518
+ if (pending.length > 0)
519
+ return "pending";
520
+ return refused.length > 0 ? "refused" : "complete";
521
+ }
522
+ function journalPath(root, operationId) {
523
+ return join(root, "operations", operationId, "journal.json");
524
+ }
525
+ function writeJournal(root, journal) {
526
+ const path = journalPath(root, journal.plan.operation_id);
527
+ assertPathChainHasNoLinks(root, true);
528
+ assertPathChainHasNoLinks(dirname(path), true);
529
+ assertPathChainHasNoLinks(path, true);
530
+ mkdirSync(dirname(path), { recursive: true });
531
+ atomicWriteFile(path, `${JSON.stringify(journal, null, 2)}\n`);
532
+ }
533
+ function readJournal(root, operationId) {
534
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(operationId))
535
+ throw new Error("install-reconcile-invalid-operation-id");
536
+ const path = journalPath(root, operationId);
537
+ assertPathChainHasNoLinks(root, true);
538
+ assertPathChainHasNoLinks(dirname(path), true);
539
+ assertPathChainHasNoLinks(path, true);
540
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
541
+ if (!Array.isArray(parsed.plan?.objects) ||
542
+ !Array.isArray(parsed.completed) ||
543
+ !Array.isArray(parsed.pending) ||
544
+ !Array.isArray(parsed.refused)) {
545
+ throw new Error("install-reconcile-journal-tampered");
546
+ }
547
+ const planIds = new Set(parsed.plan?.objects?.map((object) => object.object_id) ?? []);
548
+ const completed = new Set(parsed.completed ?? []);
549
+ const pending = new Set(parsed.pending ?? []);
550
+ const refused = new Set(parsed.refused ?? []);
551
+ const partition = new Set([...completed, ...pending, ...refused]);
552
+ const dispositions = new Map((parsed.plan?.objects ?? []).map((object) => [object.object_id, object.disposition]));
553
+ if (parsed.schema_id !== "lyt.install-reconcile-journal" ||
554
+ parsed.schema_version?.major !== 1 ||
555
+ parsed.schema_version?.minor !== 0 ||
556
+ !["pending", "complete", "refused", "interrupted"].includes(parsed.status) ||
557
+ parsed.plan?.schema_id !== "lyt.install-reconcile-plan" ||
558
+ parsed.plan?.schema_version?.major !== 1 ||
559
+ parsed.plan?.schema_version?.minor !== 0 ||
560
+ parsed.plan.operation_id !== operationId ||
561
+ !Array.isArray(parsed.plan.objects) ||
562
+ !Array.isArray(parsed.completed) ||
563
+ !Array.isArray(parsed.pending) ||
564
+ !Array.isArray(parsed.refused) ||
565
+ !parsed.completed.every((value) => typeof value === "string") ||
566
+ !parsed.pending.every((value) => typeof value === "string") ||
567
+ !parsed.refused.every((value) => typeof value === "string") ||
568
+ completed.size !== parsed.completed.length ||
569
+ pending.size !== parsed.pending.length ||
570
+ refused.size !== parsed.refused.length ||
571
+ partition.size !== planIds.size ||
572
+ [...partition].some((id) => !planIds.has(id)) ||
573
+ [...completed].some((id) => refused.has(id) || pending.has(id) || dispositions.get(id) === "refused") ||
574
+ [...pending].some((id) => refused.has(id) || dispositions.get(id) !== "planned") ||
575
+ [...refused].some((id) => dispositions.get(id) !== "refused") ||
576
+ (parsed.active_attempt_id !== null && typeof parsed.active_attempt_id !== "string") ||
577
+ (parsed.active_started_at !== null && typeof parsed.active_started_at !== "string") ||
578
+ (parsed.active_attempt_id === null) !== (parsed.active_started_at === null) ||
579
+ (parsed.status === "interrupted") !== (parsed.active_attempt_id !== null) ||
580
+ (parsed.status === "pending" && (parsed.active_attempt_id !== null || pending.size === 0)) ||
581
+ (parsed.status === "complete" &&
582
+ (parsed.active_attempt_id !== null || pending.size > 0 || refused.size > 0)) ||
583
+ (parsed.status === "refused" &&
584
+ (parsed.active_attempt_id !== null || pending.size > 0 || refused.size === 0)) ||
585
+ parsed.plan.plan_digest !==
586
+ digestCanonical({
587
+ schema_id: parsed.plan.schema_id,
588
+ schema_version: parsed.plan.schema_version,
589
+ journal_root: parsed.plan.journal_root,
590
+ objects: parsed.plan.objects,
591
+ })) {
592
+ throw new Error("install-reconcile-journal-tampered");
593
+ }
594
+ return parsed;
595
+ }
596
+ export function inspectReconcileJournalV1(root, operationId) {
597
+ try {
598
+ return Object.freeze({
599
+ valid: true,
600
+ journal: readJournal(resolve(root), operationId),
601
+ error_code: null,
602
+ });
603
+ }
604
+ catch (error) {
605
+ return Object.freeze({
606
+ valid: false,
607
+ journal: null,
608
+ error_code: safeErrorCode(error instanceof Error ? error.message : "install-reconcile-journal-invalid"),
609
+ });
610
+ }
611
+ }
612
+ function atomicWriteFile(path, content) {
613
+ const temporary = join(dirname(path), `.${basename(path)}.${process.pid}.tmp`);
614
+ assertPathChainHasNoLinks(dirname(path), true);
615
+ assertPathChainHasNoLinks(path, true);
616
+ assertPathChainHasNoLinks(temporary, true);
617
+ writeFileSync(temporary, content, { encoding: "utf8", flag: "w" });
618
+ renameSync(temporary, path);
619
+ }
620
+ function envelopeFromState(args) {
621
+ const failure = args.failure ?? null;
622
+ const incomplete = args.pending.length > 0 || args.refused.length > 0 || failure !== null;
623
+ const status = args.dryRun
624
+ ? "planned"
625
+ : incomplete
626
+ ? args.mutations > 0
627
+ ? "partial"
628
+ : args.refused.length > 0
629
+ ? "refused"
630
+ : "failed"
631
+ : args.mutations > 0
632
+ ? "success"
633
+ : "no-op";
634
+ const nextAction = args.nextActionOverride ??
635
+ (args.dryRun && (args.pending.length > 0 || args.refused.length > 0)
636
+ ? args.refused.length > 0
637
+ ? "lyt doctor --json"
638
+ : "lyt install reconcile --apply --json"
639
+ : incomplete
640
+ ? args.refused.length > 0 && args.pending.length === 0 && failure === null
641
+ ? "lyt doctor --json"
642
+ : `lyt install reconcile --resume ${args.plan.operation_id} --apply --json`
643
+ : null);
644
+ const receiptStatus = args.dryRun ? "no-op" : status === "planned" ? "no-op" : status;
645
+ const receipt = baseReceipt({
646
+ plan: args.plan,
647
+ attemptId: args.attemptId,
648
+ startedAt: args.startedAt,
649
+ finishedAt: args.finishedAt,
650
+ disposition: status === "refused" ? "rejected" : args.disposition,
651
+ status: receiptStatus,
652
+ mutations: args.mutations,
653
+ errorCode: args.dryRun || !incomplete ? null : (failure ?? "install-reconcile-refused-object"),
654
+ nextAction: args.dryRun || !incomplete
655
+ ? null
656
+ : args.refused.length > 0 && args.pending.length === 0 && failure === null
657
+ ? "inspect-install-reconcile"
658
+ : "resume-install-reconcile",
659
+ });
660
+ return Object.freeze({
661
+ schema_id: "lyt.install-reconcile-result",
662
+ schema_version: Object.freeze({ major: 1, minor: 0 }),
663
+ success: !incomplete,
664
+ status,
665
+ operation_id: args.plan.operation_id,
666
+ attempt_id: args.attemptId,
667
+ plan_digest: args.plan.plan_digest,
668
+ plan: args.plan,
669
+ mutations: args.mutations,
670
+ completed: Object.freeze([...args.completed]),
671
+ pending: Object.freeze([...args.pending]),
672
+ refused: Object.freeze([...args.refused]),
673
+ next_action: nextAction,
674
+ receipt,
675
+ });
676
+ }
677
+ function baseReceipt(args) {
678
+ return parseReceiptV1ForEmission({
679
+ schema_id: "lyt.receipt",
680
+ schema_version: { major: 1, minor: 0 },
681
+ operation_id: args.plan.operation_id,
682
+ attempt_id: args.attemptId,
683
+ operation: "install-reconcile",
684
+ scope: { kind: "system" },
685
+ timestamps: { started_at: args.startedAt, finished_at: args.finishedAt },
686
+ replay: { disposition: args.disposition, key_digest: args.plan.plan_digest },
687
+ status: args.status,
688
+ exit_code: args.status === "success" || args.status === "no-op" ? 0 : 1,
689
+ mutations: { local: args.mutations, remote: 0 },
690
+ evidence: {
691
+ before: [
692
+ {
693
+ kind: "reconcile-plan",
694
+ subject: "sealed install reconciliation plan",
695
+ digest: args.plan.plan_digest,
696
+ },
697
+ ],
698
+ after: [
699
+ {
700
+ kind: "managed-objects",
701
+ subject: "verified managed destination objects",
702
+ count: args.mutations,
703
+ },
704
+ ],
705
+ },
706
+ next_action: args.nextAction === null
707
+ ? null
708
+ : { code: args.nextAction, summary: "Resume the exact install reconciliation operation." },
709
+ error: args.errorCode === null
710
+ ? null
711
+ : {
712
+ code: safeErrorCode(args.errorCode),
713
+ summary: "Install reconciliation is incomplete.",
714
+ retryable: true,
715
+ },
716
+ });
717
+ }
718
+ function safeErrorCode(value) {
719
+ const safe = value
720
+ .toLowerCase()
721
+ .replace(/[^a-z0-9]+/g, "-")
722
+ .replace(/^-|-$/g, "");
723
+ return safe.length > 0 ? safe.slice(0, 96) : "install-reconcile-failed";
724
+ }
725
+ export function assertPathChainHasNoLinks(path, includeLeaf) {
726
+ const absolute = resolve(path);
727
+ const parsed = parse(absolute);
728
+ const parts = relative(parsed.root, absolute).split(/[\\/]/).filter(Boolean);
729
+ let current = parsed.root;
730
+ const last = includeLeaf ? parts.length : Math.max(0, parts.length - 1);
731
+ for (let index = 0; index < last; index += 1) {
732
+ current = join(current, parts[index]);
733
+ try {
734
+ if (lstatSync(current).isSymbolicLink())
735
+ throw new Error("install-reconcile-symlink-path");
736
+ }
737
+ catch (error) {
738
+ if (error.code === "ENOENT")
739
+ return;
740
+ throw error;
741
+ }
742
+ }
743
+ }
744
+ export function digestBytes(value) {
745
+ return createHash("sha256").update(value).digest("hex");
746
+ }
747
+ export function digestPathTree(path) {
748
+ const stat = lstatSync(path);
749
+ if (stat.isSymbolicLink())
750
+ throw new Error("install-provider-source-symlink");
751
+ if (stat.isFile())
752
+ return digestBytes(readFileSync(path));
753
+ if (!stat.isDirectory())
754
+ throw new Error("install-provider-source-type");
755
+ const hash = createHash("sha256");
756
+ for (const name of readdirSync(path).sort()) {
757
+ const child = join(path, name);
758
+ hash.update(name, "utf8");
759
+ hash.update("\0");
760
+ hash.update(digestPathTree(child), "hex");
761
+ }
762
+ return hash.digest("hex");
763
+ }
764
+ function digestToUuidV7(digest) {
765
+ const chars = digest.slice(0, 32).split("");
766
+ chars[12] = "7";
767
+ chars[16] = ["8", "9", "a", "b"][Number.parseInt(chars[16], 16) % 4];
768
+ const raw = chars.join("");
769
+ return `${raw.slice(0, 8)}-${raw.slice(8, 12)}-${raw.slice(12, 16)}-${raw.slice(16, 20)}-${raw.slice(20)}`;
770
+ }
771
+ //# sourceMappingURL=reconcile-engine.js.map