vouchington-tooling 0.3.6 → 0.4.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 (37) hide show
  1. package/README.md +11 -0
  2. package/dist/cli/commands/prepare-vitest-reports.d.mts +2 -0
  3. package/dist/cli/commands/prepare-vitest-reports.mjs +11 -0
  4. package/dist/cli/commands/vitest-report-attempt.d.mts +2 -0
  5. package/dist/cli/commands/vitest-report-attempt.mjs +11 -0
  6. package/dist/cli/index.mjs +6 -0
  7. package/dist/cli/parse.d.mts +6 -0
  8. package/dist/cli/parse.mjs +4 -0
  9. package/dist/cli/usage.d.mts +1 -1
  10. package/dist/cli/usage.mjs +4 -0
  11. package/dist/pnpm-install/install-operations.d.mts +1 -0
  12. package/dist/pnpm-install/install-operations.mjs +26 -6
  13. package/dist/pnpm-install/native-health.d.mts +2 -0
  14. package/dist/pnpm-install/native-health.mjs +22 -6
  15. package/dist/pnpm-install/pending-builds.d.mts +1 -0
  16. package/dist/pnpm-install/pending-builds.mjs +17 -0
  17. package/dist/pnpm-install/pnpm-install-fake-pnpm.test-helpers.mjs +6 -0
  18. package/dist/pnpm-install/pnpm-install-fixture.test-helpers.d.mts +1 -0
  19. package/dist/pnpm-install/pnpm-install-fixture.test-helpers.mjs +17 -0
  20. package/dist/pnpm-install/runner.mjs +21 -5
  21. package/dist/pnpm-install/support.d.mts +1 -0
  22. package/dist/pnpm-install/support.mjs +3 -3
  23. package/dist/vitest-blob-manifest/cli.mjs +2 -4
  24. package/dist/vitest-blob-manifest/report-attempt-cli.d.mts +2 -0
  25. package/dist/vitest-blob-manifest/report-attempt-cli.mjs +32 -0
  26. package/dist/vitest-blob-manifest/reports-cli.d.mts +2 -0
  27. package/dist/vitest-blob-manifest/reports-cli.mjs +55 -0
  28. package/dist/vitest-blob-manifest/run-attempt.d.mts +2 -0
  29. package/dist/vitest-blob-manifest/run-attempt.mjs +11 -0
  30. package/dist/workspace-gates/manifest-version-assertions.d.mts +3 -0
  31. package/dist/workspace-gates/manifest-version-assertions.mjs +100 -0
  32. package/dist/workspace-gates/manifest-version-parser.d.mts +6 -0
  33. package/dist/workspace-gates/manifest-version-parser.mjs +104 -0
  34. package/dist/workspace-gates/manifest-version-patterns.d.mts +9 -0
  35. package/dist/workspace-gates/manifest-version-patterns.mjs +21 -0
  36. package/dist/workspace-gates/policy.mjs +2 -0
  37. package/package.json +2 -2
package/README.md CHANGED
@@ -30,6 +30,9 @@ vouchington prepare-trivy-db
30
30
  vouchington gha-artifacts-cleanup run --run-id 123 --keep-pattern 'plan-*' --delete-pattern 'coverage-*'
31
31
  vouchington http-origin --field cdn_origin https://images.example.com
32
32
  vouchington vitest-blob-manifest <suite> [reports-directory]
33
+ vouchington vitest-report-attempt write <directory> <suite>
34
+ vouchington vitest-report-attempt read <root>
35
+ vouchington prepare-vitest-reports [primary-directory] [fallback-directory] [output-directory]
33
36
  vouchington pnpm-install --runner-lifecycle persistent --install-scripts true
34
37
  vouchington check-cache-size /tmp/cache 1048576 node-modules
35
38
  vouchington make-shard-matrix 4
@@ -63,6 +66,10 @@ For persistent `pnpm-install`, v4 metadata tracks structural inputs separately f
63
66
  uses one script-suppressed verification install followed by `pnpm rebuild --pending --recursive`.
64
67
  When only newly pending dependency package IDs remain, it instead rebuilds those exact IDs without
65
68
  rerunning first-party workspace hooks.
69
+ An isolated native-binary mismatch uses one strict forced install only when structural provenance
70
+ matches, workspace links are valid, and pnpm records empty `ignoredBuilds` and `pendingBuilds` ledgers;
71
+ otherwise it retains the script-free then strict reconciliation. Native and workspace-link health
72
+ are verified before its metadata stamp is refreshed.
66
73
  The command emits a structured non-secret provenance diagnostic identifying changed structural
67
74
  categories, the last script policy, script capability, and native-binary health.
68
75
 
@@ -183,6 +190,10 @@ import { appendJournal, probeBlackboard } from 'vouchington-tooling/agent-blackb
183
190
  import { buildSessionFrictionReport, recordFriction } from 'vouchington-tooling/session-friction'
