vouchington-tooling 0.11.1 → 0.12.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.
package/README.md CHANGED
@@ -215,6 +215,7 @@ import {
215
215
  import { runRetrospectiveTranscript } from 'vouchington-tooling/retrospective-transcript'
216
216
  import { appendJournal, probeBlackboard } from 'vouchington-tooling/agent-blackboard'
217
217
  import { buildSessionFrictionReport, recordFriction } from 'vouchington-tooling/session-friction'
218
+ import { GITHUB_BODY_MAX_CHARACTERS, validateGitHubBodyLength } from 'vouchington-tooling'
218
219
  import { createPullRequest, getDiffAgainstBase, runGh, runGit } from 'vouchington-tooling/gh-cli'
219
220
  import {
220
221
  shellScriptViolations,
@@ -231,6 +232,10 @@ throws on a `run:` value that is a YAML alias or a multiline PLAIN scalar, shape
231
232
  scan safely. Both functions scan already-in-scope source text — deciding which files count as a
232
233
  shell script or a workflow/action YAML file is left to the caller.
233
234
 
235
+ `validateGitHubBodyLength` measures an issue or pull-request body against GitHub's 65,536 Unicode
236
+ code-point limit without altering it. Its result includes the UTF-8 byte count for diagnostics; that
237
+ byte count is not a validation limit.
238
+
234
239
  `agent-harness-config` merges classifier-auto and sandbox keys into Claude, Codex, Grok, and Cursor
235
240
  config files. See [docs/agent-harness-config.md](./docs/agent-harness-config.md). `--global` updates
236
241
  home-directory configs; `--repo` updates a checkout. It does not copy allowlists, hooks, or plugins.
@@ -0,0 +1,15 @@
1
+ /** GitHub's maximum issue or pull-request body length, counted in Unicode code points. */
2
+ export declare const GITHUB_BODY_MAX_CHARACTERS = 65536;
3
+ /** The measured GitHub body length and whether it fits within GitHub's character limit. */
4
+ export interface GitHubBodyLengthValidation {
5
+ readonly ok: boolean;
6
+ readonly characterCount: number;
7
+ readonly utf8ByteCount: number;
8
+ readonly maxCharacterCount: number;
9
+ }
10
+ /**
11
+ * Measures a GitHub issue or pull-request body without altering its content.
12
+ *
13
+ * GitHub's limit is evaluated in Unicode code points. UTF-8 bytes are returned only for diagnostics.
14
+ */
15
+ export declare function validateGitHubBodyLength(body: string): GitHubBodyLengthValidation;
@@ -0,0 +1,16 @@
1
+ /** GitHub's maximum issue or pull-request body length, counted in Unicode code points. */
2
+ export const GITHUB_BODY_MAX_CHARACTERS = 65_536;
3
+ /**
4
+ * Measures a GitHub issue or pull-request body without altering its content.
5
+ *
6
+ * GitHub's limit is evaluated in Unicode code points. UTF-8 bytes are returned only for diagnostics.
7
+ */
8
+ export function validateGitHubBodyLength(body) {
9
+ const characterCount = Array.from(body).length;
10
+ return {
11
+ ok: characterCount <= GITHUB_BODY_MAX_CHARACTERS,
12
+ characterCount,
13
+ utf8ByteCount: Buffer.byteLength(body, 'utf8'),
14
+ maxCharacterCount: GITHUB_BODY_MAX_CHARACTERS,
15
+ };
16
+ }
@@ -1,5 +1,7 @@
1
1
  export { createCommandRunner, runGh, runGit } from './exec.mts';
2
2
  export type { ExecFileText, RunTextCommand } from './exec.mts';
3
3
  export { getDiffAgainstBase } from './diff.mts';
4
+ export { GITHUB_BODY_MAX_CHARACTERS, validateGitHubBodyLength } from './body-length.mts';
5
+ export type { GitHubBodyLengthValidation } from './body-length.mts';
4
6
  export { assertHeadPushed, buildGhPrCreateArgs, createPullRequest, DetachedHeadError, HeadNotPushedError, HeadOutOfDateError, resolveHeadBranch, } from './pr-create.mts';
5
7
  export type { AssertHeadPushedOptions, BuildGhPrCreateArgsOptions, CreatePullRequestExecutors, CreatePullRequestOptions, } from './pr-create.mts';
@@ -1,3 +1,4 @@
1
1
  export { createCommandRunner, runGh, runGit } from './exec.mjs';
2
2
  export { getDiffAgainstBase } from './diff.mjs';
3
+ export { GITHUB_BODY_MAX_CHARACTERS, validateGitHubBodyLength } from './body-length.mjs';
3
4
  export { assertHeadPushed, buildGhPrCreateArgs, createPullRequest, DetachedHeadError, HeadNotPushedError, HeadOutOfDateError, resolveHeadBranch, } from './pr-create.mjs';
package/dist/index.d.mts CHANGED
@@ -89,5 +89,7 @@ export type { DiagnosticReportLimitOptions, DiagnosticReportSummary, } from './v
89
89
  export { createCommandRunner, runGh, runGit } from './gh-cli/index.mts';
90
90
  export type { ExecFileText, RunTextCommand } from './gh-cli/index.mts';
91
91
  export { getDiffAgainstBase } from './gh-cli/index.mts';
92
+ export { GITHUB_BODY_MAX_CHARACTERS, validateGitHubBodyLength } from './gh-cli/index.mts';
93
+ export type { GitHubBodyLengthValidation } from './gh-cli/index.mts';
92
94
  export { assertHeadPushed, buildGhPrCreateArgs, createPullRequest, DetachedHeadError, HeadNotPushedError, HeadOutOfDateError, resolveHeadBranch, } from './gh-cli/index.mts';
93
95
  export type { AssertHeadPushedOptions, BuildGhPrCreateArgsOptions, CreatePullRequestExecutors, CreatePullRequestOptions, } from './gh-cli/index.mts';
package/dist/index.mjs CHANGED
@@ -50,4 +50,5 @@ export { validateResolvedPinDelta } from './swift-resolved-pin-delta/index.mjs';
50
50
  export { DEFAULT_MAX_DIAGNOSTIC_REPORTS, DEFAULT_MAX_FORMATTED_DIAGNOSTIC_REPORTS, formatDiagnosticReportSummaries, HARD_MAX_DIAGNOSTIC_REPORTS, readDiagnosticReportSummaries, summarizeDiagnosticReport, } from './vitest-diagnostics/index.mjs';
51
51
  export { createCommandRunner, runGh, runGit } from './gh-cli/index.mjs';
52
52
  export { getDiffAgainstBase } from './gh-cli/index.mjs';
53
+ export { GITHUB_BODY_MAX_CHARACTERS, validateGitHubBodyLength } from './gh-cli/index.mjs';
53
54
  export { assertHeadPushed, buildGhPrCreateArgs, createPullRequest, DetachedHeadError, HeadNotPushedError, HeadOutOfDateError, resolveHeadBranch, } from './gh-cli/index.mjs';
@@ -1,5 +1,6 @@
1
1
  import { type CommandResult, type InstallOptions } from './support.mts';
2
2
  export declare function withScriptPolicy(args: string[], installScripts: boolean): string[];
3
3
  export declare function install(args: string[], options: InstallOptions, label: string): Promise<void>;
4
- export declare function reconcileOrFail(options: InstallOptions, runCapture: (args: string[]) => Promise<CommandResult>): Promise<void>;
4
+ export declare function reconcileOrFail(options: InstallOptions): Promise<void>;
5
+ export declare function finalizePendingBuilds(options: InstallOptions, runCapture: (args: string[]) => Promise<CommandResult>, phase: string): Promise<import("./pending-builds.mts").PendingBuildState>;
5
6
  export declare function repairIsolatedNativeMismatch(options: InstallOptions, runCapture: (args: string[]) => Promise<CommandResult>, mismatchedNativePaths: string[]): Promise<boolean>;
@@ -1,7 +1,7 @@
1
1
  import { scheduler } from 'node:timers/promises';
2
2
  import { runPnpm } from './exec.mjs';
3
3
  import { nativeBinariesMatchRuntime, repairedNativeBinariesMatchRuntime } from './native-health.mjs';
4
- import { buildLedgersAllowNativeRepair } from './pending-builds.mjs';
4
+ import { buildLedgersAllowNativeRepair, pendingBuilds } from './pending-builds.mjs';
5
5
  import { INSTALL_TERMINATION_FAILED } from './process.mjs';
6
6
  import { formatReleaseAgeFailure, isReleaseAgeViolation } from './release-age.mjs';
7
7
  import { findWorkspaceLinkMismatches, forcedInstallArgs, logWorkspaceLinkMismatches, } from './support.mjs';
@@ -28,10 +28,9 @@ export async function install(args, options, label) {
28
28
  }
29
29
  fail(`${label} failed after ${options.maxAttempts} attempt${options.maxAttempts === 1 ? '' : 's'}`);
30
30
  }
31
- export async function reconcileOrFail(options, runCapture) {
31
+ export async function reconcileOrFail(options) {
32
32
  await install([...forcedInstallArgs, '--ignore-scripts', '--ignore-pnpmfile'], options, 'script-free reconciliation');
33
- await install(withScriptPolicy(forcedInstallArgs, options.installScripts), options, 'strict persistent reconciliation');
34
- await verifyInstallHealth(runCapture, 'persistent reconciliation');
33
+ await install([...forcedInstallArgs, '--ignore-scripts'], options, 'second script-free persistent reconciliation');
35
34
  }
36
35
  async function verifyInstallHealth(runCapture, phase, repairedNativePaths = []) {
37
36
  const [nativesMatch, remaining] = await Promise.all([
@@ -47,6 +46,19 @@ async function verifyInstallHealth(runCapture, phase, repairedNativePaths = [])
47
46
  fail(`${phase} completed with invalid workspace links`);
48
47
  }
49
48
  }
49
+ export async function finalizePendingBuilds(options, runCapture, phase) {
50
+ const before = await pendingBuilds();
51
+ if (options.installScripts && before.kind === 'unknown')
52
+ fail(`${phase} completed without a clear pending build ledger`);
53
+ if (options.installScripts && before.kind === 'pending') {
54
+ await install(['rebuild', '--pending', '--recursive'], options, 'pending scripts rebuild');
55
+ const after = await pendingBuilds();
56
+ if (after.kind !== 'clear')
57
+ fail(`${phase} completed without a clear pending build ledger`);
58
+ }
59
+ await verifyInstallHealth(runCapture, phase);
60
+ return options.installScripts ? { kind: 'clear' } : before;
61
+ }
50
62
  export async function repairIsolatedNativeMismatch(options, runCapture, mismatchedNativePaths) {
51
63
  if (!(await buildLedgersAllowNativeRepair()))
52
64
  return false;
@@ -3,7 +3,7 @@ import { type ProvenanceStatus } from './transition.mts';
3
3
  declare const componentNames: readonly ['lockfile', 'npm-config', 'pnpm', 'pnpmfiles', 'runtime', 'workspace-config', 'workspace-manifests'];
4
4
  type ComponentName = (typeof componentNames)[number];
5
5
  type StructuralProvenance = Record<ComponentName, string>;
6
- export declare function persistentMetadataFingerprintV4(runCapture: CaptureCommand): Promise<{
6
+ export declare function persistentMetadataFingerprintV5(runCapture: CaptureCommand): Promise<{
7
7
  lockfile: string;
8
8
  'npm-config': string;
9
9
  pnpm: string;
@@ -12,7 +12,7 @@ export declare function persistentMetadataFingerprintV4(runCapture: CaptureComma
12
12
  'workspace-config': string;
13
13
  'workspace-manifests': string;
14
14
  }>;
15
- export declare function persistentMetadataStatusV4(provenance: StructuralProvenance): Promise<ProvenanceStatus>;
16
- export declare function writePersistentMetadataStampV4(provenance: StructuralProvenance, installScripts: boolean, resetScriptsEnabledCapability: boolean, pendingDependencyBuilds?: string[]): Promise<void>;
15
+ export declare function persistentMetadataStatusV5(provenance: StructuralProvenance): Promise<ProvenanceStatus>;
16
+ export declare function writePersistentMetadataStampV5(provenance: StructuralProvenance, installScripts: boolean, scriptsEnabledInstallVerified: boolean): Promise<void>;
17
17
  export declare function persistentDependencyTreeIsCold(): Promise<boolean>;
18
18
  export {};
@@ -47,7 +47,7 @@ function runtimePlatformIdentity() {
47
47
  platform: process.platform,
48
48
  });
49
49
  }
50
- export async function persistentMetadataFingerprintV4(runCapture) {
50
+ export async function persistentMetadataFingerprintV5(runCapture) {
51
51
  const workspaces = (await listWorkspaces(runCapture)).toSorted((left, right) => left.path.localeCompare(right.path));
52
52
  const pnpmVersion = await runCapture(['--version']);
53
53
  if (pnpmVersion.code !== 0)
@@ -75,15 +75,12 @@ function validStamp(value) {
75
75
  return false;
76
76
  // oxlint-disable-next-line no-mistakes/ts-no-const-aliases -- establish the candidate stamp type before complete field validation
77
77
  const stamp = value;
78
- return (stamp.version === 4 &&
78
+ return (stamp.version === 5 &&
79
79
  typeof stamp.lastInvocationInstallScripts === 'boolean' &&
80
- typeof stamp.scriptsEnabledInstallSucceeded === 'boolean' &&
80
+ typeof stamp.scriptsEnabledInstallVerified === 'boolean' &&
81
81
  typeof stamp.provenance === 'object' &&
82
82
  stamp.provenance !== null &&
83
- componentNames.every((name) => typeof stamp.provenance?.[name] === 'string') &&
84
- (stamp.pendingDependencyBuilds === undefined ||
85
- (Array.isArray(stamp.pendingDependencyBuilds) &&
86
- stamp.pendingDependencyBuilds.every((id) => typeof id === 'string'))));
83
+ componentNames.every((name) => typeof stamp.provenance?.[name] === 'string'));
87
84
  }
88
85
  async function readPersistentMetadataState() {
89
86
  try {
@@ -98,11 +95,7 @@ async function readPersistentMetadataState() {
98
95
  : { kind: 'unsafe' };
99
96
  }
100
97
  }
101
- async function readPersistentMetadataStamp() {
102
- const state = await readPersistentMetadataState();
103
- return state.kind === 'stamp' ? state.stamp : undefined;
104
- }
105
- export async function persistentMetadataStatusV4(provenance) {
98
+ export async function persistentMetadataStatusV5(provenance) {
106
99
  const state = await readPersistentMetadataState();
107
100
  if (state.kind === 'missing')
108
101
  return { kind: 'absent' };
@@ -115,23 +108,15 @@ export async function persistentMetadataStatusV4(provenance) {
115
108
  : {
116
109
  kind: 'matching',
117
110
  lastInvocationInstallScripts: stamp.lastInvocationInstallScripts,
118
- pendingDependencyBuilds: stamp.pendingDependencyBuilds ?? [],
119
- scriptsEnabledInstallSucceeded: stamp.scriptsEnabledInstallSucceeded,
111
+ scriptsEnabledInstallVerified: stamp.scriptsEnabledInstallVerified,
120
112
  };
121
113
  }
122
- export async function writePersistentMetadataStampV4(provenance, installScripts, resetScriptsEnabledCapability, pendingDependencyBuilds) {
123
- const existing = await readPersistentMetadataStamp();
124
- const existingMatches = existing && componentNames.every((name) => existing.provenance[name] === provenance[name]);
114
+ export async function writePersistentMetadataStampV5(provenance, installScripts, scriptsEnabledInstallVerified) {
125
115
  const stamp = {
126
116
  lastInvocationInstallScripts: installScripts,
127
- pendingDependencyBuilds: (pendingDependencyBuilds ??
128
- (existingMatches ? existing.pendingDependencyBuilds : undefined) ??
129
- []).toSorted(),
130
117
  provenance,
131
- scriptsEnabledInstallSucceeded: installScripts ||
132
- (!resetScriptsEnabledCapability &&
133
- Boolean(existingMatches && existing.scriptsEnabledInstallSucceeded)),
134
- version: 4,
118
+ scriptsEnabledInstallVerified,
119
+ version: 5,
135
120
  };
136
121
  const stampPath = persistentMetadataStampPath();
137
122
  const directory = path.dirname(stampPath);
@@ -1,18 +1,10 @@
1
- export type PendingBuilds = {
2
- kind: 'known';
3
- ids: Set<string>;
1
+ export type PendingBuildState = {
2
+ kind: 'clear';
4
3
  } | {
5
- kind: 'unknown';
6
- };
7
- export type PendingBuildDelta = {
8
- kind: 'unknown';
4
+ ids: string[];
5
+ kind: 'pending';
9
6
  } | {
10
- kind: 'known';
11
- dependencyIds: string[];
12
- workspaceIds: string[];
7
+ kind: 'unknown';
13
8
  };
14
- export declare function pendingBuilds(): Promise<PendingBuilds>;
9
+ export declare function pendingBuilds(): Promise<PendingBuildState>;
15
10
  export declare function buildLedgersAllowNativeRepair(): Promise<boolean>;
16
- export declare function validDependencyBuildIds(ids: [string, ...string[]]): Promise<[string, ...string[]] | undefined>;
17
- export declare function clearPendingDependencyBuilds(ids: string[]): Promise<boolean>;
18
- export declare function pendingBuildDelta(before: PendingBuilds, after: PendingBuilds): Promise<PendingBuildDelta>;
@@ -1,106 +1,36 @@
1
- import { readFile, rename, rm, writeFile } from 'node:fs/promises';
1
+ import { readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { parse, stringify } from 'yaml';
4
- export async function pendingBuilds() {
5
- try {
6
- const value = parse(await readFile(path.join(process.cwd(), 'node_modules', '.modules.yaml'), 'utf8'));
7
- if (typeof value !== 'object' || value === null || Array.isArray(value))
8
- return { kind: 'unknown' };
9
- const pending = value.pendingBuilds ?? [];
10
- if (!Array.isArray(pending) || !pending.every((id) => typeof id === 'string'))
11
- return { kind: 'unknown' };
12
- return { kind: 'known', ids: new Set(pending) };
13
- }
14
- catch {
15
- return { kind: 'unknown' };
16
- }
17
- }
18
- export async function buildLedgersAllowNativeRepair() {
3
+ import { parse } from 'yaml';
4
+ async function buildLedgers() {
19
5
  try {
20
6
  const value = parse(await readFile(path.join(process.cwd(), 'node_modules', '.modules.yaml'), 'utf8'));
21
7
  if (typeof value !== 'object' || value === null || Array.isArray(value))
22
- return false;
23
- // oxlint-disable-next-line no-mistakes/ts-no-const-aliases -- establish a record view after validating the pnpm manifest object
24
- const record = value;
25
- const ignored = record.ignoredBuilds;
26
- const pending = record.pendingBuilds ?? [];
27
- return (Array.isArray(ignored) &&
28
- ignored.length === 0 &&
29
- Array.isArray(pending) &&
30
- pending.length === 0);
31
- }
32
- catch {
33
- return false;
34
- }
35
- }
36
- async function lockfileDependencyIds() {
37
- try {
38
- const lockfile = parse(await readFile(path.join(process.cwd(), 'pnpm-lock.yaml'), 'utf8'));
39
- if (typeof lockfile !== 'object' || lockfile === null || !('packages' in lockfile))
40
8
  return undefined;
41
- const packages = lockfile.packages;
42
- if (typeof packages !== 'object' || packages === null || Array.isArray(packages))
9
+ // oxlint-disable-next-line no-mistakes/ts-no-const-aliases -- validate the parsed YAML object before reading its fields
10
+ const record = value;
11
+ const ignored = Object.hasOwn(record, 'ignoredBuilds') ? record.ignoredBuilds : undefined;
12
+ const pending = Object.hasOwn(record, 'pendingBuilds') ? record.pendingBuilds : [];
13
+ if ((ignored !== undefined &&
14
+ (!Array.isArray(ignored) || !ignored.every((id) => typeof id === 'string'))) ||
15
+ !Array.isArray(pending) ||
16
+ !pending.every((id) => typeof id === 'string'))
43
17
  return undefined;
44
- return new Set(Object.keys(packages));
18
+ return {
19
+ ignoredBuilds: ignored,
20
+ pendingBuilds: pending.length === 0 ? { kind: 'clear' } : { ids: pending.toSorted(), kind: 'pending' },
21
+ };
45
22
  }
46
23
  catch {
47
24
  return undefined;
48
25
  }
49
26
  }
50
- export async function validDependencyBuildIds(ids) {
51
- const packages = await lockfileDependencyIds();
52
- if (!packages || !ids.every((id) => packages.has(id)))
53
- return undefined;
54
- const [first, ...remaining] = ids.toSorted();
55
- return [first, ...remaining];
56
- }
57
- export async function clearPendingDependencyBuilds(ids) {
58
- const filename = path.join(process.cwd(), 'node_modules', '.modules.yaml');
59
- try {
60
- const value = parse(await readFile(filename, 'utf8'));
61
- if (typeof value !== 'object' || value === null || Array.isArray(value))
62
- return false;
63
- // oxlint-disable-next-line no-mistakes/ts-no-const-aliases -- establish a record view after validating the pnpm manifest object
64
- const record = value;
65
- if (!Array.isArray(record.pendingBuilds) ||
66
- !record.pendingBuilds.every((id) => typeof id === 'string'))
67
- return false;
68
- const ignored = record.ignoredBuilds ?? [];
69
- if (!Array.isArray(ignored) ||
70
- !ignored.every((id) => typeof id === 'string') ||
71
- ids.some((id) => ignored.includes(id)))
72
- return false;
73
- record.pendingBuilds = record.pendingBuilds.filter((id) => !ids.includes(id));
74
- const temporary = `${filename}.${process.pid}.tmp`;
75
- try {
76
- await writeFile(temporary, stringify(record));
77
- await rename(temporary, filename);
78
- }
79
- finally {
80
- await rm(temporary, { force: true });
81
- }
82
- return true;
83
- }
84
- catch {
85
- return false;
86
- }
27
+ export async function pendingBuilds() {
28
+ return (await buildLedgers())?.pendingBuilds ?? { kind: 'unknown' };
87
29
  }
88
- export async function pendingBuildDelta(before, after) {
89
- if (before.kind !== 'known' || after.kind !== 'known')
90
- return { kind: 'unknown' };
91
- const packages = await lockfileDependencyIds();
92
- if (!packages)
93
- return { kind: 'unknown' };
94
- const dependencyIds = [];
95
- const workspaceIds = [];
96
- for (const id of after.ids) {
97
- if (before.ids.has(id))
98
- continue;
99
- (packages.has(id) ? dependencyIds : workspaceIds).push(id);
100
- }
101
- return {
102
- kind: 'known',
103
- dependencyIds: dependencyIds.toSorted(),
104
- workspaceIds: workspaceIds.toSorted(),
105
- };
30
+ export async function buildLedgersAllowNativeRepair() {
31
+ const ledgers = await buildLedgers();
32
+ return (ledgers !== undefined &&
33
+ ledgers.ignoredBuilds !== undefined &&
34
+ ledgers.ignoredBuilds.length === 0 &&
35
+ ledgers.pendingBuilds.kind === 'clear');
106
36
  }
@@ -29,18 +29,32 @@ if [ "\${PNPM_FAIL_RELEASE_AGE_CALL:-0}" = "$calls" ]; then
29
29
  exit 1
30
30
  fi
31
31
  if [ -n "\${PNPM_SLEEP_SECONDS:-}" ]; then sleep "$PNPM_SLEEP_SECONDS"; fi
32
- if [ -n "\${PNPM_PENDING_BUILDS:-}" ]; then
32
+ if [ "\${PNPM_INVALID_PENDING_BUILDS:-0}" = 1 ]; then
33
+ mkdir -p "$PNPM_NODE_MODULES"
34
+ printf 'pendingBuilds: invalid\n' > "$PNPM_NODE_MODULES/.modules.yaml"
35
+ elif [ -n "\${PNPM_PENDING_BUILDS:-}" ]; then
33
36
  mkdir -p "$PNPM_NODE_MODULES"
34
37
  printf 'pendingBuilds: [%s]\\n' "$PNPM_PENDING_BUILDS" > "$PNPM_NODE_MODULES/.modules.yaml"
38
+ elif [ ! -f "$PNPM_NODE_MODULES/.modules.yaml" ]; then
39
+ mkdir -p "$PNPM_NODE_MODULES"
40
+ printf 'pendingBuilds: []\\n' > "$PNPM_NODE_MODULES/.modules.yaml"
35
41
  fi
36
42
  case " $* " in
37
43
  *' rebuild '*)
44
+ if [ "\${PNPM_REPAIR_NATIVE_ON_REBUILD:-0}" = 1 ]; then
45
+ cp "$PNPM_NATIVE_REPLACEMENT" "$PNPM_NATIVE_ADDON"
46
+ fi
38
47
  if [ "\${PNPM_REBUILD_INVALID_LEDGER:-0}" = 1 ]; then
39
48
  printf 'pendingBuilds: invalid\n' > "$PNPM_NODE_MODULES/.modules.yaml"
49
+ else
50
+ printf 'pendingBuilds: []\n' > "$PNPM_NODE_MODULES/.modules.yaml"
40
51
  fi
41
52
  if [ "\${PNPM_REBUILD_BREAK_LINK:-0}" = 1 ]; then rm -f "$PNPM_DEPENDENCY_LINK"; fi
42
53
  ;;
43
54
  *' --force '*)
55
+ if [ -z "\${PNPM_PENDING_BUILDS:-}" ]; then
56
+ printf 'pendingBuilds: []\n' > "$PNPM_NODE_MODULES/.modules.yaml"
57
+ fi
44
58
  if [ "\${PNPM_DELETE_NATIVE:-0}" = 1 ]; then
45
59
  rm -f "$PNPM_NATIVE_ADDON"
46
60
  elif [ "\${PNPM_REPAIR_NATIVE:-0}" = 1 ]; then
@@ -1,10 +1,10 @@
1
- import { persistentDependencyTreeIsCold, persistentMetadataFingerprintV4, persistentMetadataStatusV4, writePersistentMetadataStampV4, } from './metadata.mjs';
1
+ import { persistentDependencyTreeIsCold, persistentMetadataFingerprintV5, persistentMetadataStatusV5, writePersistentMetadataStampV5, } from './metadata.mjs';
2
2
  import { runPnpm } from './exec.mjs';
3
3
  import { mismatchedNativeBinaries } from './native-health.mjs';
4
4
  // oxfmt-ignore
5
- import { clearPendingDependencyBuilds, pendingBuildDelta, pendingBuilds, validDependencyBuildIds } from './pending-builds.mjs';
5
+ import { pendingBuilds } from './pending-builds.mjs';
6
6
  // oxfmt-ignore
7
- import { install, reconcileOrFail, repairIsolatedNativeMismatch, withScriptPolicy } from './install-operations.mjs';
7
+ import { finalizePendingBuilds, install, reconcileOrFail, repairIsolatedNativeMismatch, withScriptPolicy } from './install-operations.mjs';
8
8
  // oxfmt-ignore
9
9
  import { baseInstallArgs, findWorkspaceLinkMismatches, logWorkspaceLinkMismatches } from './support.mjs';
10
10
  import { persistentInstallTransition, persistentProvenanceDiagnostic } from './transition.mjs';
@@ -14,33 +14,30 @@ async function persistent(options) {
14
14
  if (options.ephemeralWorkspaces.trim())
15
15
  fail('ephemeral-workspaces is only valid for ephemeral runners');
16
16
  const runCapture = (args) => runPnpm(args, options, true);
17
- const fingerprint = await persistentMetadataFingerprintV4(runCapture);
18
- const provenance = await persistentMetadataStatusV4(fingerprint);
17
+ const fingerprint = await persistentMetadataFingerprintV5(runCapture);
18
+ const provenance = await persistentMetadataStatusV5(fingerprint);
19
+ const pendingBefore = await pendingBuilds();
19
20
  const mismatchedNatives = await mismatchedNativeBinaries();
20
21
  const nativesMatch = mismatchedNatives.length === 0;
21
22
  const repairedNativeMismatch = !nativesMatch &&
22
23
  provenance.kind === 'matching' &&
23
24
  (await repairIsolatedNativeMismatch(options, runCapture, mismatchedNatives));
24
25
  if (repairedNativeMismatch) {
26
+ const pendingAfterRepair = await finalizePendingBuilds(options, runCapture, 'native health reconciliation');
25
27
  console.warn('persistent optional native binaries do not match this runtime; reconciled');
26
28
  console.warn(persistentProvenanceDiagnostic(provenance, options.installScripts, nativesMatch, {
27
29
  action: 'reconcile',
28
30
  reason: 'native-health-mismatch',
29
31
  }));
30
- await writePersistentMetadataStampV4(fingerprint, options.installScripts, true, []);
32
+ await writePersistentMetadataStampV5(fingerprint, options.installScripts, options.installScripts || persistentStampRemainsVerified(provenance, pendingAfterRepair));
31
33
  return 'persistent native health reconciled';
32
34
  }
33
35
  const provisionalTransition = persistentInstallTransition(provenance, options.installScripts);
34
- let transition = nativesMatch
35
- ? provisionalTransition
36
- : { action: 'reconcile', reason: 'native-health-mismatch' };
37
- if (transition.action === 'upgrade-dependencies') {
38
- const ids = await validDependencyBuildIds(transition.pendingDependencyBuilds);
39
- if (!ids)
40
- transition = { action: 'upgrade-scripts', reason: 'invalid-pending-dependency-builds' };
41
- else
42
- transition = { ...transition, pendingDependencyBuilds: ids };
43
- }
36
+ const transition = !nativesMatch
37
+ ? { action: 'reconcile', reason: 'native-health-mismatch' }
38
+ : provenance.kind !== 'matching' || pendingBefore.kind === 'clear'
39
+ ? provisionalTransition
40
+ : { action: 'reconcile', reason: 'pending-build-ledger-unverified' };
44
41
  const provenanceOk = provenance.kind === 'matching' && transition.action !== 'reconcile' && nativesMatch;
45
42
  const cold = !provenanceOk && (await persistentDependencyTreeIsCold());
46
43
  if (!provenanceOk && !cold) {
@@ -51,54 +48,19 @@ async function persistent(options) {
51
48
  console.warn(provenance.kind === 'matching' && !nativesMatch
52
49
  ? 'persistent optional native binaries do not match this runtime; reconciling'
53
50
  : 'persistent dependency metadata provenance is missing or changed; reconciling');
54
- await reconcileOrFail(options, runCapture);
55
- await writePersistentMetadataStampV4(fingerprint, options.installScripts, true, []);
51
+ await reconcileOrFail(options);
52
+ const pendingAfterReconcile = await finalizePendingBuilds(options, runCapture, 'persistent reconciliation');
53
+ await writePersistentMetadataStampV5(fingerprint, options.installScripts, options.installScripts || persistentStampRemainsVerified(provenance, pendingAfterReconcile));
56
54
  return 'persistent metadata reconciled';
57
55
  }
58
56
  if (provenance.kind === 'absent')
59
57
  console.warn('persistent dependency tree is absent; installing cold');
60
- const pendingBefore = transition.action === 'ordinary' && !options.installScripts ? await pendingBuilds() : undefined;
61
- await install(withScriptPolicy([...baseInstallArgs], transition.action.startsWith('upgrade-') ? false : options.installScripts), options, 'ordinary persistent install');
58
+ await install(withScriptPolicy([...baseInstallArgs], options.installScripts), options, 'ordinary persistent install');
62
59
  const stale = await findWorkspaceLinkMismatches(runCapture);
63
60
  if (stale.length === 0) {
64
- if (transition.action.startsWith('upgrade-')) {
65
- const pendingDependencyBuilds = transition.action === 'upgrade-dependencies'
66
- ? transition.pendingDependencyBuilds
67
- : undefined;
68
- const rebuild = pendingDependencyBuilds === undefined
69
- ? ['rebuild', '--pending', '--recursive']
70
- : ['rebuild', '--recursive', '--', ...pendingDependencyBuilds];
71
- await install(rebuild, options, 'pending scripts rebuild');
72
- const rebuiltStale = await findWorkspaceLinkMismatches(runCapture);
73
- if (rebuiltStale.length > 0) {
74
- logWorkspaceLinkMismatches(rebuiltStale);
75
- await reconcileOrFail(options, runCapture);
76
- console.warn(persistentProvenanceDiagnostic(provenance, options.installScripts, nativesMatch, {
77
- action: 'reconcile',
78
- reason: 'workspace-links-stale-after-rebuild',
79
- }));
80
- await writePersistentMetadataStampV4(fingerprint, options.installScripts, true, []);
81
- return 'persistent reconciled';
82
- }
83
- if (pendingDependencyBuilds !== undefined &&
84
- !(await clearPendingDependencyBuilds(pendingDependencyBuilds)))
85
- fail('dependency rebuild completed but pending build ledger could not be updated safely');
86
- }
61
+ const pendingAfterInstall = await finalizePendingBuilds(options, runCapture, 'persistent install');
87
62
  console.warn(persistentProvenanceDiagnostic(provenance, options.installScripts, nativesMatch, transition));
88
- const pendingAfter = pendingBefore ? await pendingBuilds() : undefined;
89
- const delta = pendingBefore && pendingAfter
90
- ? await pendingBuildDelta(pendingBefore, pendingAfter)
91
- : undefined;
92
- const unsafePending = delta?.kind === 'unknown' || Boolean(delta?.workspaceIds.length);
93
- const pendingDependencyBuilds = transition.action.startsWith('upgrade-') || unsafePending
94
- ? []
95
- : [
96
- ...new Set([
97
- ...(provenance.kind === 'matching' ? provenance.pendingDependencyBuilds : []),
98
- ...(delta?.kind === 'known' ? delta.dependencyIds : []),
99
- ]),
100
- ].toSorted();
101
- await writePersistentMetadataStampV4(fingerprint, options.installScripts, unsafePending, pendingDependencyBuilds);
63
+ await writePersistentMetadataStampV5(fingerprint, options.installScripts, options.installScripts || persistentStampRemainsVerified(provenance, pendingAfterInstall));
102
64
  return provenance.kind === 'absent' ? 'persistent cold' : 'persistent ordinary';
103
65
  }
104
66
  logWorkspaceLinkMismatches(stale);
@@ -106,10 +68,16 @@ async function persistent(options) {
106
68
  action: 'reconcile',
107
69
  reason: 'workspace-links-stale',
108
70
  }));
109
- await reconcileOrFail(options, runCapture);
110
- await writePersistentMetadataStampV4(fingerprint, options.installScripts, true, []);
71
+ await reconcileOrFail(options);
72
+ const pendingAfterReconcile = await finalizePendingBuilds(options, runCapture, 'persistent reconciliation');
73
+ await writePersistentMetadataStampV5(fingerprint, options.installScripts, options.installScripts || persistentStampRemainsVerified(provenance, pendingAfterReconcile));
111
74
  return 'persistent reconciled';
112
75
  }
76
+ function persistentStampRemainsVerified(provenance, pending) {
77
+ return (provenance.kind === 'matching' &&
78
+ provenance.scriptsEnabledInstallVerified &&
79
+ pending.kind === 'clear');
80
+ }
113
81
  function isUnbracedPathClosureSelector(selector) {
114
82
  return /^\.{0,2}\//.test(selector) && selector.endsWith('...') && !selector.startsWith('{');
115
83
  }
@@ -6,17 +6,12 @@ export type ProvenanceStatus = {
6
6
  } | {
7
7
  kind: 'matching';
8
8
  lastInvocationInstallScripts: boolean;
9
- pendingDependencyBuilds: string[];
10
- scriptsEnabledInstallSucceeded: boolean;
9
+ scriptsEnabledInstallVerified: boolean;
11
10
  } | {
12
11
  kind: 'unsafe';
13
12
  };
14
13
  export type PersistentInstallTransition = {
15
- action: 'ordinary' | 'reconcile' | 'upgrade-scripts';
16
- reason: string;
17
- } | {
18
- action: 'upgrade-dependencies';
19
- pendingDependencyBuilds: [string, ...string[]];
14
+ action: 'ordinary' | 'reconcile';
20
15
  reason: string;
21
16
  };
22
17
  export declare function persistentInstallTransition(provenance: ProvenanceStatus, installScripts: boolean): PersistentInstallTransition;
@@ -2,15 +2,8 @@
2
2
  // evidence that this structural tree has already completed a scripts-enabled install.
3
3
  export function persistentInstallTransition(provenance, installScripts) {
4
4
  if (provenance.kind === 'matching') {
5
- if (installScripts && !provenance.scriptsEnabledInstallSucceeded)
6
- return { action: 'upgrade-scripts', reason: 'pending-scripts-rebuild' };
7
- const [firstPendingDependencyBuild, ...remainingPendingDependencyBuilds] = provenance.pendingDependencyBuilds;
8
- if (installScripts && firstPendingDependencyBuild !== undefined)
9
- return {
10
- action: 'upgrade-dependencies',
11
- pendingDependencyBuilds: [firstPendingDependencyBuild, ...remainingPendingDependencyBuilds],
12
- reason: 'pending-dependency-rebuild',
13
- };
5
+ if (installScripts && !provenance.scriptsEnabledInstallVerified)
6
+ return { action: 'reconcile', reason: 'scripts-enabled-install-unverified' };
14
7
  return { action: 'ordinary', reason: 'matching-structural-provenance' };
15
8
  }
16
9
  if (provenance.kind === 'absent')
@@ -26,9 +19,8 @@ export function persistentProvenanceDiagnostic(provenance, installScripts, nativ
26
19
  changedComponents: provenance.kind === 'changed' ? provenance.components : [],
27
20
  event: 'pnpm-install-persistent-provenance',
28
21
  installScripts,
29
- scriptsEnabledInstallSucceeded: provenance.kind === 'matching' ? provenance.scriptsEnabledInstallSucceeded : false,
22
+ scriptsEnabledInstallVerified: provenance.kind === 'matching' ? provenance.scriptsEnabledInstallVerified : false,
30
23
  lastInvocationInstallScripts: provenance.kind === 'matching' ? provenance.lastInvocationInstallScripts : null,
31
- pendingDependencyBuildCount: provenance.kind === 'matching' ? provenance.pendingDependencyBuilds.length : 0,
32
24
  nativeBinariesMatchRuntime,
33
25
  reason: transition.reason,
34
26
  state: provenance.kind,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vouchington-tooling",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "description": "Vouchington CLI and extractable tooling libraries.",
5
5
  "homepage": "https://github.com/vouchington/vouchington-tooling/tree/main/packages/vouchington-tooling#readme",
6
6
  "bugs": {