@timurproko/a1 0.1.8-dev.218 → 0.1.8-dev.224

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 (42) hide show
  1. package/bin/cli.js +6 -0
  2. package/bin/guardian.js +3 -0
  3. package/bin/release-cleanup.js +8 -0
  4. package/bin/ui.js +4 -0
  5. package/bin/warmup.js +18 -0
  6. package/dist/features/owned-ui/run.js +2 -0
  7. package/dist/foundation/launch-guardian/main.js +2 -0
  8. package/dist/foundation/release/bootstrap.d.ts +3 -1
  9. package/dist/foundation/release/bootstrap.js +9 -0
  10. package/dist/foundation/release/cohort-state.d.ts +61 -1
  11. package/dist/foundation/release/cohort-state.js +192 -11
  12. package/dist/foundation/release/dependency-layer.d.ts +78 -0
  13. package/dist/foundation/release/dependency-layer.js +487 -0
  14. package/dist/foundation/release/endpoints.d.ts +1 -1
  15. package/dist/foundation/release/endpoints.js +8 -8
  16. package/dist/foundation/release/index.d.ts +2 -0
  17. package/dist/foundation/release/index.js +2 -0
  18. package/dist/foundation/release/release-gc.d.ts +59 -7
  19. package/dist/foundation/release/release-gc.js +533 -19
  20. package/dist/foundation/release/release-store.d.ts +3 -1
  21. package/dist/foundation/release/release-store.js +60 -10
  22. package/dist/foundation/release/release.d.ts +4 -1
  23. package/dist/foundation/release/release.js +9 -2
  24. package/dist/foundation/release/update.d.ts +10 -2
  25. package/dist/foundation/release/update.js +27 -4
  26. package/dist/foundation/release/warmup.d.ts +3 -0
  27. package/dist/foundation/release/warmup.js +35 -0
  28. package/dist/foundation/startup/index.d.ts +1 -0
  29. package/dist/foundation/startup/index.js +1 -0
  30. package/dist/foundation/startup/startup-runtime.d.ts +36 -0
  31. package/dist/foundation/startup/startup-runtime.js +140 -0
  32. package/dist/integrations/pi/engine/runtime-integration.js +5 -0
  33. package/dist/native/darwin-arm64/manifest.json +3 -3
  34. package/dist/native/darwin-arm64/process-guardian +0 -0
  35. package/dist/native/linux-x64/manifest.json +1 -1
  36. package/dist/native/win32-x64/manifest.json +2 -2
  37. package/dist/native/win32-x64/process-guardian.exe +0 -0
  38. package/dist/product-identity.d.ts +2 -2
  39. package/dist/product-identity.js +4 -1
  40. package/dist/product-identity.json +8 -1
  41. package/dist/runtime-payload-inventory.json +122328 -0
  42. package/package.json +2 -2
package/bin/cli.js CHANGED
@@ -1,6 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ const commandStartedAtMs = performance.timeOrigin + performance.now();
3
4
  const packageRoot = new URL("..", import.meta.url);
5
+ const startup = await import("../dist/foundation/startup/index.js");
6
+ const startupProfile = process.argv[2] === "pi" ? "pi" : "a1";
7
+ startup.initializeStartupTrace(process.env, startupProfile, commandStartedAtMs);
8
+ startup.enableEnvironmentCompileCache(process.env);
9
+ await startup.markStartupPhase(process.env, "command-invoked");
4
10
  const { fileURLToPath } = await import("node:url");
5
11
  const { readFile } = await import("node:fs/promises");
6
12
  const { cliCapabilities, dispatchCli } = await import("../dist/cli/index.js");
package/bin/guardian.js CHANGED
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ const startup = await import("../dist/foundation/startup/index.js");
4
+ startup.enableEnvironmentCompileCache(process.env);
5
+ await startup.markStartupPhase(process.env, "guardian-start");
3
6
  const { fileURLToPath } = await import("node:url");
4
7
  const { resolve } = await import("node:path");
5
8
  const { runLaunchGuardian } = await import("../dist/foundation/launch-guardian/index.js");
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { runReleaseCleanupWorker } = await import("../dist/foundation/release/index.js");
4
+
5
+ runReleaseCleanupWorker().catch(error => {
6
+ console.error(error instanceof Error ? error.message : String(error));
7
+ process.exitCode = 1;
8
+ });
package/bin/ui.js CHANGED
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ const startup = await import("../dist/foundation/startup/index.js");
4
+ startup.enableEnvironmentCompileCache(process.env);
5
+ await startup.markStartupPhase(process.env, "ui-entry");
3
6
  const { assertSinglePiTuiModuleAtLaunch } = await import("./module-identity.js");
4
7
  const { fileURLToPath } = await import("node:url");