184
191
  ```
185
192
 
193
+ `checkWorkspaceGatesPolicy` rejects tracked test assertions that hard-code the exact version of a
194
+ dependency declared by a non-fixture package manifest. Assert dependency membership or placement,
195
+ or derive a configuration or documentation package spec from that manifest instead.
196
+
186
197
  `session-friction` is an opt-in capture and reporting library. Callers supply the session id,
187
198
  absolute log directory, host-independent observation, and journal loader; it does not inspect host
188
199
  environment variables, install hooks, or connect to a journal service by itself. Invoking
@@ -0,0 +1,2 @@
1
+ import { runPrepareVitestReportsCli } from '../../vitest-blob-manifest/reports-cli.mts';
2
+ export declare function runPrepareVitestReportsCommand(args: readonly string[], run?: typeof runPrepareVitestReportsCli): number;
@@ -0,0 +1,11 @@
1
+ import { runPrepareVitestReportsCli } from '../../vitest-blob-manifest/reports-cli.mjs';
2
+ export function runPrepareVitestReportsCommand(args, run = runPrepareVitestReportsCli) {
3
+ try {
4
+ run(args);
5
+ return 0;
6
+ }
7
+ catch (error) {
8
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
9
+ return 1;
10
+ }
11
+ }
@@ -0,0 +1,2 @@
1
+ import { runVitestReportAttemptCli } from '../../vitest-blob-manifest/report-attempt-cli.mts';
2
+ export declare function runVitestReportAttemptCommand(args: readonly string[], run?: typeof runVitestReportAttemptCli): number;
@@ -0,0 +1,11 @@
1
+ import { runVitestReportAttemptCli } from '../../vitest-blob-manifest/report-attempt-cli.mjs';
2
+ export function runVitestReportAttemptCommand(args, run = runVitestReportAttemptCli) {
3
+ try {
4
+ run(args);
5
+ return 0;
6
+ }
7
+ catch (error) {
8
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
9
+ return 1;
10
+ }
11
+ }
@@ -14,6 +14,8 @@ import { runPostReviewCommand } from './commands/post-review.mjs';
14
14
  import { runStageReviewPayloadCommand } from './commands/stage-review-payload.mjs';
15
15
  import { runSwiftSemanticEqualCommand } from './commands/swift-semantic-equal.mjs';
16
16
  import { runVitestBlobManifestCommand } from './commands/vitest-blob-manifest.mjs';
17
+ import { runVitestReportAttemptCommand } from './commands/vitest-report-attempt.mjs';
18
+ import { runPrepareVitestReportsCommand } from './commands/prepare-vitest-reports.mjs';
17
19
  import { runRetrospectiveTranscriptCommand } from './commands/retrospective-transcript.mjs';
18
20
  import { runLinkSkill } from './commands/link-skill.mjs';
19
21
  import { runRetrospectiveFactsCommand } from './commands/retrospective-facts.mjs';
@@ -93,6 +95,10 @@ export function runCli(argv = process.argv) {
93
95
  return runPnpmInstallCli(parsed.args);
94
96
  case 'vitest-blob-manifest':
95
97
  return runVitestBlobManifestCommand(parsed.args);
98
+ case 'vitest-report-attempt':
99
+ return runVitestReportAttemptCommand(parsed.args);
100
+ case 'prepare-vitest-reports':
101
+ return runPrepareVitestReportsCommand(parsed.args);
96
102
  case 'nuget-central-version':
97
103
  return runNugetCentralVersionCommand(parsed.args);
98
104
  case 'swift-semantic-equal':
@@ -24,6 +24,12 @@ export type ParsedCli = {
24
24
  } | {
25
25
  kind: 'vitest-blob-manifest';
26
26
  args: string[];
27
+ } | {
28
+ kind: 'vitest-report-attempt';
29
+ args: string[];
30
+ } | {
31
+ kind: 'prepare-vitest-reports';
32
+ args: string[];
27
33
  } | {
28
34
  kind: 'nuget-central-version';
29
35
  args: string[];
@@ -40,6 +40,10 @@ export function parseCli(argv) {
40
40
  return { kind: 'pnpm-install', args: rest };
41
41
  if (command === 'vitest-blob-manifest')
42
42
  return { kind: 'vitest-blob-manifest', args: rest };
43
+ if (command === 'vitest-report-attempt')
44
+ return { kind: 'vitest-report-attempt', args: rest };
45
+ if (command === 'prepare-vitest-reports')
46
+ return { kind: 'prepare-vitest-reports', args: rest };
43
47
  if (command === 'nuget-central-version')
44
48
  return { kind: 'nuget-central-version', args: rest };
45
49
  if (command === 'swift-semantic-equal')
@@ -1,2 +1,2 @@
1
- export declare const USAGE = "Usage: vouchington <command> [options]\n\nCommands:\n runner-port-policy Print or validate a runner port policy\n with-host-lock Run a command under a host-wide lock\n gha-runtime-audit Audit successful GitHub Actions job runtimes\n gha-output Write a collision-safe multiline GITHUB_OUTPUT record\n gha-needs-results Fail if required GitHub Actions job results failed\n download-with-diagnostics Download a URL and report HTTP status on failure\n download-optional-run-artifacts Download optional artifacts from the current run\n host-pressure-diagnostics Print a bounded host memory/OOM/PSI snapshot\n allocate-browser-safe-ports Allocate Fetch-safe localhost ports\n diagnose-port-collision Capture bounded localhost port diagnostics\n prepare-trivy-db Download the Trivy vulnerability database\n gha-artifacts-cleanup Delete classified GitHub Actions artifacts\n http-origin Validate an optional HTTP(S) origin\n vitest-blob-manifest Stamp a vitest-blob-manifest:v1 identity file\n pnpm-install Install a pnpm workspace with retry and release-age fail-fast\n check-cache-size Measure a path and decide whether to save a GHA cache\n make-shard-matrix Emit a [1..N] GitHub Actions shard matrix\n load-runner-env Overlay a runner env file onto GITHUB_ENV with injection guards\n clean-workspace Reset a persistent-runner workspace with a fork-PR trust gate\n install-github-release Download a checksum-verified GitHub Release binary\n run-with-timeout Run a command with GNU timeout or a Perl fallback\n lint-links Two-pass lychee: internal links fail, external warn\n materialize-pr-context Dump PR title/body/files/diff/comments and #N crawl\n wait-for-apt-locks Wait until apt/dpkg lock files are free\n install-playwright-chromium-arm64 Install Playwright Chromium from browsers.json\n ghcr-package-retention Delete old GHCR package versions past KEEP_MIN\n harness-admission-lane Compute a GITHUB_RUN_ID admission lane for fleet fan-out\n harness-assert-gates Fail if any named HARNESS_*_ENABLED gate is enabled\n nuget-central-version Validate a Directory.Packages.props PackageVersion delta\n swift-semantic-equal Compare Swift sources ignoring comments and whitespace\n post-review Post one COMMENT review from a staged payload file\n stage-review-payload Validate a review payload file into a staging directory\n retrospective-transcript Format facts from Claude-compatible, Codex, or Grok transcripts\n link-skill Link one packaged skill into an explicit consumer directory\n retrospective-facts Gather immutable facts for a retrospective\n agent-blackboard Probe and journal an Agent Blackboard deployment\n\nOptions:\n -h, --help Show this help\n -v, --version Print the package version\n\nrunner-port-policy\n (no args) Print the shipped policy as JSON\n --file <path> Validate and print a policy file\n --reserved <port> Print true if the port is reserved\n\nwith-host-lock\n --name <family>\n [--slots <n>]\n --timeout-seconds <n>\n [--command-timeout-seconds <n>]\n [--failure-diagnostics <absolute-script>]\n [--on-acquire-timeout fail|run-unlocked]\n -- <command> [args...]\n\ngha-runtime-audit\n [--repository owner/name] Default GITHUB_REPOSITORY\n [--branch main]\n --pr-workflow <name|/regex/> Repeatable\n --push-workflow <name|/regex/> Repeatable\n\ngha-output <name>\ngha-needs-results [label]\ndownload-with-diagnostics <url> <destination> [-- curl-args...]\ndownload-optional-run-artifacts (--name <name> | --pattern <pattern>) --dir <directory>\nhost-pressure-diagnostics\nallocate-browser-safe-ports [count] [--policy path] [--forbidden-ports path]\ndiagnose-port-collision [--ports \"2200 2216\"] [--output-dir PATH]\nprepare-trivy-db\ngha-artifacts-cleanup run --run-id <id> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\ngha-artifacts-cleanup sweep --older-than-hours <n> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\nhttp-origin [--field NAME] [value]\nvitest-blob-manifest <suite> [reports-directory]\npnpm-install --runner-lifecycle persistent|ephemeral|ephemeral-full --install-scripts true|false\ncheck-cache-size <path> <max-bytes> <label>\nmake-shard-matrix <total>\nload-runner-env\nclean-workspace\ninstall-github-release --repo owner/name --version X --asset 'name-{platform}.tar.gz' --bin name [--tag-prefix PREFIX] [--expected-sha256 SHA256] [--no-checksum] [--checksums-asset NAME] [--version-flag FLAG] [--bin-dir DIR]\nrun-with-timeout <timeout-seconds> <kill-after-seconds> <command...>\nlint-links [--offline] [--config PATH] [--glob PATTERN] [files...]\nmaterialize-pr-context\nwait-for-apt-locks\ninstall-playwright-chromium-arm64 [name:archive...]\nghcr-package-retention <url-encoded-package>...\nharness-admission-lane <lanes>\nharness-assert-gates <gate>...\nnuget-central-version <trusted-props> <candidate-props> <metadata-json> <output-props>\nswift-semantic-equal <base> <head> <file.swift>\npost-review\nstage-review-payload optional|required <source> <destination>\nretrospective-transcript [--session-id ID] [--jsonl PATH] [--projects-dir PATH] [--codex-sessions-dir PATH] [--grok-sessions-dir PATH]\nlink-skill <name> --source-root <skills-dir> --target-root <consumer-skills-dir> Link a packaged or repository-local skill\nretrospective-facts (--pr NUMBER | --branch NAME | --no-pr) [--repo OWNER/NAME] [--raw]\nagent-blackboard probe\nagent-blackboard journal append --session-id UUID --agent NAME --version VERSION --file PATH [--parent-session-id UUID] [--timestamp ISO8601]\nagent-blackboard journal entries --session-id UUID\nagent-blackboard snapshot partition --snapshot PATH --checksum SHA256 --counts '{\"sessions\":N,\"entries\":N,\"records\":N,\"bytes\":N}'\nagent-blackboard snapshot cleanup [--snapshot PATH] [--partition-directory PATH --receipt JSON]\n";
1
+ export declare const USAGE = "Usage: vouchington <command> [options]\n\nCommands:\n runner-port-policy Print or validate a runner port policy\n with-host-lock Run a command under a host-wide lock\n gha-runtime-audit Audit successful GitHub Actions job runtimes\n gha-output Write a collision-safe multiline GITHUB_OUTPUT record\n gha-needs-results Fail if required GitHub Actions job results failed\n download-with-diagnostics Download a URL and report HTTP status on failure\n download-optional-run-artifacts Download optional artifacts from the current run\n host-pressure-diagnostics Print a bounded host memory/OOM/PSI snapshot\n allocate-browser-safe-ports Allocate Fetch-safe localhost ports\n diagnose-port-collision Capture bounded localhost port diagnostics\n prepare-trivy-db Download the Trivy vulnerability database\n gha-artifacts-cleanup Delete classified GitHub Actions artifacts\n http-origin Validate an optional HTTP(S) origin\n vitest-blob-manifest Stamp a vitest-blob-manifest:v1 identity file\n vitest-report-attempt Write or read a Vitest report-attempt marker\n prepare-vitest-reports Validate and select Vitest report JSON files\n pnpm-install Install a pnpm workspace with retry and release-age fail-fast\n check-cache-size Measure a path and decide whether to save a GHA cache\n make-shard-matrix Emit a [1..N] GitHub Actions shard matrix\n load-runner-env Overlay a runner env file onto GITHUB_ENV with injection guards\n clean-workspace Reset a persistent-runner workspace with a fork-PR trust gate\n install-github-release Download a checksum-verified GitHub Release binary\n run-with-timeout Run a command with GNU timeout or a Perl fallback\n lint-links Two-pass lychee: internal links fail, external warn\n materialize-pr-context Dump PR title/body/files/diff/comments and #N crawl\n wait-for-apt-locks Wait until apt/dpkg lock files are free\n install-playwright-chromium-arm64 Install Playwright Chromium from browsers.json\n ghcr-package-retention Delete old GHCR package versions past KEEP_MIN\n harness-admission-lane Compute a GITHUB_RUN_ID admission lane for fleet fan-out\n harness-assert-gates Fail if any named HARNESS_*_ENABLED gate is enabled\n nuget-central-version Validate a Directory.Packages.props PackageVersion delta\n swift-semantic-equal Compare Swift sources ignoring comments and whitespace\n post-review Post one COMMENT review from a staged payload file\n stage-review-payload Validate a review payload file into a staging directory\n retrospective-transcript Format facts from Claude-compatible, Codex, or Grok transcripts\n link-skill Link one packaged skill into an explicit consumer directory\n retrospective-facts Gather immutable facts for a retrospective\n agent-blackboard Probe and journal an Agent Blackboard deployment\n\nOptions:\n -h, --help Show this help\n -v, --version Print the package version\n\nrunner-port-policy\n (no args) Print the shipped policy as JSON\n --file <path> Validate and print a policy file\n --reserved <port> Print true if the port is reserved\n\nwith-host-lock\n --name <family>\n [--slots <n>]\n --timeout-seconds <n>\n [--command-timeout-seconds <n>]\n [--failure-diagnostics <absolute-script>]\n [--on-acquire-timeout fail|run-unlocked]\n -- <command> [args...]\n\ngha-runtime-audit\n [--repository owner/name] Default GITHUB_REPOSITORY\n [--branch main]\n --pr-workflow <name|/regex/> Repeatable\n --push-workflow <name|/regex/> Repeatable\n\ngha-output <name>\ngha-needs-results [label]\ndownload-with-diagnostics <url> <destination> [-- curl-args...]\ndownload-optional-run-artifacts (--name <name> | --pattern <pattern>) --dir <directory>\nhost-pressure-diagnostics\nallocate-browser-safe-ports [count] [--policy path] [--forbidden-ports path]\ndiagnose-port-collision [--ports \"2200 2216\"] [--output-dir PATH]\nprepare-trivy-db\ngha-artifacts-cleanup run --run-id <id> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\ngha-artifacts-cleanup sweep --older-than-hours <n> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\nhttp-origin [--field NAME] [value]\nvitest-blob-manifest <suite> [reports-directory]\nvitest-report-attempt <write DIRECTORY SUITE|read ROOT>\nprepare-vitest-reports [primary-directory] [fallback-directory] [output-directory]\npnpm-install --runner-lifecycle persistent|ephemeral|ephemeral-full --install-scripts true|false\ncheck-cache-size <path> <max-bytes> <label>\nmake-shard-matrix <total>\nload-runner-env\nclean-workspace\ninstall-github-release --repo owner/name --version X --asset 'name-{platform}.tar.gz' --bin name [--tag-prefix PREFIX] [--expected-sha256 SHA256] [--no-checksum] [--checksums-asset NAME] [--version-flag FLAG] [--bin-dir DIR]\nrun-with-timeout <timeout-seconds> <kill-after-seconds> <command...>\nlint-links [--offline] [--config PATH] [--glob PATTERN] [files...]\nmaterialize-pr-context\nwait-for-apt-locks\ninstall-playwright-chromium-arm64 [name:archive...]\nghcr-package-retention <url-encoded-package>...\nharness-admission-lane <lanes>\nharness-assert-gates <gate>...\nnuget-central-version <trusted-props> <candidate-props> <metadata-json> <output-props>\nswift-semantic-equal <base> <head> <file.swift>\npost-review\nstage-review-payload optional|required <source> <destination>\nretrospective-transcript [--session-id ID] [--jsonl PATH] [--projects-dir PATH] [--codex-sessions-dir PATH] [--grok-sessions-dir PATH]\nlink-skill <name> --source-root <skills-dir> --target-root <consumer-skills-dir> Link a packaged or repository-local skill\nretrospective-facts (--pr NUMBER | --branch NAME | --no-pr) [--repo OWNER/NAME] [--raw]\nagent-blackboard probe\nagent-blackboard journal append --session-id UUID --agent NAME --version VERSION --file PATH [--parent-session-id UUID] [--timestamp ISO8601]\nagent-blackboard journal entries --session-id UUID\nagent-blackboard snapshot partition --snapshot PATH --checksum SHA256 --counts '{\"sessions\":N,\"entries\":N,\"records\":N,\"bytes\":N}'\nagent-blackboard snapshot cleanup [--snapshot PATH] [--partition-directory PATH --receipt JSON]\n";
2
2
  export declare function printUsage(stream?: NodeJS.WritableStream): void;
@@ -15,6 +15,8 @@ Commands:
15
15
  gha-artifacts-cleanup Delete classified GitHub Actions artifacts
16
16
  http-origin Validate an optional HTTP(S) origin
17
17
  vitest-blob-manifest Stamp a vitest-blob-manifest:v1 identity file
18
+ vitest-report-attempt Write or read a Vitest report-attempt marker
19
+ prepare-vitest-reports Validate and select Vitest report JSON files
18
20
  pnpm-install Install a pnpm workspace with retry and release-age fail-fast
19
21
  check-cache-size Measure a path and decide whether to save a GHA cache
20
22
  make-shard-matrix Emit a [1..N] GitHub Actions shard matrix
@@ -74,6 +76,8 @@ gha-artifacts-cleanup run --run-id <id> [--keep-pattern glob] [--delete-pattern
74
76
  gha-artifacts-cleanup sweep --older-than-hours <n> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]
75
77
  http-origin [--field NAME] [value]
76
78
  vitest-blob-manifest <suite> [reports-directory]
79
+ vitest-report-attempt <write DIRECTORY SUITE|read ROOT>
80
+ prepare-vitest-reports [primary-directory] [fallback-directory] [output-directory]
77
81
  pnpm-install --runner-lifecycle persistent|ephemeral|ephemeral-full --install-scripts true|false
78
82
  check-cache-size <path> <max-bytes> <label>
79
83
  make-shard-matrix <total>
@@ -2,3 +2,4 @@ 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
4
  export declare function reconcileOrFail(options: InstallOptions, runCapture: (args: string[]) => Promise<CommandResult>): Promise<void>;
5
+ export declare function repairIsolatedNativeMismatch(options: InstallOptions, runCapture: (args: string[]) => Promise<CommandResult>, mismatchedNativePaths: string[]): Promise<boolean>;
@@ -1,8 +1,10 @@
1
1
  import { scheduler } from 'node:timers/promises';
2
2
  import { runPnpm } from './exec.mjs';
3
+ import { nativeBinariesMatchRuntime, repairedNativeBinariesMatchRuntime } from './native-health.mjs';
4
+ import { buildLedgersAllowNativeRepair } from './pending-builds.mjs';
3
5
  import { INSTALL_TERMINATION_FAILED } from './process.mjs';
4
6
  import { formatReleaseAgeFailure, isReleaseAgeViolation } from './release-age.mjs';
5
- import { baseInstallArgs, findWorkspaceLinkMismatches, logWorkspaceLinkMismatches, } from './support.mjs';
7
+ import { findWorkspaceLinkMismatches, forcedInstallArgs, logWorkspaceLinkMismatches, } from './support.mjs';
6
8
  function fail(message) {
7
9
  throw new Error(message);
8
10
  }
@@ -27,12 +29,30 @@ export async function install(args, options, label) {
27
29
  fail(`${label} failed after ${options.maxAttempts} attempt${options.maxAttempts === 1 ? '' : 's'}`);
28
30
  }
29
31
  export async function reconcileOrFail(options, runCapture) {
30
- const forced = ['install', '--frozen-lockfile', '--force', ...baseInstallArgs.slice(2)];
31
- await install([...forced, '--ignore-scripts', '--ignore-pnpmfile'], options, 'script-free reconciliation');
32
- await install(withScriptPolicy(forced, options.installScripts), options, 'strict persistent reconciliation');
33
- const remaining = await findWorkspaceLinkMismatches(runCapture);
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');
35
+ }
36
+ async function verifyInstallHealth(runCapture, phase, repairedNativePaths = []) {
37
+ const [nativesMatch, remaining] = await Promise.all([
38
+ repairedNativePaths.length > 0
39
+ ? repairedNativeBinariesMatchRuntime(repairedNativePaths)
40
+ : nativeBinariesMatchRuntime(),
41
+ findWorkspaceLinkMismatches(runCapture),
42
+ ]);
43
+ if (!nativesMatch)
44
+ fail(`${phase} completed with mismatched native binaries`);
34
45
  if (remaining.length > 0) {
35
46
  logWorkspaceLinkMismatches(remaining);
36
- fail('persistent reconciliation completed with invalid workspace links');
47
+ fail(`${phase} completed with invalid workspace links`);
37
48
  }
38
49
  }
50
+ export async function repairIsolatedNativeMismatch(options, runCapture, mismatchedNativePaths) {
51
+ if (!(await buildLedgersAllowNativeRepair()))
52
+ return false;
53
+ if ((await findWorkspaceLinkMismatches(runCapture)).length > 0)
54
+ return false;
55
+ await install(withScriptPolicy(forcedInstallArgs, options.installScripts), options, 'native health reconciliation');
56
+ await verifyInstallHealth(runCapture, 'native health reconciliation', mismatchedNativePaths);
57
+ return true;
58
+ }
@@ -1,4 +1,6 @@
1
1
  export type NativeFamily = 'elf' | 'macho' | 'pe';
2
2
  export declare function nativeFamilyFromMagic(buffer: Buffer): NativeFamily | undefined;
3
3
  export declare function expectedNativeFamily(platform?: NodeJS.Platform): NativeFamily | undefined;
4
+ export declare function mismatchedNativeBinaries(root?: string, platform?: NodeJS.Platform): Promise<string[]>;
4
5
  export declare function nativeBinariesMatchRuntime(root?: string, platform?: NodeJS.Platform): Promise<boolean>;
6
+ export declare function repairedNativeBinariesMatchRuntime(paths: string[], platform?: NodeJS.Platform): Promise<boolean>;
@@ -58,27 +58,43 @@ async function searchRoot(nodeModules) {
58
58
  return nodeModules;
59
59
  }
60
60
  }
61
- export async function nativeBinariesMatchRuntime(root = process.cwd(), platform = process.platform) {
61
+ export async function mismatchedNativeBinaries(root = process.cwd(), platform = process.platform) {
62
62
  const expected = expectedNativeFamily(platform);
63
63
  if (expected === undefined)
64
- return true;
64
+ return [];
65
65
  const nodeModules = path.join(root, 'node_modules');
66
66
  try {
67
67
  const info = await stat(nodeModules);
68
68
  if (!info.isDirectory())
69
- return true;
69
+ return [];
70
70
  }
71
71
  catch {
72
- return true;
72
+ return [];
73
73
  }
74
74
  const cwd = await searchRoot(nodeModules);
75
+ const mismatches = [];
75
76
  for await (const relative of glob('**/*.{node,bin}', { cwd })) {
76
- const magic = await readMagic(path.join(cwd, relative));
77
+ const pathname = path.join(cwd, relative);
78
+ const magic = await readMagic(pathname);
77
79
  if (magic === undefined)
78
80
  continue;
79
81
  const family = nativeFamilyFromMagic(magic);
80
82
  if (family !== undefined && family !== expected)
83
+ mismatches.push(pathname);
84
+ }
85
+ return mismatches;
86
+ }
87
+ export async function nativeBinariesMatchRuntime(root = process.cwd(), platform = process.platform) {
88
+ return (await mismatchedNativeBinaries(root, platform)).length === 0;
89
+ }
90
+ export async function repairedNativeBinariesMatchRuntime(paths, platform = process.platform) {
91
+ const expected = expectedNativeFamily(platform);
92
+ if (expected === undefined)
93
+ return true;
94
+ for (const pathname of paths) {
95
+ const magic = await readMagic(pathname);
96
+ if (magic === undefined || nativeFamilyFromMagic(magic) !== expected)
81
97
  return false;
82
98
  }
83
- return true;
99
+ return nativeBinariesMatchRuntime();
84
100
  }
@@ -12,6 +12,7 @@ export type PendingBuildDelta = {
12
12
  workspaceIds: string[];
13
13
  };
14
14
  export declare function pendingBuilds(): Promise<PendingBuilds>;
15
+ export declare function buildLedgersAllowNativeRepair(): Promise<boolean>;
15
16
  export declare function validDependencyBuildIds(ids: [string, ...string[]]): Promise<[string, ...string[]] | undefined>;
16
17
  export declare function clearPendingDependencyBuilds(ids: string[]): Promise<boolean>;
17
18
  export declare function pendingBuildDelta(before: PendingBuilds, after: PendingBuilds): Promise<PendingBuildDelta>;
@@ -15,6 +15,23 @@ export async function pendingBuilds() {
15
15
  return { kind: 'unknown' };
16
16
  }
17
17
  }
18
+ export async function buildLedgersAllowNativeRepair() {
19
+ try {
20
+ const value = parse(await readFile(path.join(process.cwd(), 'node_modules', '.modules.yaml'), 'utf8'));
21
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
22
+ return false;
23
+ const record = value;
24
+ const ignored = record.ignoredBuilds;
25
+ const pending = record.pendingBuilds ?? [];
26
+ return (Array.isArray(ignored) &&
27
+ ignored.length === 0 &&
28
+ Array.isArray(pending) &&
29
+ pending.length === 0);
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ }
18
35
  async function lockfileDependencyIds() {
19
36
  try {
20
37
  const lockfile = parse(await readFile(path.join(process.cwd(), 'pnpm-lock.yaml'), 'utf8'));
@@ -41,6 +41,12 @@ case " $* " in
41
41
  if [ "\${PNPM_REBUILD_BREAK_LINK:-0}" = 1 ]; then rm -f "$PNPM_DEPENDENCY_LINK"; fi
42
42
  ;;
43
43
  *' --force '*)
44
+ if [ "\${PNPM_DELETE_NATIVE:-0}" = 1 ]; then
45
+ rm -f "$PNPM_NATIVE_ADDON"
46
+ elif [ "\${PNPM_REPAIR_NATIVE:-0}" = 1 ]; then
47
+ cp "$PNPM_NATIVE_REPLACEMENT" "$PNPM_NATIVE_ADDON"
48
+ fi
49
+ if [ "\${PNPM_FORCE_BREAK_LINK:-0}" = 1 ]; then rm -f "$PNPM_DEPENDENCY_LINK"; fi
44
50
  if [ "\${PNPM_REPAIR_LINK:-0}" = 1 ]; then
45
51
  mkdir -p "$(dirname "$PNPM_DEPENDENCY_LINK")"
46
52
  rm -f "$PNPM_DEPENDENCY_LINK"
@@ -14,6 +14,7 @@ export declare function makeFixture(): Promise<{
14
14
  root: string;
15
15
  summary: string;
16
16
  }>;
17
+ export declare function configureNativeRepair(fixture: Awaited<ReturnType<typeof makeFixture>>, addon: string): Promise<void>;
17
18
  export declare function runInstaller(fixture: Awaited<ReturnType<typeof makeFixture>>, options?: FixtureOptions): Promise<{
18
19
  stdout: string;
19
20
  stderr: string;
@@ -45,9 +45,14 @@ export async function makeFixture() {
45
45
  PNPM_CALLS: join(root, 'pnpm.calls'),
46
46
  PNPM_DEPENDENCY: dependency,
47
47
  PNPM_DEPENDENCY_LINK: dependencyLink,
48
+ PNPM_DELETE_NATIVE: '0',
48
49
  PNPM_LOG: pnpmLog,
49
50
  PNPM_NODE_MODULES: join(root, 'node_modules'),
51
+ PNPM_NATIVE_ADDON: '',
52
+ PNPM_NATIVE_REPLACEMENT: '',
50
53
  PNPM_PENDING_BUILDS: '',
54
+ PNPM_FORCE_BREAK_LINK: '0',
55
+ PNPM_REPAIR_NATIVE: '0',
51
56
  PNPM_REPAIR_LINK: '0',
52
57
  PNPM_REBUILD_BREAK_LINK: '0',
53
58
  PNPM_WORKSPACES_JSON: JSON.stringify(workspaces),
@@ -62,6 +67,18 @@ export async function makeFixture() {
62
67
  summary,
63
68
  };
64
69
  }
70
+ export async function configureNativeRepair(fixture, addon) {
71
+ const replacement = join(fixture.root, 'native-replacement.node');
72
+ const magic = process.platform === 'darwin'
73
+ ? Buffer.from([0xcf, 0xfa, 0xed, 0xfe])
74
+ : process.platform === 'win32'
75
+ ? Buffer.from([0x4d, 0x5a])
76
+ : Buffer.from([0x7f, 0x45, 0x4c, 0x46]);
77
+ await writeFile(replacement, magic);
78
+ fixture.env.PNPM_NATIVE_ADDON = addon;
79
+ fixture.env.PNPM_NATIVE_REPLACEMENT = replacement;
80
+ fixture.env.PNPM_REPAIR_NATIVE = '1';
81
+ }
65
82
  export async function runInstaller(fixture, options = {}) {
66
83
  const lifecycle = options.lifecycle ?? 'persistent';
67
84
  const installScripts = options.installScripts ?? true;
@@ -1,9 +1,12 @@
1
1
  import { persistentDependencyTreeIsCold, persistentMetadataFingerprintV4, persistentMetadataStatusV4, writePersistentMetadataStampV4, } from './metadata.mjs';
2
2
  import { runPnpm } from './exec.mjs';
3
- import { nativeBinariesMatchRuntime } from './native-health.mjs';
4
- import { clearPendingDependencyBuilds, pendingBuildDelta, pendingBuilds, validDependencyBuildIds, } from './pending-builds.mjs';
5
- import { install, reconcileOrFail, withScriptPolicy } from './install-operations.mjs';
6
- import { baseInstallArgs, findWorkspaceLinkMismatches, logWorkspaceLinkMismatches, } from './support.mjs';
3
+ import { mismatchedNativeBinaries } from './native-health.mjs';
4
+ // oxfmt-ignore
5
+ import { clearPendingDependencyBuilds, pendingBuildDelta, pendingBuilds, validDependencyBuildIds } from './pending-builds.mjs';
6
+ // oxfmt-ignore
7
+ import { install, reconcileOrFail, repairIsolatedNativeMismatch, withScriptPolicy } from './install-operations.mjs';
8
+ // oxfmt-ignore
9
+ import { baseInstallArgs, findWorkspaceLinkMismatches, logWorkspaceLinkMismatches } from './support.mjs';
7
10
  import { persistentInstallTransition, persistentProvenanceDiagnostic } from './transition.mjs';
8
11
  // oxfmt-ignore
9
12
  const fail = (message) => { throw new Error(message); };
@@ -13,7 +16,20 @@ async function persistent(options) {
13
16
  const runCapture = (args) => runPnpm(args, options, true);
14
17
  const fingerprint = await persistentMetadataFingerprintV4(runCapture);
15
18
  const provenance = await persistentMetadataStatusV4(fingerprint);
16
- const nativesMatch = await nativeBinariesMatchRuntime();
19
+ const mismatchedNatives = await mismatchedNativeBinaries();
20
+ const nativesMatch = mismatchedNatives.length === 0;
21
+ const repairedNativeMismatch = !nativesMatch &&
22
+ provenance.kind === 'matching' &&
23
+ (await repairIsolatedNativeMismatch(options, runCapture, mismatchedNatives));
24
+ if (repairedNativeMismatch) {
25
+ console.warn('persistent optional native binaries do not match this runtime; reconciled');
26
+ console.warn(persistentProvenanceDiagnostic(provenance, options.installScripts, nativesMatch, {
27
+ action: 'reconcile',
28
+ reason: 'native-health-mismatch',
29
+ }));
30
+ await writePersistentMetadataStampV4(fingerprint, options.installScripts, true, []);
31
+ return 'persistent native health reconciled';
32
+ }
17
33
  const provisionalTransition = persistentInstallTransition(provenance, options.installScripts);
18
34
  let transition = nativesMatch
19
35
  ? provisionalTransition
@@ -23,6 +23,7 @@ export type Workspace = {
23
23
  path: string;
24
24
  };
25
25
  export declare const baseInstallArgs: string[];
26
+ export declare const forcedInstallArgs: string[];
26
27
  export declare function parseInstallOptions(argv: string[]): InstallOptions;
27
28
  export declare function listWorkspaces(runCapture: CaptureCommand): Promise<Workspace[]>;
28
29
  export declare function findWorkspaceLinkMismatches(runCapture: CaptureCommand): Promise<WorkspaceLinkMismatch[]>;
@@ -1,12 +1,12 @@
1
1
  import { readFile, realpath } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- export const baseInstallArgs = [
4
- 'install',
5
- '--frozen-lockfile',
3
+ const commonInstallArgs = [
6
4
  '--prefer-offline',
7
5
  '--prod=false',
8
6
  '--config.disallow-workspace-cycles=false',
9
7
  ];
8
+ export const baseInstallArgs = ['install', '--frozen-lockfile', ...commonInstallArgs];
9
+ export const forcedInstallArgs = ['install', '--frozen-lockfile', '--force', ...commonInstallArgs];
10
10
  const usage = 'usage: vouchington pnpm-install --runner-lifecycle persistent|ephemeral|ephemeral-full --install-scripts true|false [--ephemeral-workspaces <newline-separated selectors>] [--command-timeout-seconds <nonnegative integer>] [--max-attempts <positive integer>]';
11
11
  const MAX_COMMAND_TIMEOUT_SECONDS = 3600;
12
12
  const MAX_ATTEMPTS = 10;
@@ -2,6 +2,7 @@
2
2
  import { execFileSync } from 'node:child_process';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { writeVitestBlobManifest } from './index.mjs';
5
+ import { parseGitHubRunAttempt } from './run-attempt.mjs';
5
6
  function failRepository() {
6
7
  throw new Error('GITHUB_REPOSITORY is required');
7
8
  }
@@ -15,10 +16,7 @@ export function runVitestBlobManifestCli(args, env = process.env, revision = exe
15
16
  if (!suite || extra.length > 0 || !runId || !rawAttempt) {
16
17
  throw new Error('Usage: vouchington vitest-blob-manifest <suite> [reports-directory]');
17
18
  }
18
- const runAttempt = Number(rawAttempt);
19
- if (!Number.isSafeInteger(runAttempt) || runAttempt < 1) {
20
- throw new Error('GITHUB_RUN_ATTEMPT must be a positive integer');
21
- }
19
+ const runAttempt = parseGitHubRunAttempt(rawAttempt);
22
20
  writeVitestBlobManifest(directory, {
23
21
  suite,
24
22
  repository: env.GITHUB_REPOSITORY || failRepository(),
@@ -0,0 +1,2 @@
1
+ /** Writes or reads authenticated-for-run Vitest report-attempt markers. */
2
+ export declare function runVitestReportAttemptCli(args: readonly string[], env?: NodeJS.ProcessEnv, log?: (line: string) => void): void;
@@ -0,0 +1,32 @@
1
+ import { readVitestReportAttempts, writeVitestReportAttempt, } from './report-attempt.mjs';
2
+ import { parseGitHubRunAttempt } from './run-attempt.mjs';
3
+ function required(env, name) {
4
+ const value = env[name];
5
+ if (!value)
6
+ throw new Error(`${name} is required`);
7
+ return value;
8
+ }
9
+ function identity(env) {
10
+ return {
11
+ repository: required(env, 'GITHUB_REPOSITORY'),
12
+ revision: required(env, 'GITHUB_SHA'),
13
+ runId: required(env, 'GITHUB_RUN_ID'),
14
+ attempt: parseGitHubRunAttempt(env.GITHUB_RUN_ATTEMPT),
15
+ };
16
+ }
17
+ /** Writes or reads authenticated-for-run Vitest report-attempt markers. */
18
+ export function runVitestReportAttemptCli(args, env = process.env, log = (line) => process.stdout.write(`${line}\n`)) {
19
+ const [command, path, suite, ...extra] = args;
20
+ if (extra.length > 0 || !path || (command !== 'write' && command !== 'read'))
21
+ throw new Error('Usage: vouchington vitest-report-attempt <write DIRECTORY SUITE|read ROOT>');
22
+ const current = identity(env);
23
+ if (command === 'write') {
24
+ if (!suite)
25
+ throw new Error('Usage: vouchington vitest-report-attempt <write DIRECTORY SUITE|read ROOT>');
26
+ writeVitestReportAttempt(path, suite, current);
27
+ return;
28
+ }
29
+ if (suite)
30
+ throw new Error('Usage: vouchington vitest-report-attempt <write DIRECTORY SUITE|read ROOT>');
31
+ log(JSON.stringify(readVitestReportAttempts(path, current)));
32
+ }
@@ -0,0 +1,2 @@
1
+ /** Validates GitHub run context, then prepares the selected report JSON files. */
2
+ export declare function runPrepareVitestReportsCli(args: readonly string[], env?: NodeJS.ProcessEnv, log?: (line: string) => void): void;
@@ -0,0 +1,55 @@
1
+ import { VITEST_SUITE_PATTERN } from './constants.mjs';
2
+ import { prepareVitestReports } from './reports.mjs';
3
+ import { parseGitHubRunAttempt } from './run-attempt.mjs';
4
+ function required(env, name) {
5
+ const value = env[name];
6
+ if (!value)
7
+ throw new Error(`${name} is required`);
8
+ return value;
9
+ }
10
+ function parseContext(raw, attempt) {
11
+ const parsed = JSON.parse(raw);
12
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
13
+ throw new Error('Vitest report expectation context must be an object');
14
+ const context = parsed;
15
+ if (Object.keys(context).toSorted().join('\0') !== ['attempt', 'suites', 'version'].join('\0') ||
16
+ context.version !== 'vitest-report-expectations:v2' ||
17
+ context.attempt !== attempt ||
18
+ !Array.isArray(context.suites) ||
19
+ context.suites.some((expectation) => typeof expectation !== 'object' ||
20
+ expectation === null ||
21
+ Array.isArray(expectation) ||
22
+ Object.keys(expectation).toSorted().join('\0') !== ['minimumAttempt', 'suite'].join('\0') ||
23
+ typeof expectation.suite !== 'string' ||
24
+ !VITEST_SUITE_PATTERN.test(expectation.suite) ||
25
+ !Number.isSafeInteger(expectation.minimumAttempt) ||
26
+ Number(expectation.minimumAttempt) < 1 ||
27
+ Number(expectation.minimumAttempt) > attempt))
28
+ throw new Error('Vitest report expectation context has an invalid schema');
29
+ const typed = context;
30
+ const suites = typed.suites.map((expectation) => expectation.suite);
31
+ if (new Set(suites).size !== suites.length || suites.join('\0') !== suites.toSorted().join('\0'))
32
+ throw new Error('Vitest report expectation suites must be unique and sorted');
33
+ return typed;
34
+ }
35
+ /** Validates GitHub run context, then prepares the selected report JSON files. */
36
+ export function runPrepareVitestReportsCli(args, env = process.env, log = (line) => process.stdout.write(`${line}\n`)) {
37
+ const [primaryDir = './vitest-blob-primary', fallbackDir = './vitest-blob-fallback', outputDir = './vitest-blob-reports/merge-input', ...extra] = args;
38
+ if (extra.length > 0)
39
+ throw new Error('Expected at most three Vitest report directories');
40
+ const currentAttempt = parseGitHubRunAttempt(env.GITHUB_RUN_ATTEMPT);
41
+ const result = prepareVitestReports({
42
+ primaryDir,
43
+ fallbackDir,
44
+ outputDir,
45
+ expectedSuites: parseContext(required(env, 'VITEST_REPORT_EXPECTATIONS'), currentAttempt)
46
+ .suites,
47
+ repository: required(env, 'GITHUB_REPOSITORY'),
48
+ revision: required(env, 'GITHUB_SHA'),
49
+ run: { id: required(env, 'GITHUB_RUN_ID'), currentAttempt },
50
+ });
51
+ for (const rejected of result.rejectedSources)
52
+ log(`::warning::Rejected Vitest ${rejected.source} report source: ${rejected.reason}`);
53
+ for (const selected of result.selected)
54
+ log(`Selected Vitest report ${selected.suite} from attempt ${selected.attempt} (${selected.sources.join('+')})`);
55
+ }
@@ -0,0 +1,2 @@
1
+ /** Reads a GitHub Actions attempt as a canonical, positive decimal integer. */
2
+ export declare function parseGitHubRunAttempt(rawAttempt: string | undefined): number;
@@ -0,0 +1,11 @@
1
+ /** Reads a GitHub Actions attempt as a canonical, positive decimal integer. */
2
+ export function parseGitHubRunAttempt(rawAttempt) {
3
+ if (!rawAttempt)
4
+ throw new Error('GITHUB_RUN_ATTEMPT is required');
5
+ if (!/^[1-9][0-9]*$/.test(rawAttempt))
6
+ throw new Error('GITHUB_RUN_ATTEMPT must be a positive integer');
7
+ const attempt = Number(rawAttempt);
8
+ if (!Number.isSafeInteger(attempt))
9
+ throw new Error('GITHUB_RUN_ATTEMPT must be a positive integer');
10
+ return attempt;
11
+ }
@@ -0,0 +1,3 @@
1
+ import type { SharedContext } from '../shared-context/index.mts';
2
+ /** Keeps Dependabot updates independent from literal dependency-version test assertions. */
3
+ export declare function checkManifestDependencyVersionAssertions(ctx: SharedContext, errors: string[]): void;
@@ -0,0 +1,100 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { findExpectations } from './manifest-version-parser.mjs';
4
+ import { buildDependencyMatchers, SEMVER_LITERAL } from './manifest-version-patterns.mjs';
5
+ const TEST_SOURCE_FILE = /(?:^|\/)[^/]+\.(?:test|spec)\.[cm]?[jt]sx?$/u;
6
+ const PACKAGE_JSON_FILE = /(?:^|\/)package\.json$/u;
7
+ const TEST_OR_FIXTURE_DIRECTORY = /(?:^|\/)(?:test|tests|__tests__|fixture|fixtures|__fixtures__)(?:\/|$)/u;
8
+ const DEPENDENCY_FIELD = /\b(?:dependencies|devDependencies|optionalDependencies|peerDependencies)\b/u;
9
+ const STRING_LITERAL = /'([^'\\]*(?:\\.[^'\\]*)*)'|"([^"\\]*(?:\\.[^"\\]*)*)"|`([^`\\$]*(?:\\.[^`\\$]*)*)`/gu;
10
+ function readTrackedSource(ctx, file) {
11
+ try {
12
+ return ctx.readTrackedFile
13
+ ? ctx.readTrackedFile(file)
14
+ : readFileSync(join(ctx.repoRoot, file), 'utf8');
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ }
20
+ function dependencyNames(ctx) {
21
+ const names = new Set();
22
+ for (const file of ctx.trackedFiles) {
23
+ if (!PACKAGE_JSON_FILE.test(file) || TEST_OR_FIXTURE_DIRECTORY.test(file))
24
+ continue;
25
+ const source = readTrackedSource(ctx, file);
26
+ if (source === null)
27
+ continue;
28
+ let manifest;
29
+ try {
30
+ manifest = JSON.parse(source);
31
+ }
32
+ catch {
33
+ continue;
34
+ }
35
+ if (typeof manifest !== 'object' || manifest === null || Array.isArray(manifest))
36
+ continue;
37
+ for (const field of [
38
+ 'dependencies',
39
+ 'devDependencies',
40
+ 'optionalDependencies',
41
+ 'peerDependencies',
42
+ ]) {
43
+ const dependencies = manifest[field];
44
+ if (typeof dependencies !== 'object' || dependencies === null || Array.isArray(dependencies))
45
+ continue;
46
+ for (const name of Object.keys(dependencies))
47
+ names.add(name);
48
+ }
49
+ }
50
+ return names;
51
+ }
52
+ function literalValues(source) {
53
+ return [...source.matchAll(STRING_LITERAL)].map((match) => (match[1] ?? match[2] ?? match[3]));
54
+ }
55
+ function expectedDependencyNames(values, names) {
56
+ return values.some((value) => SEMVER_LITERAL.test(value))
57
+ ? values.filter((value) => names.has(value))
58
+ : [];
59
+ }
60
+ /** Keeps Dependabot updates independent from literal dependency-version test assertions. */
61
+ export function checkManifestDependencyVersionAssertions(ctx, errors) {
62
+ const names = dependencyNames(ctx);
63
+ if (names.size === 0)
64
+ return;
65
+ const matchers = buildDependencyMatchers(names);
66
+ for (const file of ctx.trackedFiles) {
67
+ if (!TEST_SOURCE_FILE.test(file))
68
+ continue;
69
+ const source = readTrackedSource(ctx, file);
70
+ if (source === null) {
71
+ errors.push(`::error file=${file}::${file}: failed to read test source for manifest dependency version assertions`);
72
+ continue;
73
+ }
74
+ for (const expectation of findExpectations(source)) {
75
+ const asserted = new Set();
76
+ if (DEPENDENCY_FIELD.test(expectation.expression)) {
77
+ const values = literalValues(expectation.expected);
78
+ if (values.length === 1 && SEMVER_LITERAL.test(values[0])) {
79
+ for (const matcher of matchers)
80
+ if (matcher.member.test(expectation.expression))
81
+ asserted.add(matcher.name);
82
+ }
83
+ for (const name of expectedDependencyNames(values, names))
84
+ asserted.add(name);
85
+ for (const matcher of matchers)
86
+ if (matcher.objectValue.test(expectation.expected))
87
+ asserted.add(matcher.name);
88
+ }
89
+ for (const value of literalValues(expectation.expected)) {
90
+ for (const matcher of matchers)
91
+ if (matcher.packageSpec.test(value))
92
+ asserted.add(matcher.name);
93
+ }
94
+ for (const name of asserted) {
95
+ const line = source.slice(0, expectation.index).split('\n').length;
96
+ errors.push(`::error file=${file},line=${line}::${file}:${line}: tests must not assert the exact version of declared dependency "${name}"; assert dependency membership or derive the value from the manifest instead`);
97
+ }
98
+ }
99
+ }
100
+ }
@@ -0,0 +1,6 @@
1
+ export type Expectation = {
2
+ expression: string;
3
+ expected: string;
4
+ index: number;
5
+ };
6
+ export declare function findExpectations(source: string): Expectation[];
@@ -0,0 +1,104 @@
1
+ const EXPECTATION_MATCHER = /^\s*(?:\.|\?\.)\s*(?:not\s*\.\s*)?to(?:Be|Equal|StrictEqual|MatchObject|Contain|ContainEqual|HaveProperty)\s*\(/u;
2
+ function previousToken(source, index) {
3
+ for (let cursor = index - 1; cursor >= 0; cursor -= 1) {
4
+ if (!/\s/u.test(source[cursor]))
5
+ return source[cursor];
6
+ }
7
+ return undefined;
8
+ }
9
+ function skipRegex(source, index) {
10
+ const previous = previousToken(source, index);
11
+ if (previous && /[\w$)\]]/u.test(previous))
12
+ return index;
13
+ let characterClass = false;
14
+ for (let cursor = index + 1; cursor < source.length; cursor += 1) {
15
+ if (source[cursor] === '\\') {
16
+ cursor += 1;
17
+ continue;
18
+ }
19
+ if (source[cursor] === '[')
20
+ characterClass = true;
21
+ if (source[cursor] === ']')
22
+ characterClass = false;
23
+ if (source[cursor] === '/' && !characterClass)
24
+ return cursor + 1;
25
+ if (source[cursor] === '\n')
26
+ return index;
27
+ }
28
+ return index;
29
+ }
30
+ function skipLiteralOrComment(source, index) {
31
+ const marker = source[index];
32
+ if (marker === '/' && source[index + 1] === '/') {
33
+ const end = source.indexOf('\n', index + 2);
34
+ return end === -1 ? source.length : end;
35
+ }
36
+ if (marker === '/' && source[index + 1] === '*') {
37
+ const end = source.indexOf('*/', index + 2);
38
+ return end === -1 ? source.length : end + 2;
39
+ }
40
+ if (marker === '/')
41
+ return skipRegex(source, index);
42
+ if (marker !== "'" && marker !== '"' && marker !== '`')
43
+ return index;
44
+ for (let cursor = index + 1; cursor < source.length; cursor += 1) {
45
+ if (source[cursor] === '\\') {
46
+ cursor += 1;
47
+ continue;
48
+ }
49
+ if (source[cursor] === marker)
50
+ return cursor + 1;
51
+ }
52
+ return source.length;
53
+ }
54
+ function closingParenthesis(source, open) {
55
+ let depth = 1;
56
+ for (let cursor = open + 1; cursor < source.length; cursor += 1) {
57
+ const skipped = skipLiteralOrComment(source, cursor);
58
+ if (skipped !== cursor) {
59
+ cursor = skipped - 1;
60
+ continue;
61
+ }
62
+ if (source[cursor] === '(')
63
+ depth += 1;
64
+ if (source[cursor] === ')')
65
+ depth -= 1;
66
+ if (depth === 0)
67
+ return cursor;
68
+ }
69
+ return -1;
70
+ }
71
+ export function findExpectations(source) {
72
+ const found = [];
73
+ for (let cursor = 0; cursor < source.length; cursor += 1) {
74
+ const skipped = skipLiteralOrComment(source, cursor);
75
+ if (skipped !== cursor) {
76
+ cursor = skipped - 1;
77
+ continue;
78
+ }
79
+ if (!source.startsWith('expect', cursor) || /[\w$]/u.test(source[cursor - 1] ?? ''))
80
+ continue;
81
+ let open = cursor + 'expect'.length;
82
+ while (/\s/u.test(source[open]))
83
+ open += 1;
84
+ if (source[open] !== '(')
85
+ continue;
86
+ const expressionEnd = closingParenthesis(source, open);
87
+ if (expressionEnd === -1)
88
+ continue;
89
+ const matcher = source.slice(expressionEnd + 1).match(EXPECTATION_MATCHER);
90
+ if (!matcher)
91
+ continue;
92
+ const expectedOpen = expressionEnd + 1 + matcher[0].length - 1;
93
+ const expectedEnd = closingParenthesis(source, expectedOpen);
94
+ if (expectedEnd === -1)
95
+ continue;
96
+ found.push({
97
+ expression: source.slice(open + 1, expressionEnd),
98
+ expected: source.slice(expectedOpen + 1, expectedEnd),
99
+ index: cursor,
100
+ });
101
+ cursor = expectedEnd;
102
+ }
103
+ return found;
104
+ }
@@ -0,0 +1,9 @@
1
+ export declare const SEMVER_SOURCE = "[~^<>=*v\\s]*\\d+\\.\\d+(?:\\.\\d+)?(?:[-+][0-9A-Za-z.-]+)?";
2
+ export declare const SEMVER_LITERAL: RegExp;
3
+ export type DependencyMatcher = {
4
+ readonly name: string;
5
+ readonly member: RegExp;
6
+ readonly objectValue: RegExp;
7
+ readonly packageSpec: RegExp;
8
+ };
9
+ export declare function buildDependencyMatchers(names: ReadonlySet<string>): DependencyMatcher[];
@@ -0,0 +1,21 @@
1
+ export const SEMVER_SOURCE = '[~^<>=*v\\s]*\\d+\\.\\d+(?:\\.\\d+)?(?:[-+][0-9A-Za-z.-]+)?';
2
+ export const SEMVER_LITERAL = new RegExp(`^${SEMVER_SOURCE}$`, 'u');
3
+ function escapeRegex(value) {
4
+ return value.replaceAll(/[.*+?^${}()|[\]\\]/gu, '\\$&');
5
+ }
6
+ export function buildDependencyMatchers(names) {
7
+ return [...names].map((name) => {
8
+ const escaped = escapeRegex(name);
9
+ const plainName = /^[A-Za-z_$][\w$]*$/u.test(name);
10
+ const member = plainName
11
+ ? `(?:\\[\\s*['"]${escaped}['"]\\s*\\]|\\.\\s*${escaped})`
12
+ : `\\[\\s*['"]${escaped}['"]\\s*\\]`;
13
+ const key = plainName ? `(?:${escaped}|['"]${escaped}['"])` : `['"]${escaped}['"]`;
14
+ return {
15
+ name,
16
+ member: new RegExp(member, 'u'),
17
+ objectValue: new RegExp(`${key}\\s*:\\s*(['"])${SEMVER_SOURCE}\\1`, 'u'),
18
+ packageSpec: new RegExp(`^${escaped}@${SEMVER_SOURCE}$`, 'u'),
19
+ };
20
+ });
21
+ }
@@ -1,12 +1,14 @@
1
1
  import { validateReleaseAgeExemptionGroups } from '../pnpm-install/index.mjs';
