vouchington-tooling 0.0.13 → 0.0.14

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 (44) hide show
  1. package/README.md +16 -2
  2. package/dist/ast-grep-rule/index.d.mts +13 -0
  3. package/dist/ast-grep-rule/index.mjs +43 -0
  4. package/dist/csv/index.d.mts +5 -0
  5. package/dist/csv/index.mjs +31 -0
  6. package/dist/gha-review-payload/diff.d.mts +30 -0
  7. package/dist/gha-review-payload/diff.mjs +121 -0
  8. package/dist/gha-review-payload/file.d.mts +7 -0
  9. package/dist/gha-review-payload/file.mjs +62 -0
  10. package/dist/gha-review-payload/index.d.mts +7 -0
  11. package/dist/gha-review-payload/index.mjs +4 -0
  12. package/dist/gha-review-payload/payload.d.mts +30 -0
  13. package/dist/gha-review-payload/payload.mjs +111 -0
  14. package/dist/gha-review-payload/remap.d.mts +13 -0
  15. package/dist/gha-review-payload/remap.mjs +89 -0
  16. package/dist/http-body/index.d.mts +18 -0
  17. package/dist/http-body/index.mjs +86 -0
  18. package/dist/http-link-pagination/index.d.mts +19 -0
  19. package/dist/http-link-pagination/index.mjs +149 -0
  20. package/dist/index.d.mts +16 -2
  21. package/dist/index.mjs +10 -1
  22. package/dist/pnpm-install/index.d.mts +2 -0
  23. package/dist/pnpm-install/index.mjs +1 -0
  24. package/dist/pnpm-install/release-age-policy-types.d.mts +34 -0
  25. package/dist/pnpm-install/release-age-policy-types.mjs +1 -0
  26. package/dist/pnpm-install/release-age-policy.d.mts +31 -0
  27. package/dist/pnpm-install/release-age-policy.mjs +136 -0
  28. package/dist/transient-retry/attempts.d.mts +5 -0
  29. package/dist/transient-retry/attempts.mjs +8 -0
  30. package/dist/transient-retry/decision-evaluator.d.mts +18 -0
  31. package/dist/transient-retry/decision-evaluator.mjs +56 -0
  32. package/dist/transient-retry/index.d.mts +4 -0
  33. package/dist/transient-retry/index.mjs +2 -0
  34. package/dist/transient-retry/types.d.mts +39 -0
  35. package/dist/transient-retry/types.mjs +1 -0
  36. package/dist/vitest-blob-manifest/constants.d.mts +1 -0
  37. package/dist/vitest-blob-manifest/constants.mjs +1 -0
  38. package/dist/vitest-blob-manifest/index.d.mts +2 -1
  39. package/dist/vitest-blob-manifest/index.mjs +3 -1
  40. package/dist/vitest-blob-manifest/report-attempt.d.mts +24 -0
  41. package/dist/vitest-blob-manifest/report-attempt.mjs +127 -0
  42. package/dist/vitest-blob-manifest/reports.d.mts +28 -0
  43. package/dist/vitest-blob-manifest/reports.mjs +160 -0
  44. package/package.json +38 -1