5
8
 
@@ -27,6 +30,7 @@ runSelectedInteractiveRuntime(process.env.A1_LAUNCH_PROFILE ?? "a1", {
27
30
  import("../dist/features/owned-ui/index.js"),
28
31
  import("../dist/composition/index.js"),
29
32
  ]);
33
+ await startup.markStartupPhase(process.env, "ui-modules-loaded");
30
34
  const { application, settings } = await composeOwnedUi({
31
35
  cwd: process.cwd(),
32
36
  profileId,
package/bin/warmup.js ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+
3
+ const startup = await import("../dist/foundation/startup/index.js");
4
+ startup.assertImmutableWarmupEnvironment(process.env);
5
+ startup.enableEnvironmentCompileCache(process.env);
6
+
7
+ // Security: this entry imports but never composes, so it creates no terminal, profile/session
8
+ // path, trust callback, executable resource loader, extension runner, or network client.
9
+ const [composition, engine, components, runtime] = await Promise.all([
10
+ import("../dist/composition/index.js"),
11
+ import("../dist/integrations/pi/engine/index.js"),
12
+ import("../dist/integrations/pi/components/index.js"),
13
+ import("../dist/integrations/pi/tui-runtime/index.js"),
14
+ ]);
15
+ if (typeof composition.composeOwnedUi !== "function" || typeof engine.createPiEngineAdapter !== "function"
16
+ || typeof components.applyConfiguredPiTheme !== "function" || typeof runtime.createPiTerminalBridge !== "function") {
17
+ throw new Error("immutable startup graph is incomplete");
18
+ }
@@ -1,3 +1,4 @@
1
+ import { markStartupPhase } from "../../foundation/startup/index.js";
1
2
  export async function runOwnedUi(options) {
2
3
  const { application, settings } = options;
3
4
  try {
@@ -5,6 +6,7 @@ export async function runOwnedUi(options) {
5
6
  await settings.load();
6
7
  application.start();
7
8
  await application.flush();
9
+ await markStartupPhase(process.env, "first-input-ready-render");
8
10
  if (!application.disposed)
9
11
  await application.waitUntilStopped();
10
12
  return 0;
@@ -5,6 +5,7 @@ import { assertLaunchProfileId } from "../lifecycle/index.js";
5
5
  import { closeVerifiedContainment, LinuxNativeProcessInspector, NativeGuardianContainment, resolveProcessGuardianPath, verifyProcessGuardianArtifact, WindowsNativeProcessInspector, } from "../process-containment/index.js";
6
6
  import { SupervisorClient } from "../protocol/index.js";
7
7
  import { resolveCohortEndpoint, resolveProductPaths } from "../supervision/index.js";
8
+ import { markStartupPhase } from "../startup/index.js";
8
9
  export async function runLaunchGuardian(options) {
9
10
  assertLaunchProfileId(options.profileId);
10
11
  const environment = { ...(options.environment ?? process.env) };
@@ -24,6 +25,7 @@ export async function runLaunchGuardian(options) {
24
25
  ? resolveCohortEndpoint(paths, releaseId, environment).endpoint
25
26
  : paths.endpoint;
26
27
  await client.connect(endpoint);
28
+ await markStartupPhase(environment, "guardian-connected");
27
29
  const containment = options.containment ?? new NativeGuardianContainment(instanceId, helperPath, resolve(paths.runtimeDir, "launch-instances", `${instanceId}.ready.json`));
28
30
  const stop = stopRequests(client, instanceId);
29
31
  let rootIdentity = null;
@@ -1,6 +1,6 @@
1
1
  import { type OwnershipProbe } from "./cohort-selection.js";
2
2
  import { type SupervisorEndpointMetadata } from "./cohort-state.js";
3
- import { type LaunchProfileId } from "../lifecycle/index.js";
3
+ import { resolveProductPaths, type LaunchProfileId } from "../lifecycle/index.js";
4
4
  import { cleanupProvenIdleOwner } from "./process-cleanup.js";
5
5
  import { type MaterializedRelease, type VerifyMaterializedReleaseOptions } from "./release-store.js";
6
6
  export interface BootstrapOptions {
@@ -21,6 +21,8 @@ export interface BootstrapOptions {
21
21
  * release is fit.
22
22
  */
23
23
  readonly releaseIsLaunchable?: (releaseRoot: string) => boolean;
24
+ /** Test seam for bounded maintenance scheduled after stale endpoint sweeping. */
25
+ readonly scheduleMaintenance?: (dataDir: string, paths: ReturnType<typeof resolveProductPaths>) => Promise<void>;
24
26
  }
25
27
  export declare function runBootstrap(options: BootstrapOptions): Promise<number>;
26
28
  export declare function certifyMaterializedRelease(release: MaterializedRelease, dataDir: string, verification?: VerifyMaterializedReleaseOptions): Promise<string>;
@@ -10,9 +10,12 @@ import { encodeFrame, LineFrameDecoder } from "../protocol/index.js";
10
10
  import { cleanupProvenIdleOwner, processIsAlive } from "./process-cleanup.js";
11
11
  import { sweepDeadEndpoints } from "./endpoints.js";
12
12
  import { consumeMaterializationProof, materializeRelease, readCertifiedReleaseManifest, readMaterializedRelease, resolveReleaseEntryPoint, verifyMaterializedRelease } from "./release-store.js";
13
+ import { scheduleReleaseCleanup } from "./release-gc.js";
13
14
  import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
15
+ import { markStartupPhase } from "../startup/index.js";
14
16
  export async function runBootstrap(options) {
15
17
  const environment = { ...(options.environment ?? process.env) };
18
+ await markStartupPhase(environment, "bootstrap-start");
16
19
  const launchProfileId = options.launchIntent?.profileId ?? "a1";
17
20
  assertLaunchProfileId(launchProfileId);
18
21
  environment[PRODUCT_IDENTITY.environment.launchProfile] = launchProfileId;
@@ -25,6 +28,10 @@ export async function runBootstrap(options) {
25
28
  // can now be several of them. Clearing them first keeps the decision below about what is
26
29
  // actually running.
27
30
  await sweepDeadEndpoints(paths).catch(() => []);
31
+ // Performance: reconciliation is serialized by cohort state, but launch never awaits a physical
32
+ // backlog. The scheduler commits a plan and starts a detached bounded worker.
33
+ const scheduleMaintenance = options.scheduleMaintenance ?? scheduleReleaseCleanup;
34
+ void scheduleMaintenance(paths.dataDir, paths).catch(() => { });
28
35
  // Protocol: each cohort keeps its own endpoint, so a launch looks for the endpoint of the release it
29
36
  // is about to run rather than for the one endpoint the runtime directory used to have.
30
37
  const activeReleaseId = state.references.active;
@@ -196,6 +203,7 @@ export async function startSupervisor(release, environment) {
196
203
  child.unref();
197
204
  }
198
205
  async function launchUi(release, environment) {
206
+ await markStartupPhase(environment, "bootstrap-selected");
199
207
  const entry = await resolveReleaseEntryPoint(release, "bin/guardian.js");
200
208
  return await new Promise((resolvePromise, rejectPromise) => {
201
209
  const child = spawn(process.execPath, [entry], {
@@ -211,6 +219,7 @@ export function releaseEnvironment(environment, release) {
211
219
  return {
212
220
  ...environment,
213
221
  [PRODUCT_IDENTITY.environment.releaseId]: release.releaseId,
222
+ [PRODUCT_IDENTITY.environment.releaseLayers]: (release.dependencyLayers ?? []).map(layer => layer.layerId).join(","),
214
223
  [PRODUCT_IDENTITY.environment.releaseRoot]: release.releaseRoot,
215
224
  [PRODUCT_IDENTITY.environment.releaseDigest]: release.contentDigest,
216
225
  };
@@ -18,11 +18,41 @@ export interface ReleaseReferences {
18
18
  readonly rollback: string | null;
19
19
  readonly retention: readonly string[];
20
20
  }
21
+ export type ExternalReleaseHoldAuthority = "agent" | "migration";
22
+ /** A current hold supplied by an authority that must participate in every reconciliation. */
23
+ export interface ExternalReleaseHold {
24
+ readonly authority: ExternalReleaseHoldAuthority;
25
+ readonly releaseId: string;
26
+ }
27
+ export interface ActiveReleaseTransactionReference {
28
+ readonly status: "active" | "completed" | "rolled-back" | "failed";
29
+ readonly priorActiveReleaseId: string | null;
30
+ }
31
+ export type ReleaseCleanupStage = "detached" | "trash";
32
+ export interface ReleaseCleanupDisposition {
33
+ readonly release: ReleaseRecord;
34
+ readonly stage: ReleaseCleanupStage;
35
+ readonly trashPath: string | null;
36
+ readonly attempts: number;
37
+ readonly lastAttemptAt: string | null;
38
+ readonly lastError: string | null;
39
+ }
40
+ export interface ReleaseCleanupDiagnostic {
41
+ readonly releaseId: string;
42
+ readonly stage: string;
43
+ readonly attemptedAt: string;
44
+ readonly error: string;
45
+ }
46
+ export interface ReleaseCleanupState {
47
+ readonly pending: Readonly<Record<string, ReleaseCleanupDisposition>>;
48
+ readonly diagnostics: readonly ReleaseCleanupDiagnostic[];
49
+ }
21
50
  export interface CohortState {
22
51
  readonly schema: typeof RELEASE_COHORT_SCHEMA;
23
52
  readonly revision: number;
24
53
  readonly releases: Readonly<Record<string, ReleaseRecord>>;
25
54
  readonly references: ReleaseReferences;
55
+ readonly cleanup: ReleaseCleanupState;
26
56
  readonly activation: {
27
57
  readonly state: "idle" | "pending" | "draining" | "blocked" | "failed";
28
58
  readonly reason: string | null;
@@ -54,18 +84,48 @@ export interface SupervisorEndpointMetadata {
54
84
  readonly optionalFeatures: readonly string[];
55
85
  readonly contractDigest: string;
56
86
  }
87
+ export interface ProtectedReleaseInputs {
88
+ readonly liveReleaseIds?: readonly string[];
89
+ readonly externalHolds?: readonly ExternalReleaseHold[];
90
+ readonly transaction?: ActiveReleaseTransactionReference | null;
91
+ }
92
+ export interface ProtectedReleasePlan {
93
+ readonly protectedReleaseIds: readonly string[];
94
+ readonly retainedReleaseIds: readonly string[];
95
+ readonly collectibleReleaseIds: readonly string[];
96
+ }
97
+ export interface OrphanReleaseDisposition {
98
+ readonly release: ReleaseRecord;
99
+ readonly stage: ReleaseCleanupStage;
100
+ readonly trashPath?: string | null;
101
+ }
102
+ export interface ReleaseRetentionReconciliation {
103
+ readonly state: CohortState;
104
+ readonly plan: ProtectedReleasePlan;
105
+ readonly detachedReleaseIds: readonly string[];
106
+ }
107
+ /**
108
+ * Derive protection exclusively from current selectors and ownership authorities.
109
+ * The historical retention snapshot is deliberately not an input to this plan.
110
+ */
111
+ export declare function planProtectedReleases(state: CohortState, inputs?: ProtectedReleaseInputs): ProtectedReleasePlan;
57
112
  /** Serializes validated cohort-state revisions under a lock and commits each update atomically. */
58
113
  export declare class CohortStateStore {
59
114
  readonly path: string;
60
115
  constructor(dataDir: string);
61
116
  read(): Promise<CohortState>;
62
- update(operation: (current: CohortState) => CohortState): Promise<CohortState>;
117
+ update(operation: (current: CohortState) => CohortState | Promise<CohortState>): Promise<CohortState>;
63
118
  recordCandidate(release: MaterializedRelease): Promise<CohortState>;
64
119
  approve(releaseId: string, diagnosticsPath: string): Promise<CohortState>;
65
120
  activate(releaseId: string): Promise<CohortState>;
66
121
  blockPending(reason: string, blockerGenerationIds: readonly string[]): Promise<CohortState>;
67
122
  rollback(ownershipReleased: boolean): Promise<CohortState>;
68
123
  setRetention(releaseIds: readonly string[]): Promise<CohortState>;
124
+ /** Atomically replace legacy retention history and detach every currently collectible record. */
125
+ reconcileRetention(resolveInputs: (current: CohortState) => ProtectedReleaseInputs | Promise<ProtectedReleaseInputs>, orphans?: readonly OrphanReleaseDisposition[]): Promise<ReleaseRetentionReconciliation>;
126
+ markCleanupTrash(releaseId: string, trashPath: string): Promise<CohortState>;
127
+ recordCleanupFailure(releaseId: string, stage: string, error: unknown): Promise<CohortState>;
128
+ completeCleanup(releaseId: string): Promise<CohortState>;
69
129
  removeUnreferencedRelease(releaseId: string, externalReferences: readonly string[]): Promise<CohortState>;
70
130
  }
71
131
  export declare function emptyState(): CohortState;
@@ -1,8 +1,42 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { mkdir, open, readFile, rename, rm } from "node:fs/promises";
2
+ import { mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
3
3
  import { dirname, resolve } from "node:path";
4
4
  import { PRODUCT_IDENTITY } from "../../product-identity.js";
5
5
  export const RELEASE_COHORT_SCHEMA = PRODUCT_IDENTITY.protocol.releaseCohortSchema;
6
+ const RELEASE_ID_PATTERN = /^[0-9A-Za-z.+_-]+-[a-f0-9]{20}$/;
7
+ /**
8
+ * Derive protection exclusively from current selectors and ownership authorities.
9
+ * The historical retention snapshot is deliberately not an input to this plan.
10
+ */
11
+ export function planProtectedReleases(state, inputs = {}) {
12
+ const holds = inputs.externalHolds ?? [];
13
+ for (const hold of holds) {
14
+ if (hold.authority !== "agent" && hold.authority !== "migration")
15
+ throw new Error(`unknown release hold authority: ${String(hold.authority)}`);
16
+ if (!state.releases[hold.releaseId] && !state.cleanup.pending[hold.releaseId]) {
17
+ throw new Error(`external ${hold.authority} hold names unknown release ${hold.releaseId}`);
18
+ }
19
+ }
20
+ const protectedIds = new Set();
21
+ for (const id of [state.references.active, state.references.pending, state.references.approved, state.references.rollback]) {
22
+ if (id !== null)
23
+ protectedIds.add(id);
24
+ }
25
+ for (const id of inputs.liveReleaseIds ?? [])
26
+ protectedIds.add(id);
27
+ for (const hold of holds)
28
+ protectedIds.add(hold.releaseId);
29
+ if (inputs.transaction?.status === "active" && inputs.transaction.priorActiveReleaseId !== null) {
30
+ protectedIds.add(inputs.transaction.priorActiveReleaseId);
31
+ }
32
+ const protectedReleaseIds = [...protectedIds].sort();
33
+ const known = Object.keys(state.releases).sort();
34
+ return {
35
+ protectedReleaseIds,
36
+ retainedReleaseIds: known.filter(id => protectedIds.has(id)),
37
+ collectibleReleaseIds: known.filter(id => !protectedIds.has(id)),
38
+ };
39
+ }
6
40
  /** Serializes validated cohort-state revisions under a lock and commits each update atomically. */
7
41
  export class CohortStateStore {
8
42
  path;
@@ -11,7 +45,7 @@ export class CohortStateStore {
11
45
  }
12
46
  async read() {
13
47
  try {
14
- const state = JSON.parse(await readFile(this.path, "utf8"));
48
+ const state = normalizeState(JSON.parse(await readFile(this.path, "utf8")));
15
49
  validateState(state);
16
50
  return state;
17
51
  }
@@ -27,7 +61,7 @@ export class CohortStateStore {
27
61
  const lock = await acquireLock(lockPath);
28
62
  try {
29
63
  const current = await this.read();
30
- const next = operation(current);
64
+ const next = await operation(current);
31
65
  validateTransition(current, next);
32
66
  const committed = { ...next, revision: current.revision + 1 };
33
67
  const temporary = `${this.path}.${process.pid}.${randomUUID()}.tmp`;
@@ -66,7 +100,6 @@ export class CohortStateStore {
66
100
  references: {
67
101
  ...current.references,
68
102
  pending: current.references.active === release.releaseId ? current.references.pending : release.releaseId,
69
- retention: unique([...current.references.retention, release.releaseId]),
70
103
  },
71
104
  activation: activation("pending", "candidate awaits certification and safe activation"),
72
105
  };
@@ -91,11 +124,11 @@ export class CohortStateStore {
91
124
  return {
92
125
  ...current,
93
126
  references: {
127
+ ...current.references,
94
128
  active: releaseId,
95
129
  pending: null,
96
130
  approved: releaseId,
97
131
  rollback: prior && prior !== releaseId ? prior : current.references.rollback,
98
- retention: unique([...current.references.retention, releaseId, ...(prior ? [prior] : [])]),
99
132
  },
100
133
  activation: activation("idle", null),
101
134
  };
@@ -122,7 +155,6 @@ export class CohortStateStore {
122
155
  pending: null,
123
156
  approved: rollbackId,
124
157
  rollback: current.references.active,
125
- retention: unique([...current.references.retention, rollbackId, ...(current.references.active ? [current.references.active] : [])]),
126
158
  },
127
159
  activation: activation("idle", null),
128
160
  };
@@ -135,9 +167,74 @@ export class CohortStateStore {
135
167
  return { ...current, references: { ...current.references, retention: unique(releaseIds) } };
136
168
  });
137
169
  }
170
+ /** Atomically replace legacy retention history and detach every currently collectible record. */
171
+ async reconcileRetention(resolveInputs, orphans = []) {
172
+ let plan = null;
173
+ let detachedReleaseIds = [];
174
+ const state = await this.update(async (current) => {
175
+ const inputs = await resolveInputs(current);
176
+ plan = planProtectedReleases(current, inputs);
177
+ const nextReleases = { ...current.releases };
178
+ const pending = { ...current.cleanup.pending };
179
+ for (const releaseId of plan.collectibleReleaseIds) {
180
+ const release = nextReleases[releaseId];
181
+ pending[releaseId] ??= cleanupDisposition(release, "detached", null);
182
+ delete nextReleases[releaseId];
183
+ }
184
+ for (const orphan of orphans) {
185
+ const releaseId = orphan.release.releaseId;
186
+ if (nextReleases[releaseId] || pending[releaseId] || plan.protectedReleaseIds.includes(releaseId))
187
+ continue;
188
+ pending[releaseId] = cleanupDisposition(orphan.release, orphan.stage, orphan.trashPath ?? null);
189
+ }
190
+ detachedReleaseIds = plan.collectibleReleaseIds;
191
+ return {
192
+ ...current,
193
+ releases: nextReleases,
194
+ references: { ...current.references, retention: plan.retainedReleaseIds },
195
+ cleanup: { ...current.cleanup, pending },
196
+ };
197
+ });
198
+ if (plan === null)
199
+ throw new Error("release retention reconciliation did not produce a plan");
200
+ return { state, plan, detachedReleaseIds };
201
+ }
202
+ async markCleanupTrash(releaseId, trashPath) {
203
+ return await this.update(current => {
204
+ const disposition = current.cleanup.pending[releaseId];
205
+ if (!disposition)
206
+ throw new Error(`release ${releaseId} has no cleanup disposition`);
207
+ return replaceCleanup(current, releaseId, { ...disposition, stage: "trash", trashPath, lastError: null });
208
+ });
209
+ }
210
+ async recordCleanupFailure(releaseId, stage, error) {
211
+ return await this.update(current => {
212
+ const attemptedAt = new Date().toISOString();
213
+ const message = boundedError(error);
214
+ const disposition = current.cleanup.pending[releaseId];
215
+ const pending = disposition ? {
216
+ ...current.cleanup.pending,
217
+ [releaseId]: { ...disposition, attempts: disposition.attempts + 1, lastAttemptAt: attemptedAt, lastError: message },
218
+ } : current.cleanup.pending;
219
+ return {
220
+ ...current,
221
+ cleanup: {
222
+ pending,
223
+ diagnostics: [...current.cleanup.diagnostics, { releaseId, stage, attemptedAt, error: message }].slice(-64),
224
+ },
225
+ };
226
+ });
227
+ }
228
+ async completeCleanup(releaseId) {
229
+ return await this.update(current => {
230
+ const pending = { ...current.cleanup.pending };
231
+ delete pending[releaseId];
232
+ return { ...current, cleanup: { ...current.cleanup, pending } };
233
+ });
234
+ }
138
235
  async removeUnreferencedRelease(releaseId, externalReferences) {
139
236
  return await this.update(current => {
140
- requiredRelease(current, releaseId);
237
+ const release = requiredRelease(current, releaseId);
141
238
  const protectedIds = new Set([
142
239
  current.references.active,
143
240
  current.references.pending,
@@ -150,7 +247,14 @@ export class CohortStateStore {
150
247
  throw new Error(`release ${releaseId} is still referenced and cannot be collected`);
151
248
  const releases = { ...current.releases };
152
249
  delete releases[releaseId];
153
- return { ...current, releases };
250
+ return {
251
+ ...current,
252
+ releases,
253
+ cleanup: {
254
+ ...current.cleanup,
255
+ pending: { ...current.cleanup.pending, [releaseId]: cleanupDisposition(release, "detached", null) },
256
+ },
257
+ };
154
258
  });
155
259
  }
156
260
  }
@@ -160,9 +264,16 @@ export function emptyState() {
160
264
  revision: 0,
161
265
  releases: {},
162
266
  references: { active: null, pending: null, approved: null, rollback: null, retention: [] },
267
+ cleanup: { pending: {}, diagnostics: [] },
163
268
  activation: activation("idle", null),
164
269
  };
165
270
  }
271
+ function cleanupDisposition(release, stage, trashPath) {
272
+ return { release, stage, trashPath, attempts: 0, lastAttemptAt: null, lastError: null };
273
+ }
274
+ function replaceCleanup(current, releaseId, disposition) {
275
+ return { ...current, cleanup: { ...current.cleanup, pending: { ...current.cleanup.pending, [releaseId]: disposition } } };
276
+ }
166
277
  function activation(state, reason) {
167
278
  return { state, reason, blockerGenerationIds: [], updatedAt: new Date().toISOString() };
168
279
  }
@@ -177,22 +288,89 @@ async function acquireLock(path) {
177
288
  const deadline = Date.now() + 5_000;
178
289
  while (true) {
179
290
  try {
180
- return await open(path, "wx", 0o600);
291
+ const lock = await open(path, "wx", 0o600);
292
+ await lock.writeFile(JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }));
293
+ await lock.sync();
294
+ return lock;
181
295
  }
182
296
  catch (error) {
183
- if (!(error instanceof Error && "code" in error && error.code === "EEXIST") || Date.now() >= deadline)
297
+ if (!(error instanceof Error && "code" in error && error.code === "EEXIST"))
298
+ throw error;
299
+ if (await reclaimAbandonedLock(path))
300
+ continue;
301
+ if (Date.now() >= deadline)
184
302
  throw error;
185
303
  await new Promise(resolvePromise => setTimeout(resolvePromise, 20));
186
304
  }
187
305
  }
188
306
  }
307
+ async function reclaimAbandonedLock(path) {
308
+ let abandoned = false;
309
+ try {
310
+ const value = JSON.parse(await readFile(path, "utf8"));
311
+ if (typeof value.pid === "number") {
312
+ try {
313
+ process.kill(value.pid, 0);
314
+ }
315
+ catch (error) {
316
+ abandoned = !(error instanceof Error && "code" in error && error.code === "EPERM");
317
+ }
318
+ }
319
+ }
320
+ catch {
321
+ const metadata = await stat(path).catch(() => null);
322
+ abandoned = metadata !== null && Date.now() - metadata.mtimeMs > 10_000;
323
+ }
324
+ if (!abandoned)
325
+ return false;
326
+ const quarantine = `${path}.abandoned`;
327
+ const oldQuarantine = await stat(quarantine).catch(() => null);
328
+ if (oldQuarantine && Date.now() - oldQuarantine.mtimeMs > 10_000)
329
+ await rm(quarantine, { force: true });
330
+ try {
331
+ // Concurrency: the fixed destination lets exactly one waiter claim the stale lock;
332
+ // another waiter cannot accidentally rename a newly acquired replacement.
333
+ await rename(path, quarantine);
334
+ await rm(quarantine, { force: true });
335
+ return true;
336
+ }
337
+ catch {
338
+ return false;
339
+ }
340
+ }
341
+ function normalizeState(value) {
342
+ if (!value || typeof value !== "object")
343
+ return value;
344
+ const state = value;
345
+ return {
346
+ ...state,
347
+ cleanup: state.cleanup ?? { pending: {}, diagnostics: [] },
348
+ };
349
+ }
189
350
  function validateState(state) {
190
- if (state.schema !== RELEASE_COHORT_SCHEMA || !Number.isSafeInteger(state.revision) || state.revision < 0)
351
+ if (!state || state.schema !== RELEASE_COHORT_SCHEMA || !Number.isSafeInteger(state.revision) || state.revision < 0
352
+ || !state.releases || typeof state.releases !== "object" || !state.references || !Array.isArray(state.references.retention)
353
+ || !state.cleanup || typeof state.cleanup.pending !== "object" || !Array.isArray(state.cleanup.diagnostics)) {
191
354
  throw new Error("invalid release cohort state");
355
+ }
356
+ for (const [releaseId, release] of Object.entries(state.releases))
357
+ validateReleaseRecord(releaseId, release);
192
358
  for (const reference of [state.references.active, state.references.pending, state.references.approved, state.references.rollback]) {
193
359
  if (reference !== null && !state.releases[reference])
194
360
  throw new Error(`release reference points to an unknown release: ${reference}`);
195
361
  }
362
+ for (const [releaseId, disposition] of Object.entries(state.cleanup.pending)) {
363
+ validateReleaseRecord(releaseId, disposition.release);
364
+ if (!["detached", "trash"].includes(disposition.stage))
365
+ throw new Error(`invalid cleanup disposition for ${releaseId}`);
366
+ }
367
+ }
368
+ function validateReleaseRecord(releaseId, release) {
369
+ if (release.releaseId !== releaseId || !RELEASE_ID_PATTERN.test(releaseId) || typeof release.releaseRoot !== "string"
370
+ || typeof release.packageVersion !== "string" || !/^[a-f0-9]{64}$/.test(release.contentDigest)
371
+ || !["candidate", "approved", "rejected"].includes(release.approval)) {
372
+ throw new Error(`invalid release record for ${releaseId}`);
373
+ }
196
374
  }
197
375
  function validateTransition(current, next) {
198
376
  validateState(next);
@@ -201,3 +379,6 @@ function validateTransition(current, next) {
201
379
  if (next.revision !== current.revision)
202
380
  throw new Error("release cohort revision is controlled by the state store");
203
381
  }
382
+ function boundedError(error) {
383
+ return (error instanceof Error ? error.message : String(error)).replace(/[\r\n]+/g, " ").slice(0, 1_000);
384
+ }
@@ -0,0 +1,78 @@
1
+ import { type ReleaseFileIdentity } from "./release.js";
2
+ import { PRODUCT_IDENTITY } from "../../product-identity.js";
3
+ export declare const DEPENDENCY_LAYER_SCHEMA: string;
4
+ export declare const DEPENDENCY_LAYER_MANIFEST = "dependency-layer-manifest.json";
5
+ export declare const RUNTIME_PAYLOAD_INVENTORY = "dist/runtime-payload-inventory.json";
6
+ export interface RuntimePayloadExclusion {
7
+ readonly path: string;
8
+ readonly reason: "type-declaration" | "source-map" | "unreachable-development";
9
+ }
10
+ export interface RuntimePayloadInventory {
11
+ readonly schema: typeof PRODUCT_IDENTITY.evidence.runtimePayloadSchema;
12
+ readonly includedFiles: number;
13
+ readonly includedBytes: number;
14
+ readonly excludedFiles: number;
15
+ readonly excludedBytes: number;
16
+ readonly uncertaintyRetainedFiles: number;
17
+ readonly exclusions: readonly RuntimePayloadExclusion[];
18
+ }
19
+ export interface DependencyLayerIdentity {
20
+ readonly schema: typeof DEPENDENCY_LAYER_SCHEMA;
21
+ readonly layerId: string;
22
+ readonly contentDigest: string;
23
+ readonly files: readonly ReleaseFileIdentity[];
24
+ readonly inventory: RuntimePayloadInventory;
25
+ }
26
+ export interface DependencyLayerReference {
27
+ readonly layerId: string;
28
+ readonly contentDigest: string;
29
+ readonly binding: "node_modules";
30
+ }
31
+ export interface MaterializedDependencyLayer extends DependencyLayerIdentity {
32
+ readonly layerRoot: string;
33
+ readonly reused: boolean;
34
+ }
35
+ export interface DependencyLayerOperationEvent {
36
+ readonly operation: "source-read" | "layer-write" | "layer-reuse" | "verification-read";
37
+ readonly path: string;
38
+ readonly bytes: number;
39
+ }
40
+ export interface SelectedRuntimePayload {
41
+ readonly paths: readonly string[];
42
+ readonly inventory: RuntimePayloadInventory;
43
+ }
44
+ export interface PublishedRuntimeFileClassification {
45
+ readonly packageName: string;
46
+ readonly packageVersion: string;
47
+ readonly packagePath: string;
48
+ readonly disposition: "include" | "exclude";
49
+ readonly reason?: RuntimePayloadExclusion["reason"];
50
+ }
51
+ export interface GeneratedRuntimePayload extends SelectedRuntimePayload {
52
+ readonly classifications: readonly PublishedRuntimeFileClassification[];
53
+ }
54
+ export interface PublishedRuntimePayload extends GeneratedRuntimePayload {
55
+ readonly schema: typeof PRODUCT_IDENTITY.evidence.runtimePayloadSchema;
56
+ readonly entryPoints: readonly string[];
57
+ readonly declaredAssets: readonly string[];
58
+ }
59
+ /** Consume topology-independent publication classifications, retaining unknown installed content. */
60
+ export declare function selectPublishedDependencyRuntimePayload(packageRoot: string, paths: readonly string[]): Promise<SelectedRuntimePayload>;
61
+ /** Generate publication evidence from literal module reachability and declared runtime assets. */
62
+ export declare function generateDependencyRuntimePayload(packageRoot: string, allPaths: readonly string[], productPaths: readonly string[]): Promise<GeneratedRuntimePayload>;
63
+ /** Conservatively omit only files Node never executes; uncertain assets remain selected. */
64
+ export declare function selectDependencyRuntimePayload(packageRoot: string, paths: readonly string[]): Promise<SelectedRuntimePayload>;
65
+ export interface MaterializeDependencyLayerOptions {
66
+ readonly inventory: RuntimePayloadInventory;
67
+ readonly cachedFiles?: ReadonlyMap<string, Buffer>;
68
+ readonly onOperation?: (event: DependencyLayerOperationEvent) => void;
69
+ readonly writeCandidateFile?: (path: string, bytes: Uint8Array, mode: number) => Promise<void>;
70
+ }
71
+ /** Materialize or reuse an exact immutable dependency layer after one source-content pass. */
72
+ export declare function materializeDependencyLayer(packageRoot: string, dataDir: string, paths: readonly string[], options: MaterializeDependencyLayerOptions): Promise<MaterializedDependencyLayer | null>;
73
+ /** Read trusted layer certification and canonical metadata without rereading every payload byte. */
74
+ export declare function readCertifiedDependencyLayer(dataDir: string, layerId: string, expected?: Pick<DependencyLayerIdentity, "layerId" | "contentDigest">): Promise<Omit<MaterializedDependencyLayer, "reused">>;
75
+ /** Fully verify a layer when certification is absent or explicit tamper evidence is required. */
76
+ export declare function verifyDependencyLayer(dataDir: string, reference: DependencyLayerReference, onOperation?: (event: DependencyLayerOperationEvent) => void): Promise<Omit<MaterializedDependencyLayer, "reused">>;
77
+ export declare function dependencyReference(layer: MaterializedDependencyLayer): DependencyLayerReference;
78
+ export declare function dependencyLayerCertificationPath(dataDir: string, layerId: string): string;