owlrunkit 0.13.0 → 0.15.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.
@@ -1,8 +1,10 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import {
3
3
  existsSync,
4
+ lstatSync,
4
5
  mkdirSync,
5
6
  readFileSync,
7
+ readdirSync,
6
8
  realpathSync,
7
9
  renameSync,
8
10
  rmSync,
@@ -12,7 +14,7 @@ import { isAbsolute, relative, resolve, win32 } from "node:path";
12
14
  import { fileURLToPath } from "node:url";
13
15
 
14
16
  export const CONTRACT_VERSION = "0.2";
15
- export const CORE_VERSION = "0.13.0";
17
+ export const CORE_VERSION = "0.15.0";
16
18
  export const RUNTIME_ROOT = ".owlcoda/runkit";
17
19
 
18
20
  const CORE_FILES = [
@@ -26,7 +28,9 @@ const CORE_FILES = [
26
28
  "lease-lifecycle.mjs",
27
29
  "lifecycle-orchestration.mjs",
28
30
  "offline-store.mjs",
31
+ "onboarding-doctor.mjs",
29
32
  "profile-impact.mjs",
33
+ "profile-onboarding.mjs",
30
34
  "provenance-common.mjs",
31
35
  "quick-attest.mjs",
32
36
  "quick-canonical.mjs",
@@ -36,6 +40,8 @@ const CORE_FILES = [
36
40
  "quick-workspace-snapshot.mjs",
37
41
  "ready-for-commit.mjs",
38
42
  "receipt-lineage.mjs",
43
+ "registry-adoption-gate.mjs",
44
+ "registry-adoption.mjs",
39
45
  "repair-execution.mjs",
40
46
  "resume-execution.mjs",
41
47
  "resource-preflight.mjs",
@@ -47,6 +53,7 @@ const CORE_FILES = [
47
53
  "verification-plan.mjs",
48
54
  "visual-smoke.mjs",
49
55
  "schemas/closeout-receipt-v2.schema.json",
56
+ "schemas/adoption-evidence-v1.schema.json",
50
57
  "schemas/coverage-adopt-request-v1.schema.json",
51
58
  "schemas/core-artifact.schema.json",
52
59
  "schemas/core-artifact-v2.schema.json",
@@ -67,6 +74,8 @@ const CORE_FILES = [
67
74
  "schemas/visual-smoke-request-v1.schema.json",
68
75
  "schemas/visual-smoke-result-v1.schema.json",
69
76
  "schemas/verification-context-v1.schema.json",
77
+ "schemas/verification-finalize-attempt-v1.schema.json",
78
+ "schemas/verification-preflight-v1.schema.json",
70
79
  "schemas/verification-plan-v1.schema.json",
71
80
  "schemas/verification-receipt-v2.schema.json",
72
81
  "schemas/verify-plan-request-v1.schema.json",
@@ -131,6 +140,22 @@ function migrationReceiptName(migration, fromCore, toCore) {
131
140
  return `config-v02-core-refresh-${from}-to-${to}.json`;
132
141
  }
133
142
 
143
+ function writeMigrationReceiptAppendOnly(receiptsRoot, baseName, receipt) {
144
+ const bytes = `${JSON.stringify(receipt, null, 2)}\n`;
145
+ const stem = baseName.slice(0, -".json".length);
146
+ for (let attempt = 1; ; attempt += 1) {
147
+ const receiptName = attempt === 1
148
+ ? baseName
149
+ : `${stem}-attempt-${String(attempt).padStart(3, "0")}.json`;
150
+ try {
151
+ writeFileSync(resolve(receiptsRoot, receiptName), bytes, { flag: "wx" });
152
+ return receiptName;
153
+ } catch (error) {
154
+ if (error?.code !== "EEXIST") throw error;
155
+ }
156
+ }
157
+ }
158
+
134
159
  function migrateProjectConfig({ runtimeRoot, configPath, config, migration }) {
135
160
  validateCoreIdentity(config.core);
136
161
  const next = projectConfig();
@@ -145,8 +170,11 @@ function migrateProjectConfig({ runtimeRoot, configPath, config, migration }) {
145
170
  };
146
171
  const receiptsRoot = resolve(runtimeRoot, "config-migration-receipts");
147
172
  mkdirSync(receiptsRoot, { recursive: true });
148
- const receiptName = migrationReceiptName(migration, config.core, next.core);
149
- atomicWriteJson(resolve(receiptsRoot, receiptName), receipt);
173
+ const receiptName = writeMigrationReceiptAppendOnly(
174
+ receiptsRoot,
175
+ migrationReceiptName(migration, config.core, next.core),
176
+ receipt,
177
+ );
150
178
  atomicWriteJson(configPath, next);
151
179
  return `${RUNTIME_ROOT}/config-migration-receipts/${receiptName}`;
152
180
  }
@@ -253,6 +281,98 @@ export function validateProjectConfigV2(config) {
253
281
  }
254
282
  }
255
283
 
284
+ export function inspectProjectUpgradeSafety({ workspaceRoot } = {}) {
285
+ if (typeof workspaceRoot !== "string" || workspaceRoot.length === 0) {
286
+ throw new Error("workspaceRoot is required.");
287
+ }
288
+ const root = realpathSync(workspaceRoot);
289
+ const executionsRoot = resolve(root, RUNTIME_ROOT, "executions");
290
+ if (!existsSync(executionsRoot)) {
291
+ return {
292
+ status: "safe",
293
+ activeRunIds: [],
294
+ activeLeaseIds: [],
295
+ issues: [],
296
+ };
297
+ }
298
+ const executionsStat = lstatSync(executionsRoot);
299
+ if (executionsStat.isSymbolicLink() || !executionsStat.isDirectory()) {
300
+ return {
301
+ status: "unsafe",
302
+ activeRunIds: [],
303
+ activeLeaseIds: [],
304
+ issues: ["RunKit executions root must be a regular directory."],
305
+ };
306
+ }
307
+ const activeRunIds = [];
308
+ const activeLeaseIds = [];
309
+ const issues = [];
310
+ const entries = readdirSync(executionsRoot, { withFileTypes: true })
311
+ .sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
312
+ for (const entry of entries) {
313
+ if (entry.isSymbolicLink()) {
314
+ issues.push(`Execution directory must not be a symlink: ${entry.name}`);
315
+ continue;
316
+ }
317
+ if (!entry.isDirectory()) continue;
318
+ const executionRoot = resolve(executionsRoot, entry.name);
319
+ const closeoutPath = resolve(executionRoot, "closeout-receipt.json");
320
+ if (!existsSync(closeoutPath)) {
321
+ activeRunIds.push(entry.name);
322
+ } else {
323
+ const closeoutStat = lstatSync(closeoutPath);
324
+ if (closeoutStat.isSymbolicLink() || !closeoutStat.isFile()) {
325
+ issues.push(`Closeout receipt must be a regular file: ${entry.name}`);
326
+ } else {
327
+ try {
328
+ const closeout = JSON.parse(readFileSync(closeoutPath, "utf8"));
329
+ if (closeout?.artifact?.payload?.runId !== entry.name) {
330
+ issues.push(`Closeout receipt runId mismatch: ${entry.name}`);
331
+ }
332
+ } catch {
333
+ issues.push(`Closeout receipt is invalid JSON: ${entry.name}`);
334
+ }
335
+ }
336
+ }
337
+ const leasesRoot = resolve(executionRoot, "leases");
338
+ if (!existsSync(leasesRoot)) continue;
339
+ const leasesStat = lstatSync(leasesRoot);
340
+ if (leasesStat.isSymbolicLink() || !leasesStat.isDirectory()) {
341
+ issues.push(`Lease directory must be a regular directory: ${entry.name}`);
342
+ continue;
343
+ }
344
+ const leaseEntries = readdirSync(leasesRoot, { withFileTypes: true })
345
+ .sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
346
+ for (const leaseEntry of leaseEntries) {
347
+ if (!leaseEntry.name.endsWith(".json")) continue;
348
+ if (leaseEntry.isSymbolicLink() || !leaseEntry.isFile()) {
349
+ issues.push(`Lease artifact must be a regular file: ${entry.name}/${leaseEntry.name}`);
350
+ continue;
351
+ }
352
+ try {
353
+ const lease = JSON.parse(readFileSync(resolve(leasesRoot, leaseEntry.name), "utf8"));
354
+ if (lease.state === "active") {
355
+ activeLeaseIds.push(`${entry.name}:${lease.workItemId ?? leaseEntry.name}`);
356
+ } else if (lease.state !== "released") {
357
+ issues.push(`Lease state is invalid: ${entry.name}/${leaseEntry.name}`);
358
+ }
359
+ } catch {
360
+ issues.push(`Lease artifact is invalid JSON: ${entry.name}/${leaseEntry.name}`);
361
+ }
362
+ }
363
+ }
364
+ return {
365
+ status: activeRunIds.length === 0
366
+ && activeLeaseIds.length === 0
367
+ && issues.length === 0
368
+ ? "safe"
369
+ : "unsafe",
370
+ activeRunIds,
371
+ activeLeaseIds,
372
+ issues,
373
+ };
374
+ }
375
+
256
376
  function validateProducer(producer) {
257
377
  if (!isRecord(producer) || typeof producer.adapterKind !== "string" || typeof producer.adapterVersion !== "string") {
258
378
  throw new Error("Producer requires adapterKind and adapterVersion.");
@@ -335,10 +455,10 @@ export async function initializeProjectRunKit({ workspaceRoot } = {}) {
335
455
  const runtimeRoot = resolve(root, RUNTIME_ROOT);
336
456
  const remainder = relative(root, runtimeRoot);
337
457
  if (remainder.startsWith("..") || isAbsolute(remainder)) throw new Error("Runtime root escapes the workspace.");
338
- mkdirSync(resolve(runtimeRoot, "executions"), { recursive: true });
339
458
  const configPath = resolve(runtimeRoot, "config.json");
340
459
  const profilesPath = resolve(runtimeRoot, "profiles.json");
341
460
  if (!existsSync(configPath)) {
461
+ mkdirSync(resolve(runtimeRoot, "executions"), { recursive: true });
342
462
  atomicWriteJson(configPath, projectConfig());
343
463
  } else {
344
464
  const config = JSON.parse(readFileSync(configPath, "utf8"));
@@ -350,6 +470,16 @@ export async function initializeProjectRunKit({ workspaceRoot } = {}) {
350
470
  if (config.schemaVersion === "OwlCodaRunKitConfigV2" && currentGate.status === "valid") {
351
471
  // The project already points at this exact Core identity.
352
472
  } else if (config.schemaVersion === "OwlCodaRunKitConfigV1" && config.core?.contractVersion === "0.1") {
473
+ const safety = inspectProjectUpgradeSafety({ workspaceRoot: root });
474
+ if (safety.status !== "safe") {
475
+ return {
476
+ ...safety,
477
+ status: "upgrade_blocked_active_execution",
478
+ exitCode: 2,
479
+ runtimeRoot: RUNTIME_ROOT,
480
+ core: current,
481
+ };
482
+ }
353
483
  const migrationReceipt = migrateProjectConfig({
354
484
  runtimeRoot,
355
485
  configPath,
@@ -361,6 +491,16 @@ export async function initializeProjectRunKit({ workspaceRoot } = {}) {
361
491
  }
362
492
  return { status: "upgraded", exitCode: 0, runtimeRoot: RUNTIME_ROOT, core: current, migrationReceipt };
363
493
  } else if (config.schemaVersion === "OwlCodaRunKitConfigV2" && config.core?.contractVersion === CONTRACT_VERSION) {
494
+ const safety = inspectProjectUpgradeSafety({ workspaceRoot: root });
495
+ if (safety.status !== "safe") {
496
+ return {
497
+ ...safety,
498
+ status: "upgrade_blocked_active_execution",
499
+ exitCode: 2,
500
+ runtimeRoot: RUNTIME_ROOT,
501
+ core: current,
502
+ };
503
+ }
364
504
  const migrationReceipt = migrateProjectConfig({
365
505
  runtimeRoot,
366
506
  configPath,
@@ -375,6 +515,7 @@ export async function initializeProjectRunKit({ workspaceRoot } = {}) {
375
515
  throw new Error(`Unsupported project config migration: ${config.schemaVersion ?? "missing schemaVersion"}.`);
376
516
  }
377
517
  }
518
+ mkdirSync(resolve(runtimeRoot, "executions"), { recursive: true });
378
519
  if (!existsSync(profilesPath)) {
379
520
  writeFileSync(profilesPath, `${JSON.stringify({ schemaVersion: "OwlCodaRunKitProfilesV1", profiles: [] }, null, 2)}\n`);
380
521
  }