2
2
  import { checkDependabotCrosslist } from './dependabot.mjs';
3
3
  import { checkExcludeRegistry } from './exclude-registry.mjs';
4
+ import { checkManifestDependencyVersionAssertions } from './manifest-version-assertions.mjs';
4
5
  import { checkActiveFirstPartyGraph, checkTemporaryReleaseAgeSelectorsInLockfile, } from './first-party-graph.mjs';
5
6
  import { resolveWorkspaceGatesOptions, shouldValidateTemporaryGroups, } from './options.mjs';
6
7
  import { loadYaml } from './yaml-loader.mjs';
7
8
  export async function checkWorkspaceGatesPolicy(ctx, options) {
8
9
  const errors = [];
9
10
  const resolved = resolveWorkspaceGatesOptions(options);
11
+ checkManifestDependencyVersionAssertions(ctx, errors);
10
12
  if (shouldValidateTemporaryGroups(options) && options.temporaryGroups !== undefined) {
11
13
  for (const error of validateReleaseAgeExemptionGroups(options.temporaryGroups)) {
12
14
  errors.push(`::error::${error}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vouchington-tooling",
3
- "version": "0.3.6",
3
+ "version": "0.4.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": {
@@ -255,7 +255,7 @@
255
255
  "csv-parse": "7.0.2",
256
256
  "csv-stringify": "6.8.3",
257
257
  "dockerfile-ast": "0.7.1",
258
- "picomatch": "4.0.5",
258
+ "picomatch": "4.0.7",
259
259
  "yaml": "2.9.0"
260
260
  },
261
261
  "devDependencies": {