@@ -0,0 +1,86 @@
1
+ export class MissingResponseBodyError extends Error {
2
+ url;
3
+ constructor(url) {
4
+ super(`HTTP response from ${url} has no body`);
5
+ this.name = 'MissingResponseBodyError';
6
+ this.url = url;
7
+ }
8
+ }
9
+ export class ResponseBodyTooLargeError extends Error {
10
+ url;
11
+ sizeBytes;
12
+ maxSizeBytes;
13
+ constructor(url, sizeBytes, maxSizeBytes) {
14
+ super(`HTTP response from ${url} exceeded ${maxSizeBytes} bytes (read ${sizeBytes})`);
15
+ this.name = 'ResponseBodyTooLargeError';
16
+ this.url = url;
17
+ this.sizeBytes = sizeBytes;
18
+ this.maxSizeBytes = maxSizeBytes;
19
+ }
20
+ }
21
+ function abortReason(signal) {
22
+ if (signal.reason instanceof Error)
23
+ return signal.reason;
24
+ if (signal.reason !== undefined)
25
+ return new Error(String(signal.reason));
26
+ return new Error('HTTP response body read aborted');
27
+ }
28
+ function validateMaxSize(maxSizeBytes) {
29
+ if (!Number.isSafeInteger(maxSizeBytes) || maxSizeBytes < 0) {
30
+ throw new RangeError('maxSizeBytes must be a non-negative safe integer');
31
+ }
32
+ }
33
+ export async function readResponseBodyAsBuffer({ response, url, maxSizeBytes, signal, }) {
34
+ validateMaxSize(maxSizeBytes);
35
+ const reader = response.body?.getReader();
36
+ if (!reader)
37
+ throw new MissingResponseBodyError(url);
38
+ let abortListener;
39
+ let abortPromise;
40
+ if (signal) {
41
+ abortPromise = new Promise((_resolve, reject) => {
42
+ abortListener = () => {
43
+ const reason = abortReason(signal);
44
+ void reader.cancel(reason).catch(() => undefined);
45
+ reject(reason);
46
+ };
47
+ if (signal.aborted)
48
+ abortListener();
49
+ else
50
+ signal.addEventListener('abort', abortListener, { once: true });
51
+ });
52
+ void abortPromise.catch(() => undefined);
53
+ }
54
+ const chunks = [];
55
+ let totalSize = 0;
56
+ try {
57
+ for (;;) {
58
+ const result = await (abortPromise
59
+ ? Promise.race([reader.read(), abortPromise])
60
+ : reader.read());
61
+ if (signal?.aborted)
62
+ throw abortReason(signal);
63
+ if (result.done)
64
+ return Buffer.concat(chunks, totalSize);
65
+ totalSize += result.value.byteLength;
66
+ if (totalSize > maxSizeBytes) {
67
+ const error = new ResponseBodyTooLargeError(url, totalSize, maxSizeBytes);
68
+ await reader.cancel(error).catch(() => undefined);
69
+ throw error;
70
+ }
71
+ chunks.push(result.value);
72
+ }
73
+ }
74
+ catch (error) {
75
+ await reader.cancel(error).catch(() => undefined);
76
+ throw error;
77
+ }
78
+ finally {
79
+ if (signal && abortListener)
80
+ signal.removeEventListener('abort', abortListener);
81
+ reader.releaseLock();
82
+ }
83
+ }
84
+ export async function readResponseBody(options) {
85
+ return (await readResponseBodyAsBuffer(options)).toString('utf8');
86
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Resolve the `rel=next` target in an RFC 8288 Link header without permitting a
3
+ * response to redirect an authenticated pagination client to another origin.
4
+ *
5
+ * A malformed or unsafe Link header is treated as having no next page. Callers
6
+ * can therefore stop safely without having to distinguish an exhausted list
7
+ * from a provider that returned an unusable continuation.
8
+ */
9
+ export declare function nextPageUrlFromLinkHeader(linkHeader: string | null | undefined, requestUrl: string | URL): URL | null;
10
+ /**
11
+ * Return an unambiguous next-page cursor from a safe Link header target.
12
+ *
13
+ * A missing, empty, or repeated query parameter is rejected. This prevents a
14
+ * caller from accidentally advancing with a cursor whose interpretation
15
+ * depends on a provider's duplicate-query-parameter semantics.
16
+ */
17
+ export declare function nextPageCursorFromLinkHeader(linkHeader: string | null | undefined, requestUrl: string | URL, cursorParameter?: string): string | null;
18
+ /** Validate a pagination request URL before it is used as a Link resolution base. */
19
+ export declare function validatePaginationRequestUrl(requestUrl: string | URL): URL;
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Resolve the `rel=next` target in an RFC 8288 Link header without permitting a
3
+ * response to redirect an authenticated pagination client to another origin.
4
+ *
5
+ * A malformed or unsafe Link header is treated as having no next page. Callers
6
+ * can therefore stop safely without having to distinguish an exhausted list
7
+ * from a provider that returned an unusable continuation.
8
+ */
9
+ export function nextPageUrlFromLinkHeader(linkHeader, requestUrl) {
10
+ const baseUrl = validatePaginationRequestUrl(requestUrl);
11
+ // Preserve the public fail-closed result for an empty header while letting the
12
+ // parser own the empty-input case.
13
+ if (linkHeader == null)
14
+ return null;
15
+ const links = parseLinkHeader(linkHeader);
16
+ if (!links)
17
+ return null;
18
+ const nextLinks = links.filter((link) => link.relations.includes('next'));
19
+ if (nextLinks.length !== 1)
20
+ return null;
21
+ let nextUrl;
22
+ try {
23
+ nextUrl = new URL(nextLinks[0].target, baseUrl);
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ if (nextUrl.origin !== baseUrl.origin ||
29
+ nextUrl.username !== '' ||
30
+ nextUrl.password !== '' ||
31
+ nextUrl.hash !== '') {
32
+ return null;
33
+ }
34
+ return nextUrl;
35
+ }
36
+ /**
37
+ * Return an unambiguous next-page cursor from a safe Link header target.
38
+ *
39
+ * A missing, empty, or repeated query parameter is rejected. This prevents a
40
+ * caller from accidentally advancing with a cursor whose interpretation
41
+ * depends on a provider's duplicate-query-parameter semantics.
42
+ */
43
+ export function nextPageCursorFromLinkHeader(linkHeader, requestUrl, cursorParameter = 'page') {
44
+ const nextUrl = nextPageUrlFromLinkHeader(linkHeader, requestUrl);
45
+ if (!nextUrl)
46
+ return null;
47
+ const values = nextUrl.searchParams.getAll(cursorParameter);
48
+ return values.length === 1 && values[0] !== '' ? values[0] : null;
49
+ }
50
+ /** Validate a pagination request URL before it is used as a Link resolution base. */
51
+ export function validatePaginationRequestUrl(requestUrl) {
52
+ let url;
53
+ try {
54
+ url = new URL(requestUrl);
55
+ }
56
+ catch {
57
+ throw new Error('pagination request URL must be an absolute HTTP(S) URL without credentials');
58
+ }
59
+ if (!['http:', 'https:'].includes(url.protocol) ||
60
+ url.username !== '' ||
61
+ url.password !== '' ||
62
+ url.hash !== '') {
63
+ throw new Error('pagination request URL must be an absolute HTTP(S) URL without credentials');
64
+ }
65
+ return url;
66
+ }
67
+ function parseLinkHeader(header) {
68
+ const links = [];
69
+ let index = 0;
70
+ while (index < header.length) {
71
+ index = skipWhitespace(header, index);
72
+ if (header[index] !== '<')
73
+ return null;
74
+ const targetEnd = header.indexOf('>', index + 1);
75
+ if (targetEnd === -1)
76
+ return null;
77
+ const target = header.slice(index + 1, targetEnd);
78
+ if (target === '')
79
+ return null;
80
+ index = targetEnd + 1;
81
+ const relations = [];
82
+ while (true) {
83
+ index = skipWhitespace(header, index);
84
+ if (index === header.length || header[index] === ',')
85
+ break;
86
+ if (header[index] !== ';')
87
+ return null;
88
+ index = skipWhitespace(header, index + 1);
89
+ const name = readToken(header, index);
90
+ if (!name)
91
+ return null;
92
+ index = name.end;
93
+ index = skipWhitespace(header, index);
94
+ if (header[index] !== '=')
95
+ return null;
96
+ index = skipWhitespace(header, index + 1);
97
+ const value = readParameterValue(header, index);
98
+ if (!value)
99
+ return null;
100
+ index = value.end;
101
+ if (name.value.toLowerCase() === 'rel') {
102
+ relations.push(...value.value.toLowerCase().split(/\s+/).filter(Boolean));
103
+ }
104
+ }
105
+ links.push({ target, relations });
106
+ if (index === header.length)
107
+ return links;
108
+ index += 1;
109
+ if (index === header.length)
110
+ return null;
111
+ }
112
+ return null;
113
+ }
114
+ function skipWhitespace(value, index) {
115
+ while (value[index] === ' ' || value[index] === '\t')
116
+ index += 1;
117
+ return index;
118
+ }
119
+ function readToken(value, index) {
120
+ const start = index;
121
+ while (index < value.length && isTokenCharacter(value[index]))
122
+ index += 1;
123
+ return index === start ? null : { value: value.slice(start, index), end: index };
124
+ }
125
+ function isTokenCharacter(value) {
126
+ return /^[!#$%&'*+\-.^_`|~0-9A-Za-z]$/.test(value);
127
+ }
128
+ function readParameterValue(value, index) {
129
+ if (value[index] !== '"')
130
+ return readToken(value, index);
131
+ let parsed = '';
132
+ index += 1;
133
+ while (index < value.length) {
134
+ const character = value[index];
135
+ if (character === '"')
136
+ return { value: parsed, end: index + 1 };
137
+ if (character === '\\') {
138
+ const escaped = value[index + 1];
139
+ if (!escaped)
140
+ return null;
141
+ parsed += escaped;
142
+ index += 2;
143
+ continue;
144
+ }
145
+ parsed += character;
146
+ index += 1;
147
+ }
148
+ return null;
149
+ }
package/dist/index.d.mts CHANGED
@@ -5,10 +5,14 @@ export type { ForeignKey, SqlCreateIndexMetadata, SqlCreateTableColumn, SqlCreat
5
5
  export { dollarQuoteEnd, lineOf, maskSqlQuotedText, readDollarQuoteDelimiter, readStringLiteral, splitSqlStatements, sqlFragments, stripSqlComments, } from './sql-scanner/index.mts';
6
6
  export { auditCiJobRuntime, parseWorkflowNameMatch } from './gha-runtime-audit/index.mts';
7
7
  export type { GhApiExecutor, RuntimeAuditOptions, RuntimeAuditResult, RuntimeAuditWorkflowFilter, RuntimeJobResult, RuntimeSample, } from './gha-runtime-audit/index.mts';
8
- export { createVitestBlobManifest, inspectVitestBlobBundle, parseVitestBlobManifest, serializeVitestBlobManifest, VITEST_BLOB_MANIFEST_FILENAME, VITEST_BLOB_MANIFEST_VERSION, vitestBlobBundlePaths, writeVitestBlobManifest, } from './vitest-blob-manifest/index.mts';
9
- export type { InspectedVitestBlobBundle, VitestBlobIdentity, VitestBlobManifest, } from './vitest-blob-manifest/index.mts';
8
+ export { createVitestBlobManifest, createVitestReportAttempt, inspectVitestBlobBundle, parseVitestBlobManifest, parseVitestReportAttempt, readVitestReportAttempts, serializeVitestBlobManifest, serializeVitestReportAttempt, VITEST_BLOB_MANIFEST_FILENAME, VITEST_BLOB_MANIFEST_VERSION, VITEST_REPORT_ATTEMPT_PREFIX, VITEST_REPORT_ATTEMPT_VERSION, vitestBlobBundlePaths, writeVitestBlobManifest, writeVitestReportAttempt, } from './vitest-blob-manifest/index.mts';
9
+ export type { InspectedVitestBlobBundle, VitestBlobIdentity, VitestBlobManifest, VitestReportAttempt, VitestReportAttemptIdentity, } from './vitest-blob-manifest/index.mts';
10
+ export { prepareVitestReports } from './vitest-blob-manifest/reports.mts';
11
+ export type { PrepareVitestReportsOptions, SelectedVitestReport, VitestReportExpectation, } from './vitest-blob-manifest/reports.mts';
10
12
  export { findWorkspaceLinkMismatches, formatReleaseAgeFailure, INSTALL_TERMINATION_FAILED, isReleaseAgeViolation, parseInstallOptions, parseReleaseAgeViolations, runInstallLifecycle, } from './pnpm-install/index.mts';
11
13
  export type { InstallOptions, Lifecycle } from './pnpm-install/index.mts';
14
+ export { flattenReleaseAgeSelectors, packageNameFromPnpmLockKey, pnpmLockPackageKeyMatchesSelector, validateReleaseAgeExemptionGroups, validateReleaseAgePolicy, } from './pnpm-install/index.mts';
15
+ export type { ReleaseAgeExemptionGroup, ReleaseAgePermanentExemption, ReleaseAgePolicyConfig, ReleaseAgePolicySnapshot, } from './pnpm-install/index.mts';
12
16
  export { buildContextFromTrackedFiles, buildSharedContext, clearFakeGitEnv, gitEnv, installFakeGit, runNamedChecks, setFakeGitTrackedFiles, } from './shared-context/index.mts';
13
17
  export type { FakeGitOptions, NamedCheck, SharedContext } from './shared-context/index.mts';
14
18
  export { decodeSelectedFiles, encodeSelectedFiles, formatMultilineOutput, SELECTED_FILES_ENV_MAX_BYTES, selectedFilesExceedEnvBudget, writeSelectedFilesOutput, } from './gha-selected-files/index.mts';
@@ -20,3 +24,13 @@ export { buildSchemaSnapshot, detectRenamedIndexes, generateSchemaSnapshot, inde
20
24
  export type { CatalogQuery, PartitionPolicy, SchemaCatalog, SchemaGrowthMaps, SchemaSnapshot, SchemaTableSnapshot, } from './pg-schema-snapshot/index.mts';
21
25
  export { buildOpenApiDocument, hashContractSchema, nodeToOpenApi, writeOpenApi, } from './openapi-document/index.mts';
22
26
  export type { BuildOpenApiDocumentInput, ContractSchema, OpenApiDocument, RequestContract, ResponseContract, } from './openapi-document/index.mts';
27
+ export { decide, deriveRetryAttempt } from './transient-retry/index.mts';
28
+ export type { DecisionResult, EvaluateRulesOptions, NoMatchReason, RetryContext, RetryDecision, RetryRule, RetryTarget, } from './transient-retry/index.mts';
29
+ export { escapeSpreadsheetFormula, parseCsvRows, streamCsvRows, stripCsvBom } from './csv/index.mts';
30
+ export { MissingResponseBodyError, readResponseBody, readResponseBodyAsBuffer, ResponseBodyTooLargeError, } from './http-body/index.mts';
31
+ export type { ReadResponseBodyOptions } from './http-body/index.mts';
32
+ export { parseAstGrepRuleArgs, runAstGrepRule } from './ast-grep-rule/index.mts';
33
+ export type { AstGrepRuleInvocation, RunAstGrepRuleOptions } from './ast-grep-rule/index.mts';
34
+ export { bodyOnlyReviewFallback, indexReviewFiles, MAX_REVIEW_COMMENTS, MAX_REVIEW_PAYLOAD_BYTES, nearestReviewLine, parsePatchCommentable, parseReviewFilesJson, parseReviewPayload, readRegularReviewPayload, remapReviewComments, ReviewPayloadError, reviewCommentSubject, rewriteSnappedSuggestion, snapReviewNote, stageReviewPayload, writeStagedOutput, } from './gha-review-payload/index.mts';
35
+ export type { CommentableIndex, CommentableLine, LineKind, PayloadRequirement, ReviewComment, ReviewFile, ReviewSide, SanitizedReview, } from './gha-review-payload/index.mts';
36
+ export { nextPageCursorFromLinkHeader, nextPageUrlFromLinkHeader, validatePaginationRequestUrl, } from './http-link-pagination/index.mts';
package/dist/index.mjs CHANGED
@@ -1,9 +1,12 @@
1
+ /* eslint-disable max-lines -- package entry point enumerates the supported public API. */
1
2
  export { EphemeralListenerAttemptsExhaustedError, isRunnerReservedPort, listenOnRunnerUnreservedEphemeralPort, loadRunnerPortPolicy, runnerPortPolicy, validateRunnerPortPolicy, } from './runner-port-policy/index.mjs';
2
3
  export { extractAlterTableAddColumnLocations, extractCreateIndexMetadata, extractCreateTableMetadata, extractDefaultFunction, extractDropIndexMetadata, extractFuncCallArgColumnNames, extractMigrationConstraintMetadata, initSqlAst, lineOfUtf8ByteOffset, MissingSqlAstParserError, parseSql, } from './sql-ast/index.mjs';
3
4
  export { dollarQuoteEnd, lineOf, maskSqlQuotedText, readDollarQuoteDelimiter, readStringLiteral, splitSqlStatements, sqlFragments, stripSqlComments, } from './sql-scanner/index.mjs';
4
5
  export { auditCiJobRuntime, parseWorkflowNameMatch } from './gha-runtime-audit/index.mjs';
5
- export { createVitestBlobManifest, inspectVitestBlobBundle, parseVitestBlobManifest, serializeVitestBlobManifest, VITEST_BLOB_MANIFEST_FILENAME, VITEST_BLOB_MANIFEST_VERSION, vitestBlobBundlePaths, writeVitestBlobManifest, } from './vitest-blob-manifest/index.mjs';
6
+ export { createVitestBlobManifest, createVitestReportAttempt, inspectVitestBlobBundle, parseVitestBlobManifest, parseVitestReportAttempt, readVitestReportAttempts, serializeVitestBlobManifest, serializeVitestReportAttempt, VITEST_BLOB_MANIFEST_FILENAME, VITEST_BLOB_MANIFEST_VERSION, VITEST_REPORT_ATTEMPT_PREFIX, VITEST_REPORT_ATTEMPT_VERSION, vitestBlobBundlePaths, writeVitestBlobManifest, writeVitestReportAttempt, } from './vitest-blob-manifest/index.mjs';
7
+ export { prepareVitestReports } from './vitest-blob-manifest/reports.mjs';
6
8
  export { findWorkspaceLinkMismatches, formatReleaseAgeFailure, INSTALL_TERMINATION_FAILED, isReleaseAgeViolation, parseInstallOptions, parseReleaseAgeViolations, runInstallLifecycle, } from './pnpm-install/index.mjs';
9
+ export { flattenReleaseAgeSelectors, packageNameFromPnpmLockKey, pnpmLockPackageKeyMatchesSelector, validateReleaseAgeExemptionGroups, validateReleaseAgePolicy, } from './pnpm-install/index.mjs';
7
10
  export { buildContextFromTrackedFiles, buildSharedContext, clearFakeGitEnv, gitEnv, installFakeGit, runNamedChecks, setFakeGitTrackedFiles, } from './shared-context/index.mjs';
8
11
  export { decodeSelectedFiles, encodeSelectedFiles, formatMultilineOutput, SELECTED_FILES_ENV_MAX_BYTES, selectedFilesExceedEnvBudget, writeSelectedFilesOutput, } from './gha-selected-files/index.mjs';
9
12
  export { createArtifactClassifier, parseArtifactPatternsJson, planRunDeletions, runCleanup, sweepCleanup, } from './gha-artifacts-cleanup/index.mjs';
@@ -11,3 +14,9 @@ export { validateOptionalHttpOrigin } from './http-origin/index.mjs';
11
14
  export { boundPendingLine, DEFAULT_MAX_PENDING_LINE_LENGTH, DEFAULT_TRUNCATED_LINE_MARKER, splitCompleteLines, } from './process-line-buffer/index.mjs';
12
15
  export { buildSchemaSnapshot, detectRenamedIndexes, generateSchemaSnapshot, indexShapeKey, readSchemaCatalog, renderSchemaMarkdown, stableStringify, writeSchemaSnapshot, } from './pg-schema-snapshot/index.mjs';
13
16
  export { buildOpenApiDocument, hashContractSchema, nodeToOpenApi, writeOpenApi, } from './openapi-document/index.mjs';
17
+ export { decide, deriveRetryAttempt } from './transient-retry/index.mjs';
18
+ export { escapeSpreadsheetFormula, parseCsvRows, streamCsvRows, stripCsvBom } from './csv/index.mjs';
19
+ export { MissingResponseBodyError, readResponseBody, readResponseBodyAsBuffer, ResponseBodyTooLargeError, } from './http-body/index.mjs';
20
+ export { parseAstGrepRuleArgs, runAstGrepRule } from './ast-grep-rule/index.mjs';
21
+ export { bodyOnlyReviewFallback, indexReviewFiles, MAX_REVIEW_COMMENTS, MAX_REVIEW_PAYLOAD_BYTES, nearestReviewLine, parsePatchCommentable, parseReviewFilesJson, parseReviewPayload, readRegularReviewPayload, remapReviewComments, ReviewPayloadError, reviewCommentSubject, rewriteSnappedSuggestion, snapReviewNote, stageReviewPayload, writeStagedOutput, } from './gha-review-payload/index.mjs';
22
+ export { nextPageCursorFromLinkHeader, nextPageUrlFromLinkHeader, validatePaginationRequestUrl, } from './http-link-pagination/index.mjs';
@@ -3,5 +3,7 @@ export { runPnpm } from './exec.mts';
3
3
  export { baseInstallArgs, findWorkspaceLinkMismatches, listWorkspaces, logWorkspaceLinkMismatches, parseInstallOptions, reportGlibcVersionRuntime, } from './support.mts';
4
4
  export type { CaptureCommand, CommandResult, InstallOptions, Lifecycle, Workspace, WorkspaceLinkMismatch, } from './support.mts';
5
5
  export { formatReleaseAgeFailure, isReleaseAgeViolation, parseReleaseAgeViolations, } from './release-age.mts';
6
+ export { flattenReleaseAgeSelectors, packageNameFromPnpmLockKey, pnpmLockPackageKeyMatchesSelector, validateReleaseAgeExemptionGroups, validateReleaseAgePolicy, } from './release-age-policy.mts';
7
+ export type { ReleaseAgeExemptionGroup, ReleaseAgePermanentExemption, ReleaseAgePolicyConfig, ReleaseAgePolicySnapshot, } from './release-age-policy.mts';
6
8
  export { INSTALL_TERMINATION_FAILED, installExitCode, safeProcessGroup, startInstallHeartbeat, terminateProcessGroup, terminateSafeProcessGroup, } from './process.mts';
7
9
  export { persistentDependencyTreeIsCold, persistentMetadataFingerprint, persistentMetadataMatches, writePersistentMetadataStamp, } from './metadata.mts';
@@ -2,5 +2,6 @@ export { runInstallLifecycle } from './runner.mjs';
2
2
  export { runPnpm } from './exec.mjs';
3
3
  export { baseInstallArgs, findWorkspaceLinkMismatches, listWorkspaces, logWorkspaceLinkMismatches, parseInstallOptions, reportGlibcVersionRuntime, } from './support.mjs';
4
4
  export { formatReleaseAgeFailure, isReleaseAgeViolation, parseReleaseAgeViolations, } from './release-age.mjs';
5
+ export { flattenReleaseAgeSelectors, packageNameFromPnpmLockKey, pnpmLockPackageKeyMatchesSelector, validateReleaseAgeExemptionGroups, validateReleaseAgePolicy, } from './release-age-policy.mjs';
5
6
  export { INSTALL_TERMINATION_FAILED, installExitCode, safeProcessGroup, startInstallHeartbeat, terminateProcessGroup, terminateSafeProcessGroup, } from './process.mjs';
6
7
  export { persistentDependencyTreeIsCold, persistentMetadataFingerprint, persistentMetadataMatches, writePersistentMetadataStamp, } from './metadata.mjs';
@@ -0,0 +1,34 @@
1
+ /** A group of exact package versions temporarily exempted from the age gate. */
2
+ export interface ReleaseAgeExemptionGroup {
3
+ /** Exact package@version selectors accepted by pnpm's minimumReleaseAgeExclude. */
4
+ selectors: readonly [string, ...string[]];
5
+ /** Why these releases must bypass the normal supply-chain delay. */
6
+ reason: string;
7
+ /** Canonical UTC time at which this group becomes eligible for removal. */
8
+ eligibleForRemovalAt: string;
9
+ }
10
+ /** A package permanently exempted from the age gate, usually because it is first-party. */
11
+ export interface ReleaseAgePermanentExemption {
12
+ name: string;
13
+ reason: string;
14
+ }
15
+ /** Repository-specific release-age policy. All package names and scopes are caller supplied. */
16
+ export interface ReleaseAgePolicyConfig {
17
+ /** Permanent package exemptions. */
18
+ permanentExemptions?: readonly ReleaseAgePermanentExemption[];
19
+ /** Temporary exact-version exemptions. */
20
+ temporaryExemptionGroups?: readonly ReleaseAgeExemptionGroup[];
21
+ /** Prefixes that identify packages expected to be present in the permanent registry. */
22
+ firstPartyPackagePrefixes?: readonly string[];
23
+ /** Whether every permanent exemption must occur in the active package graph. */
24
+ requirePermanentExemptionsActive?: boolean;
25
+ }
26
+ /** Parsed repository state passed to the release-age policy validator. */
27
+ export interface ReleaseAgePolicySnapshot {
28
+ /** Raw entries from pnpm-workspace.yaml minimumReleaseAgeExclude. */
29
+ workspaceExcludes: readonly unknown[];
30
+ /** Package names found in manifests and/or the lockfile. */
31
+ activePackageNames?: readonly string[];
32
+ /** Keys from the lockfile `packages` map, used to check temporary selectors. */
33
+ lockfilePackageKeys?: readonly string[];
34
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Configuration-driven validation for pnpm's `minimumReleaseAgeExclude` list.
3
+ *
4
+ * This module intentionally does not read repository files or parse YAML. A repository-specific
5
+ * adapter can parse its workspace and lockfile once, then pass the resulting values here. Keeping
6
+ * the policy engine pure makes its public contract safe to share with other repositories and keeps
7
+ * private package names, scopes, and documentation out of this package.
8
+ */
9
+ import type { ReleaseAgeExemptionGroup, ReleaseAgePolicyConfig, ReleaseAgePolicySnapshot } from './release-age-policy-types.mts';
10
+ export type { ReleaseAgeExemptionGroup, ReleaseAgePermanentExemption, ReleaseAgePolicyConfig, ReleaseAgePolicySnapshot, } from './release-age-policy-types.mts';
11
+ /** Returns a flat list of temporary selectors, preserving declaration order. */
12
+ export declare function flattenReleaseAgeSelectors(groups: ReadonlyArray<ReleaseAgeExemptionGroup>): string[];
13
+ /**
14
+ * Validates temporary exemption shape without applying repository-specific policy.
15
+ *
16
+ * The returned messages are deliberately path-free so a caller can attach its own CI annotation
17
+ * location. The function reports every independent error rather than throwing on malformed input.
18
+ */
19
+ export declare function validateReleaseAgeExemptionGroups(groups: ReadonlyArray<ReleaseAgeExemptionGroup>): string[];
20
+ /** Extracts an npm package name from a pnpm lockfile package key. */
21
+ export declare function packageNameFromPnpmLockKey(key: string): string | null;
22
+ /** Tests whether a lockfile key represents an exact package@version selector. */
23
+ export declare function pnpmLockPackageKeyMatchesSelector(key: string, selector: string): boolean;
24
+ /**
25
+ * Validates registry/workspace/graph consistency for a configured release-age policy.
26
+ *
27
+ * `activePackageNames` should be the union of dependency names discovered in tracked manifests and
28
+ * the lockfile. If `lockfilePackageKeys` is supplied, every temporary selector must match a key;
29
+ * omitting it lets adapters that do not track lockfiles use the shape and registry checks alone.
30
+ */
31
+ export declare function validateReleaseAgePolicy(config: ReleaseAgePolicyConfig, snapshot: ReleaseAgePolicySnapshot): string[];
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Configuration-driven validation for pnpm's `minimumReleaseAgeExclude` list.
3
+ *
4
+ * This module intentionally does not read repository files or parse YAML. A repository-specific
5
+ * adapter can parse its workspace and lockfile once, then pass the resulting values here. Keeping
6
+ * the policy engine pure makes its public contract safe to share with other repositories and keeps
7
+ * private package names, scopes, and documentation out of this package.
8
+ */
9
+ const EXACT_PACKAGE_VERSION_SELECTOR = /^(?:@[a-z0-9][a-z0-9._~-]*\/[a-z0-9][a-z0-9._~-]*|[a-z0-9][a-z0-9._~-]*)@(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
10
+ const CANONICAL_UTC_INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/;
11
+ function isCanonicalUtcInstant(value) {
12
+ if (!CANONICAL_UTC_INSTANT.test(value))
13
+ return false;
14
+ const timestamp = Date.parse(value);
15
+ return (Number.isFinite(timestamp) && new Date(timestamp).toISOString() === value.replace('Z', '.000Z'));
16
+ }
17
+ /** Returns a flat list of temporary selectors, preserving declaration order. */
18
+ export function flattenReleaseAgeSelectors(groups) {
19
+ return groups.flatMap((group) => group.selectors);
20
+ }
21
+ /**
22
+ * Validates temporary exemption shape without applying repository-specific policy.
23
+ *
24
+ * The returned messages are deliberately path-free so a caller can attach its own CI annotation
25
+ * location. The function reports every independent error rather than throwing on malformed input.
26
+ */
27
+ export function validateReleaseAgeExemptionGroups(groups) {
28
+ const errors = [];
29
+ const selectors = new Set();
30
+ for (const group of groups) {
31
+ if (group.selectors.length === 0) {
32
+ errors.push('release-age exemption group must contain at least one exact selector');
33
+ }
34
+ for (const selector of group.selectors) {
35
+ if (!EXACT_PACKAGE_VERSION_SELECTOR.test(selector)) {
36
+ errors.push(`release-age exemption "${selector}" must be an exact package@version selector`);
37
+ }
38
+ if (selectors.has(selector))
39
+ errors.push(`duplicate release-age exemption "${selector}"`);
40
+ selectors.add(selector);
41
+ }
42
+ if (group.reason.trim().length === 0) {
43
+ errors.push('release-age exemption group must have a nonblank reason');
44
+ }
45
+ if (!isCanonicalUtcInstant(group.eligibleForRemovalAt)) {
46
+ errors.push('release-age exemption group eligibleForRemovalAt must be a canonical UTC instant (YYYY-MM-DDTHH:mm:ssZ)');
47
+ }
48
+ }
49
+ return errors;
50
+ }
51
+ /** Extracts an npm package name from a pnpm lockfile package key. */
52
+ export function packageNameFromPnpmLockKey(key) {
53
+ const normalized = key.startsWith('/') ? key.slice(1) : key;
54
+ if (normalized.startsWith('@')) {
55
+ const packageSeparator = normalized.indexOf('/', 1);
56
+ if (packageSeparator === -1)
57
+ return null;
58
+ const versionSeparator = normalized.indexOf('@', packageSeparator + 1);
59
+ return versionSeparator === -1 ? null : normalized.slice(0, versionSeparator);
60
+ }
61
+ const versionSeparator = normalized.indexOf('@');
62
+ return versionSeparator === -1 ? null : normalized.slice(0, versionSeparator);
63
+ }
64
+ /** Tests whether a lockfile key represents an exact package@version selector. */
65
+ export function pnpmLockPackageKeyMatchesSelector(key, selector) {
66
+ const normalized = key.startsWith('/') ? key.slice(1) : key;
67
+ return (normalized === selector ||
68
+ normalized.startsWith(`${selector}(`) ||
69
+ normalized.startsWith(`${selector}_`));
70
+ }
71
+ /**
72
+ * Validates registry/workspace/graph consistency for a configured release-age policy.
73
+ *
74
+ * `activePackageNames` should be the union of dependency names discovered in tracked manifests and
75
+ * the lockfile. If `lockfilePackageKeys` is supplied, every temporary selector must match a key;
76
+ * omitting it lets adapters that do not track lockfiles use the shape and registry checks alone.
77
+ */
78
+ export function validateReleaseAgePolicy(config, snapshot) {
79
+ const permanentExemptions = config.permanentExemptions ?? [];
80
+ const temporaryGroups = config.temporaryExemptionGroups ?? [];
81
+ const prefixes = config.firstPartyPackagePrefixes ?? [];
82
+ const activeNames = new Set(snapshot.activePackageNames ?? []);
83
+ const errors = validateReleaseAgeExemptionGroups(temporaryGroups);
84
+ const permanentNames = new Set();
85
+ for (const exemption of permanentExemptions) {
86
+ if (exemption.name.trim().length === 0)
87
+ errors.push('permanent release-age exemption name must be nonblank');
88
+ if (permanentNames.has(exemption.name)) {
89
+ errors.push(`duplicate permanent release-age exemption "${exemption.name}"`);
90
+ }
91
+ permanentNames.add(exemption.name);
92
+ if (exemption.reason.trim().length === 0) {
93
+ errors.push(`permanent release-age exemption "${exemption.name}" must have a nonblank reason`);
94
+ }
95
+ }
96
+ const temporarySelectors = new Set(flattenReleaseAgeSelectors(temporaryGroups));
97
+ const registryNames = new Set([...permanentNames, ...temporarySelectors]);
98
+ const workspaceNames = new Set();
99
+ for (const entry of snapshot.workspaceExcludes) {
100
+ if (typeof entry !== 'string') {
101
+ errors.push(`minimumReleaseAgeExclude entry ${JSON.stringify(entry)} must be a string`);
102
+ continue;
103
+ }
104
+ if (workspaceNames.has(entry)) {
105
+ errors.push(`minimumReleaseAgeExclude duplicates "${entry}"`);
106
+ }
107
+ workspaceNames.add(entry);
108
+ if (!registryNames.has(entry)) {
109
+ errors.push(`minimumReleaseAgeExclude contains unregistered entry "${entry}"`);
110
+ }
111
+ }
112
+ for (const name of registryNames) {
113
+ if (!workspaceNames.has(name))
114
+ errors.push(`registered release-age exemption "${name}" is missing from minimumReleaseAgeExclude`);
115
+ }
116
+ for (const name of activeNames) {
117
+ if (prefixes.some((prefix) => name.startsWith(prefix)) && !permanentNames.has(name)) {
118
+ errors.push(`active first-party package "${name}" is missing from permanent release-age exemptions`);
119
+ }
120
+ }
121
+ if (config.requirePermanentExemptionsActive ?? true) {
122
+ for (const name of permanentNames) {
123
+ if (!activeNames.has(name)) {
124
+ errors.push(`permanent release-age exemption "${name}" is absent from the active package graph`);
125
+ }
126
+ }
127
+ }
128
+ if (snapshot.lockfilePackageKeys !== undefined) {
129
+ for (const selector of temporarySelectors) {
130
+ if (!snapshot.lockfilePackageKeys.some((key) => pnpmLockPackageKeyMatchesSelector(key, selector))) {
131
+ errors.push(`temporary release-age exemption "${selector}" is absent from lockfile packages`);
132
+ }
133
+ }
134
+ }
135
+ return errors;
136
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Normalizes a provider's attempt count when earlier attempts produced no targets.
3
+ * Provider API fetching and parsing intentionally remain outside this package.
4
+ */
5
+ export declare function deriveRetryAttempt(runAttempt: number, priorTargetCounts: readonly number[]): number;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Normalizes a provider's attempt count when earlier attempts produced no targets.
3
+ * Provider API fetching and parsing intentionally remain outside this package.
4
+ */
5
+ export function deriveRetryAttempt(runAttempt, priorTargetCounts) {
6
+ const emptyPriorAttempts = priorTargetCounts.filter((count) => count === 0).length;
7
+ return Math.max(1, runAttempt - emptyPriorAttempts);
8
+ }
@@ -0,0 +1,18 @@
1
+ import type { RetryContext, RetryDecision, RetryRule } from './types.mts';
2
+ export type NoMatchReason = 'no-rule' | 'invalid-rule' | 'invalid-target' | 'missing-target';
3
+ export type DecisionResult = {
4
+ decision: Exclude<RetryDecision, 'rerun'>;
5
+ matchedRule: string;
6
+ } | {
7
+ decision: 'rerun';
8
+ matchedRule: string;
9
+ targetName: string;
10
+ } | {
11
+ decision: 'no-match';
12
+ matchedRule: string;
13
+ reason: NoMatchReason;
14
+ };
15
+ export interface EvaluateRulesOptions {
16
+ afterRuleEvaluated?: (context: RetryContext, rule: RetryRule) => Promise<void> | void;
17
+ }
18
+ export declare function decide(context: RetryContext, rules: readonly RetryRule[], options?: EvaluateRulesOptions): Promise<DecisionResult>;
@@ -0,0 +1,56 @@
1
+ function normalizedAttempt(context, rule) {
2
+ const sharedAttempt = context.retryAttempt ?? context.runAttempt;
3
+ return rule.decision === undefined || rule.decision === 'rerun'
4
+ ? (context.retryAttempts?.get(rule.id) ?? sharedAttempt)
5
+ : sharedAttempt;
6
+ }
7
+ function resolveTargetName(target, context) {
8
+ if (target == null || typeof target !== 'object')
9
+ return { reason: 'invalid-target' };
10
+ let targetName;
11
+ try {
12
+ targetName = 'targetName' in target ? target.targetName : target.resolveTargetName(context);
13
+ }
14
+ catch {
15
+ return { reason: 'invalid-target' };
16
+ }
17
+ if (typeof targetName !== 'string') {
18
+ return { reason: targetName == null ? 'missing-target' : 'invalid-target' };
19
+ }
20
+ if ('targetFamily' in target) {
21
+ if (typeof target.targetFamily !== 'string' || target.targetFamily.length === 0) {
22
+ return { reason: 'invalid-target' };
23
+ }
24
+ if (!targetName.startsWith(target.targetFamily)) {
25
+ return { reason: 'invalid-target' };
26
+ }
27
+ }
28
+ if (targetName.length === 0)
29
+ return { reason: 'missing-target' };
30
+ if (!context.targetNames?.has(targetName))
31
+ return { reason: 'missing-target' };
32
+ return { targetName };
33
+ }
34
+ export async function decide(context, rules, options = {}) {
35
+ for (const rule of rules) {
36
+ const attempt = normalizedAttempt(context, rule);
37
+ if (!Number.isSafeInteger(attempt) || attempt < 1 || !Number.isSafeInteger(rule.maxAttempts)) {
38
+ return { decision: 'no-match', matchedRule: rule.id, reason: 'invalid-rule' };
39
+ }
40
+ if (rule.maxAttempts < attempt)
41
+ continue;
42
+ const matched = await rule.match(context);
43
+ await options.afterRuleEvaluated?.(context, rule);
44
+ if (!matched)
45
+ continue;
46
+ if (rule.decision !== undefined && rule.decision !== 'rerun') {
47
+ return { decision: rule.decision, matchedRule: rule.id };
48
+ }
49
+ const resolved = resolveTargetName(rule.retryTarget, context);
50
+ if (resolved.targetName === undefined) {
51
+ return { decision: 'no-match', matchedRule: rule.id, reason: resolved.reason };
52
+ }
53
+ return { decision: 'rerun', matchedRule: rule.id, targetName: resolved.targetName };
54
+ }
55
+ return { decision: 'no-match', matchedRule: '', reason: 'no-rule' };
56
+ }
@@ -0,0 +1,4 @@
1
+ export { deriveRetryAttempt } from './attempts.mts';
2
+ export { decide } from './decision-evaluator.mts';
3
+ export type { DecisionResult, EvaluateRulesOptions, NoMatchReason } from './decision-evaluator.mts';
4
+ export type { RetryContext, RetryDecision, RetryRule, RetryTarget } from './types.mts';
@@ -0,0 +1,2 @@
1
+ export { deriveRetryAttempt } from './attempts.mjs';
2
+ export { decide } from './decision-evaluator.mjs';