vouchington-tooling 0.13.1 → 0.13.3
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 +26 -0
- package/dist/ast-grep-examples/companion-parity.d.mts +19 -0
- package/dist/ast-grep-examples/companion-parity.mjs +156 -0
- package/dist/ast-grep-examples/index.d.mts +2 -1
- package/dist/ast-grep-examples/index.mjs +1 -0
- package/dist/gha-check-run/cli.d.mts +2 -0
- package/dist/gha-check-run/cli.mjs +81 -0
- package/dist/gha-check-run/github.d.mts +30 -0
- package/dist/gha-check-run/github.mjs +63 -0
- package/dist/gha-check-run/index.d.mts +2 -0
- package/dist/gha-check-run/index.mjs +2 -0
- package/dist/gha-post-review/cli.mjs +1 -1
- package/dist/gha-post-review/github.d.mts +6 -22
- package/dist/gha-post-review/github.mjs +8 -92
- package/dist/gha-post-review/post.d.mts +7 -1
- package/dist/gha-post-review/post.mjs +11 -3
- package/dist/gha-post-review/pull-io.d.mts +13 -0
- package/dist/gha-post-review/pull-io.mjs +88 -0
- package/dist/index.d.mts +4 -3
- package/dist/index.mjs +2 -2
- package/dist/markdown/ast.d.mts +8 -0
- package/dist/markdown/ast.mjs +29 -0
- package/dist/markdown/index.d.mts +6 -0
- package/dist/markdown/index.mjs +5 -0
- package/dist/markdown/loose-pipe-rows.d.mts +2 -0
- package/dist/markdown/loose-pipe-rows.mjs +31 -0
- package/dist/markdown/sections.d.mts +2 -0
- package/dist/markdown/sections.mjs +25 -0
- package/dist/markdown/tables.d.mts +3 -0
- package/dist/markdown/tables.mjs +43 -0
- package/dist/markdown/text.d.mts +2 -0
- package/dist/markdown/text.mjs +22 -0
- package/dist/markdown/types.d.mts +35 -0
- package/dist/markdown/types.mjs +1 -0
- package/dist/scc-complexity/baseline.d.mts +6 -0
- package/dist/scc-complexity/baseline.mjs +90 -0
- package/dist/scc-complexity/constants.d.mts +1 -0
- package/dist/scc-complexity/constants.mjs +1 -0
- package/dist/scc-complexity/index.d.mts +6 -14
- package/dist/scc-complexity/index.mjs +64 -39
- package/dist/scc-complexity/parser.d.mts +2 -0
- package/dist/scc-complexity/parser.mjs +21 -0
- package/dist/scc-complexity/paths.d.mts +4 -0
- package/dist/scc-complexity/paths.mjs +44 -0
- package/dist/scc-complexity/scope-config.d.mts +3 -0
- package/dist/scc-complexity/scope-config.mjs +35 -0
- package/dist/scc-complexity/types.d.mts +32 -0
- package/dist/scc-complexity/types.mjs +1 -0
- package/dist/scc-complexity/workflow-command.d.mts +2 -0
- package/dist/scc-complexity/workflow-command.mjs +6 -0
- package/package.json +15 -1
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { GhExec } from './github.mts';
|
|
2
|
+
import type { PostResult, PostReviewIo, SanitizedReview } from './post.mts';
|
|
3
|
+
export declare function postWithGh(repository: string, prNumber: string, payload: SanitizedReview, token: string, exec: GhExec): PostResult;
|
|
4
|
+
export declare function createGhPostReviewIo(options: {
|
|
5
|
+
repository: string;
|
|
6
|
+
prNumber: string;
|
|
7
|
+
payloadPath: string;
|
|
8
|
+
payloadBytes: Buffer;
|
|
9
|
+
token: string;
|
|
10
|
+
exec: GhExec;
|
|
11
|
+
expectedHeadSha?: string;
|
|
12
|
+
expectedBaseSha?: string;
|
|
13
|
+
}): PostReviewIo;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { rmSync } from 'node:fs';
|
|
2
|
+
import { parseReviewFilesJson, ReviewPayloadError } from '../gha-review-payload/index.mjs';
|
|
3
|
+
export function postWithGh(repository, prNumber, payload, token, exec) {
|
|
4
|
+
try {
|
|
5
|
+
exec(['api', '--method', 'POST', `repos/${repository}/pulls/${prNumber}/reviews`, '--input', '-'], {
|
|
6
|
+
input: JSON.stringify(payload),
|
|
7
|
+
env: { ...process.env, GH_TOKEN: token, GITHUB_TOKEN: token },
|
|
8
|
+
});
|
|
9
|
+
return { ok: true, status: 201, body: '' };
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
// oxlint-disable-next-line no-mistakes/ts-no-const-aliases -- isolate the process-error shape assertion at the catch boundary
|
|
13
|
+
const err = error;
|
|
14
|
+
const text = `${err.stdout ?? ''}${err.stderr ?? ''}${err.message ?? ''}`;
|
|
15
|
+
const status = Number(/HTTP\s+(\d{3})/u.exec(text)?.[1] ?? 0);
|
|
16
|
+
return { ok: false, status, body: text };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function readPullState(repository, prNumber, exec) {
|
|
20
|
+
let lastError;
|
|
21
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
22
|
+
try {
|
|
23
|
+
return exec([
|
|
24
|
+
'api',
|
|
25
|
+
`repos/${repository}/pulls/${prNumber}`,
|
|
26
|
+
'--jq',
|
|
27
|
+
'[.head.sha, .base.sha, .draft, .state] | @tsv',
|
|
28
|
+
]).split('\t');
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
lastError = error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
throw lastError;
|
|
35
|
+
}
|
|
36
|
+
export function createGhPostReviewIo(options) {
|
|
37
|
+
const { repository, prNumber, payloadBytes, token, exec, expectedHeadSha, expectedBaseSha } = options;
|
|
38
|
+
return {
|
|
39
|
+
readFile() {
|
|
40
|
+
return payloadBytes;
|
|
41
|
+
},
|
|
42
|
+
removeFile(path) {
|
|
43
|
+
rmSync(path, { force: true });
|
|
44
|
+
},
|
|
45
|
+
getHeadSha() {
|
|
46
|
+
if (!/^[1-9][0-9]*$/u.test(prNumber)) {
|
|
47
|
+
throw new ReviewPayloadError('PR_NUMBER must be a positive integer.');
|
|
48
|
+
}
|
|
49
|
+
if (Boolean(expectedHeadSha) !== Boolean(expectedBaseSha)) {
|
|
50
|
+
throw new ReviewPayloadError('EXPECTED_HEAD_SHA and EXPECTED_BASE_SHA must be provided together.');
|
|
51
|
+
}
|
|
52
|
+
if (expectedHeadSha && !/^[0-9a-f]{40}$/u.test(expectedHeadSha)) {
|
|
53
|
+
throw new ReviewPayloadError('EXPECTED_HEAD_SHA must be a full lowercase commit SHA.');
|
|
54
|
+
}
|
|
55
|
+
if (expectedBaseSha && !/^[0-9a-f]{40}$/u.test(expectedBaseSha)) {
|
|
56
|
+
throw new ReviewPayloadError('EXPECTED_BASE_SHA must be a full lowercase commit SHA.');
|
|
57
|
+
}
|
|
58
|
+
const state = readPullState(repository, prNumber, exec);
|
|
59
|
+
const [headSha = '', baseSha = '', draft = '', pullState = ''] = state;
|
|
60
|
+
if (!/^[0-9a-f]{40}$/u.test(headSha)) {
|
|
61
|
+
throw new ReviewPayloadError(`Could not resolve PR head SHA (got "${headSha}").`);
|
|
62
|
+
}
|
|
63
|
+
if (!/^[0-9a-f]{40}$/u.test(baseSha)) {
|
|
64
|
+
throw new ReviewPayloadError(`Could not resolve PR base SHA (got "${baseSha}").`);
|
|
65
|
+
}
|
|
66
|
+
if (draft !== 'false') {
|
|
67
|
+
throw new ReviewPayloadError('Pull request became a draft before posting the review.');
|
|
68
|
+
}
|
|
69
|
+
if (pullState !== 'open') {
|
|
70
|
+
throw new ReviewPayloadError('Pull request closed before posting the review.');
|
|
71
|
+
}
|
|
72
|
+
if (expectedHeadSha && headSha !== expectedHeadSha) {
|
|
73
|
+
throw new ReviewPayloadError('PR head changed before posting the selected review.');
|
|
74
|
+
}
|
|
75
|
+
if (expectedBaseSha && baseSha !== expectedBaseSha) {
|
|
76
|
+
throw new ReviewPayloadError('PR base changed before posting the selected review.');
|
|
77
|
+
}
|
|
78
|
+
// Preserve the orchestrator-selected revision as the review commit_id after equality checks.
|
|
79
|
+
return expectedHeadSha || headSha;
|
|
80
|
+
},
|
|
81
|
+
listPullFiles() {
|
|
82
|
+
return parseReviewFilesJson(exec(['api', '--paginate', `repos/${repository}/pulls/${prNumber}/files?per_page=100`]));
|
|
83
|
+
},
|
|
84
|
+
postReview(payload) {
|
|
85
|
+
return postWithGh(repository, prNumber, payload, token, exec);
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
package/dist/index.d.mts
CHANGED
|
@@ -25,7 +25,8 @@ export { requireUpToDate } from './require-up-to-date/index.mts';
|
|
|
25
25
|
export type { GitExecutor, RequireUpToDateOptions } from './require-up-to-date/index.mts';
|
|
26
26
|
export { gitleaksDirectoryScanArguments, runGitleaksDirectoryScan, } from './gitleaks-directory-scan/index.mts';
|
|
27
27
|
export type { DirectoryScanExecutor, GitleaksDirectoryScanOptions, } from './gitleaks-directory-scan/index.mts';
|
|
28
|
-
export { astGrepExamplesArguments, runAstGrepExamples } from './ast-grep-examples/index.mts';
|
|
28
|
+
export { astGrepExamplesArguments, compareAstGrepCompanions, runAstGrepExamples, } from './ast-grep-examples/index.mts';
|
|
29
|
+
export type { AstGrepCompanionContext, AstGrepCompanionDifference, AstGrepCompanionParityOptions, } from './ast-grep-examples/index.mts';
|
|
29
30
|
export type { AstGrepExamplesExecutor, AstGrepExamplesOptions } from './ast-grep-examples/index.mts';
|
|
30
31
|
export { astGrepPackPaths, astGrepPackPathsFrom } from './ast-grep-pack/index.mts';
|
|
31
32
|
export type { AstGrepPackPaths } from './ast-grep-pack/index.mts';
|
|
@@ -72,8 +73,8 @@ export { EPOCH_PRUNED_AT, normalizeDeployedLayer, pruneDeployedRuntimeDeps, rest
|
|
|
72
73
|
export type { NormalizeDeployedLayerResult, PruneResult, RestoreWorkspacePackagesOptions, } from './pnpm-deploy/index.mts';
|
|
73
74
|
export { parseDockerfilePrewarmStages, parseDockerfileRuntimeImages, } from './dockerfile-parse/index.mts';
|
|
74
75
|
export type { DockerfilePrewarmStage, DockerfileRuntimeImage, ParseDockerfilePrewarmOptions, ParseDockerfileRuntimeImagesOptions, } from './dockerfile-parse/index.mts';
|
|
75
|
-
export { buildSccArgs, checkSccComplexity, parseSccComplexityViolations, SCC_COMPLEXITY_LIMIT, } from './scc-complexity/index.mts';
|
|
76
|
-
export type { SccComplexityOptions, SccComplexityViolation } from './scc-complexity/index.mts';
|
|
76
|
+
export { buildSccArgs, checkSccComplexity, parseSccComplexityBaseline, parseSccComplexityViolations, SCC_COMPLEXITY_BASELINE_VERSION, SCC_COMPLEXITY_LIMIT, } from './scc-complexity/index.mts';
|
|
77
|
+
export type { SccComplexityBaseline, SccComplexityBaselineEntry, SccComplexityOptions, SccComplexityScope, SccComplexityViolation, } from './scc-complexity/index.mts';
|
|
77
78
|
export { assertWorkflowCommandDrift, parseCiLocalArgs, runCiLocal } from './ci-local/index.mts';
|
|
78
79
|
export type { CiLocalCommand, CiLocalSpawn, CiLocalSpawnOptions, CiLocalSpawnResult, CiLocalTarget, RunCiLocalOptions, } from './ci-local/index.mts';
|
|
79
80
|
export { GitHubRateLimitError, isRateLimited, isRetryableCancellationError, MAX_RATE_LIMIT_WAIT_MS, rateLimitDelay, reserveRateLimitDelay, } from './gha-rate-limit/index.mts';
|
package/dist/index.mjs
CHANGED
|
@@ -13,7 +13,7 @@ export { auditCiJobRuntime, parseWorkflowNameMatch } from './gha-runtime-audit/i
|
|
|
13
13
|
export { checkGhaWorkspacePolicy } from './gha-workspace-policy/index.mjs';
|
|
14
14
|
export { requireUpToDate } from './require-up-to-date/index.mjs';
|
|
15
15
|
export { gitleaksDirectoryScanArguments, runGitleaksDirectoryScan, } from './gitleaks-directory-scan/index.mjs';
|
|
16
|
-
export { astGrepExamplesArguments, runAstGrepExamples } from './ast-grep-examples/index.mjs';
|
|
16
|
+
export { astGrepExamplesArguments, compareAstGrepCompanions, runAstGrepExamples, } from './ast-grep-examples/index.mjs';
|
|
17
17
|
export { astGrepPackPaths, astGrepPackPathsFrom } from './ast-grep-pack/index.mjs';
|
|
18
18
|
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';
|
|
19
19
|
export { prepareVitestReports } from './vitest-blob-manifest/reports.mjs';
|
|
@@ -39,7 +39,7 @@ export { nextPageCursorFromLinkHeader, nextPageUrlFromLinkHeader, validatePagina
|
|
|
39
39
|
export { cmdDownloadCoverage, cmdDownloadVitestBlobs, cmdUpload, mintPresignedControl, transportObjectKeys, } from './coverage-transport/index.mjs';
|
|
40
40
|
export { EPOCH_PRUNED_AT, normalizeDeployedLayer, pruneDeployedRuntimeDeps, restoreDeployedWorkspacePackages, } from './pnpm-deploy/index.mjs';
|
|
41
41
|
export { parseDockerfilePrewarmStages, parseDockerfileRuntimeImages, } from './dockerfile-parse/index.mjs';
|
|
42
|
-
export { buildSccArgs, checkSccComplexity, parseSccComplexityViolations, SCC_COMPLEXITY_LIMIT, } from './scc-complexity/index.mjs';
|
|
42
|
+
export { buildSccArgs, checkSccComplexity, parseSccComplexityBaseline, parseSccComplexityViolations, SCC_COMPLEXITY_BASELINE_VERSION, SCC_COMPLEXITY_LIMIT, } from './scc-complexity/index.mjs';
|
|
43
43
|
export { assertWorkflowCommandDrift, parseCiLocalArgs, runCiLocal } from './ci-local/index.mjs';
|
|
44
44
|
export { GitHubRateLimitError, isRateLimited, isRetryableCancellationError, MAX_RATE_LIMIT_WAIT_MS, rateLimitDelay, reserveRateLimitDelay, } from './gha-rate-limit/index.mjs';
|
|
45
45
|
export { CHECKPOINT_MARKER, isTrustedCheckpointComment, parseCheckpoint, renderCheckpoint, sortedCheckpointCandidates, validateCheckpoint, } from './gha-pr-checkpoint/index.mjs';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Root } from 'mdast';
|
|
2
|
+
import type { MarkdownNode } from './types.mts';
|
|
3
|
+
export declare function parseGfmMarkdown(markdown: string): Root;
|
|
4
|
+
export declare function walkMarkdown(node: MarkdownNode, visitor: (node: MarkdownNode) => void): void;
|
|
5
|
+
export declare function findMarkdownNode<T extends MarkdownNode>(node: MarkdownNode, predicate: (node: MarkdownNode) => node is T): T | null;
|
|
6
|
+
export declare function findMarkdownNode(node: MarkdownNode, predicate: (node: MarkdownNode) => boolean): MarkdownNode | null;
|
|
7
|
+
export declare function findMarkdownNodes<T extends MarkdownNode>(node: MarkdownNode, predicate: (node: MarkdownNode) => node is T): T[];
|
|
8
|
+
export declare function findMarkdownNodes(node: MarkdownNode, predicate: (node: MarkdownNode) => boolean): MarkdownNode[];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { remark } from 'remark';
|
|
2
|
+
import remarkGfm from 'remark-gfm';
|
|
3
|
+
const processor = remark().use(remarkGfm);
|
|
4
|
+
export function parseGfmMarkdown(markdown) {
|
|
5
|
+
return processor.parse(markdown);
|
|
6
|
+
}
|
|
7
|
+
export function walkMarkdown(node, visitor) {
|
|
8
|
+
visitor(node);
|
|
9
|
+
if (!('children' in node))
|
|
10
|
+
return;
|
|
11
|
+
for (const child of node.children)
|
|
12
|
+
walkMarkdown(child, visitor);
|
|
13
|
+
}
|
|
14
|
+
export function findMarkdownNode(node, predicate) {
|
|
15
|
+
let match = null;
|
|
16
|
+
walkMarkdown(node, (candidate) => {
|
|
17
|
+
if (!match && predicate(candidate))
|
|
18
|
+
match = candidate;
|
|
19
|
+
});
|
|
20
|
+
return match;
|
|
21
|
+
}
|
|
22
|
+
export function findMarkdownNodes(node, predicate) {
|
|
23
|
+
const matches = [];
|
|
24
|
+
walkMarkdown(node, (candidate) => {
|
|
25
|
+
if (predicate(candidate))
|
|
26
|
+
matches.push(candidate);
|
|
27
|
+
});
|
|
28
|
+
return matches;
|
|
29
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { findMarkdownNode, findMarkdownNodes, parseGfmMarkdown, walkMarkdown } from './ast.mts';
|
|
2
|
+
export { extractLooseMarkdownTableRows } from './loose-pipe-rows.mts';
|
|
3
|
+
export { markdownSectionBetweenHeadings } from './sections.mts';
|
|
4
|
+
export { extractMarkdownTables, parseMarkdownTables } from './tables.mts';
|
|
5
|
+
export { markdownNodeText } from './text.mts';
|
|
6
|
+
export type { LooseMarkdownTableRow, LooseMarkdownTableRowsOptions, MarkdownNode, MarkdownSection, MarkdownSectionOptions, MarkdownTableRow, MarkdownTextOptions, ParseMarkdownTablesOptions, PositionedMarkdownTable, } from './types.mts';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { findMarkdownNode, findMarkdownNodes, parseGfmMarkdown, walkMarkdown } from './ast.mjs';
|
|
2
|
+
export { extractLooseMarkdownTableRows } from './loose-pipe-rows.mjs';
|
|
3
|
+
export { markdownSectionBetweenHeadings } from './sections.mjs';
|
|
4
|
+
export { extractMarkdownTables, parseMarkdownTables } from './tables.mjs';
|
|
5
|
+
export { markdownNodeText } from './text.mjs';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function extractLooseMarkdownTableRows(markdown, options = {}) {
|
|
2
|
+
const rows = [];
|
|
3
|
+
let offset = 0;
|
|
4
|
+
for (const [index, line] of markdown.split('\n').entries()) {
|
|
5
|
+
if (!isExcluded(offset, offset + line.length, options.excludePositions)) {
|
|
6
|
+
const cells = parseLoosePipeRow(line);
|
|
7
|
+
if (cells.length > 0)
|
|
8
|
+
rows.push({ cells, line: index + 1, offset });
|
|
9
|
+
}
|
|
10
|
+
offset += line.length + 1;
|
|
11
|
+
}
|
|
12
|
+
return rows;
|
|
13
|
+
}
|
|
14
|
+
function isExcluded(lineStart, lineEnd, positions) {
|
|
15
|
+
return (positions?.some((position) => {
|
|
16
|
+
const start = position.start.offset;
|
|
17
|
+
const end = position.end.offset;
|
|
18
|
+
return start !== undefined && end !== undefined && lineStart < end && lineEnd > start;
|
|
19
|
+
}) ?? false);
|
|
20
|
+
}
|
|
21
|
+
function parseLoosePipeRow(line) {
|
|
22
|
+
const trimmed = line.trim();
|
|
23
|
+
if (!trimmed.startsWith('|'))
|
|
24
|
+
return [];
|
|
25
|
+
const row = trimmed.endsWith('|') ? trimmed.slice(1, -1) : trimmed.slice(1);
|
|
26
|
+
const cells = row
|
|
27
|
+
.split('|')
|
|
28
|
+
.map((cell) => cell.trim())
|
|
29
|
+
.filter(Boolean);
|
|
30
|
+
return cells.every((cell) => /^:?-{3,}:?$/u.test(cell)) ? [] : cells;
|
|
31
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { parseGfmMarkdown } from './ast.mjs';
|
|
2
|
+
import { markdownNodeText } from './text.mjs';
|
|
3
|
+
export function markdownSectionBetweenHeadings(markdown, start, end, options = {}) {
|
|
4
|
+
const children = parseGfmMarkdown(markdown).children;
|
|
5
|
+
const startIndex = children.findIndex((node) => isNamedHeading(node, start));
|
|
6
|
+
if (startIndex < 0)
|
|
7
|
+
return null;
|
|
8
|
+
const startHeading = children[startIndex];
|
|
9
|
+
const endIndex = children.findIndex((node, index) => index > startIndex &&
|
|
10
|
+
isNamedHeading(node, end) &&
|
|
11
|
+
(options.endBoundary === 'any' || node.depth <= startHeading.depth));
|
|
12
|
+
const endHeading = endIndex < 0 ? null : children[endIndex];
|
|
13
|
+
const startOffset = startHeading.position.end.offset;
|
|
14
|
+
const endOffset = endHeading ? endHeading.position.start.offset : markdown.length;
|
|
15
|
+
return {
|
|
16
|
+
content: markdown.slice(startOffset, endOffset),
|
|
17
|
+
endHeading,
|
|
18
|
+
endOffset,
|
|
19
|
+
startHeading,
|
|
20
|
+
startOffset,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function isNamedHeading(node, expected) {
|
|
24
|
+
return node.type === 'heading' && markdownNodeText(node, { whitespace: 'collapse' }) === expected;
|
|
25
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { MarkdownNode, MarkdownTableRow, ParseMarkdownTablesOptions, PositionedMarkdownTable } from './types.mts';
|
|
2
|
+
export declare function extractMarkdownTables(root: MarkdownNode): PositionedMarkdownTable[];
|
|
3
|
+
export declare function parseMarkdownTables(markdown: string, options?: ParseMarkdownTablesOptions): MarkdownTableRow[][];
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { findMarkdownNodes, parseGfmMarkdown } from './ast.mjs';
|
|
2
|
+
import { markdownNodeText } from './text.mjs';
|
|
3
|
+
export function extractMarkdownTables(root) {
|
|
4
|
+
return findMarkdownNodes(root, (node) => node.type === 'table').map((table) => {
|
|
5
|
+
const result = {
|
|
6
|
+
rows: normalizeTableRows(table),
|
|
7
|
+
};
|
|
8
|
+
if (table.position)
|
|
9
|
+
result.position = table.position;
|
|
10
|
+
return result;
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
export function parseMarkdownTables(markdown, options = {}) {
|
|
14
|
+
const root = parseGfmMarkdown(normalizeTableDelimiterRows(markdown));
|
|
15
|
+
return findMarkdownNodes(root, (node) => node.type === 'table').map((table) => normalizeTableRows(table, {
|
|
16
|
+
inlineCode: options.preserveInlineCodeMarkers ? 'markers' : 'content',
|
|
17
|
+
issueReferences: 'brackets',
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
20
|
+
function normalizeTableRows(table, textOptions = {}) {
|
|
21
|
+
return table.children.map((row) => ({
|
|
22
|
+
cells: row.children.map((cell) => markdownNodeText(cell, textOptions).trim()),
|
|
23
|
+
line: row.position?.start.line ?? 1,
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
function normalizeTableDelimiterRows(markdown) {
|
|
27
|
+
return markdown
|
|
28
|
+
.split('\n')
|
|
29
|
+
.map((line) => {
|
|
30
|
+
const trimmed = line.trim();
|
|
31
|
+
if (!trimmed.startsWith('|'))
|
|
32
|
+
return line;
|
|
33
|
+
const cells = trimmed
|
|
34
|
+
.replace(/^\|/u, '')
|
|
35
|
+
.replace(/\|$/u, '')
|
|
36
|
+
.split('|')
|
|
37
|
+
.map((cell) => cell.trim());
|
|
38
|
+
if (!cells.every((cell) => /^:?-+:?$/u.test(cell)))
|
|
39
|
+
return line;
|
|
40
|
+
return `| ${cells.map((cell) => cell.replace(/-+/u, '---')).join(' | ')} |`;
|
|
41
|
+
})
|
|
42
|
+
.join('\n');
|
|
43
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function markdownNodeText(node, options = {}) {
|
|
2
|
+
const text = textFor(node, options);
|
|
3
|
+
return options.whitespace === 'collapse' ? text.replace(/\s+/gu, ' ').trim() : text;
|
|
4
|
+
}
|
|
5
|
+
function textFor(node, options) {
|
|
6
|
+
if (node.type === 'break')
|
|
7
|
+
return ' ';
|
|
8
|
+
if ('value' in node && typeof node.value === 'string') {
|
|
9
|
+
if (node.type === 'inlineCode' && options.inlineCode === 'markers')
|
|
10
|
+
return `\`${node.value}\``;
|
|
11
|
+
return node.value;
|
|
12
|
+
}
|
|
13
|
+
if (!('children' in node))
|
|
14
|
+
return '';
|
|
15
|
+
const text = node.children.map((child) => textFor(child, options)).join('');
|
|
16
|
+
if (options.issueReferences === 'brackets' &&
|
|
17
|
+
(node.type === 'link' || node.type === 'linkReference') &&
|
|
18
|
+
/^#\d+$/u.test(text.trim())) {
|
|
19
|
+
return `[${text.trim()}]`;
|
|
20
|
+
}
|
|
21
|
+
return text;
|
|
22
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Heading, Root, RootContent } from 'mdast';
|
|
2
|
+
import type { Position } from 'unist';
|
|
3
|
+
export type MarkdownNode = Root | RootContent;
|
|
4
|
+
export type MarkdownTableRow = {
|
|
5
|
+
cells: string[];
|
|
6
|
+
line: number;
|
|
7
|
+
};
|
|
8
|
+
export type PositionedMarkdownTable = {
|
|
9
|
+
position?: Position;
|
|
10
|
+
rows: MarkdownTableRow[];
|
|
11
|
+
};
|
|
12
|
+
export type MarkdownTextOptions = {
|
|
13
|
+
inlineCode?: 'content' | 'markers';
|
|
14
|
+
issueReferences?: 'text' | 'brackets';
|
|
15
|
+
whitespace?: 'preserve' | 'collapse';
|
|
16
|
+
};
|
|
17
|
+
export type ParseMarkdownTablesOptions = {
|
|
18
|
+
preserveInlineCodeMarkers?: boolean;
|
|
19
|
+
};
|
|
20
|
+
export type MarkdownSectionOptions = {
|
|
21
|
+
endBoundary?: 'same-or-higher' | 'any';
|
|
22
|
+
};
|
|
23
|
+
export type LooseMarkdownTableRowsOptions = {
|
|
24
|
+
excludePositions?: readonly Position[];
|
|
25
|
+
};
|
|
26
|
+
export type MarkdownSection = {
|
|
27
|
+
content: string;
|
|
28
|
+
endHeading: Heading | null;
|
|
29
|
+
endOffset: number;
|
|
30
|
+
startHeading: Heading;
|
|
31
|
+
startOffset: number;
|
|
32
|
+
};
|
|
33
|
+
export type LooseMarkdownTableRow = MarkdownTableRow & {
|
|
34
|
+
offset: number;
|
|
35
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { SccComplexityBaseline, SccComplexityBaselineEntry, SccComplexityScope, SccComplexityValue } from './types.mts';
|
|
2
|
+
export declare const SCC_COMPLEXITY_BASELINE_VERSION: 1;
|
|
3
|
+
export declare function parseSccComplexityBaseline(json: string): SccComplexityBaseline;
|
|
4
|
+
export declare function validateSccComplexityBaseline(baseline: SccComplexityBaseline, scopes: readonly SccComplexityScope[], trackedFileSet: ReadonlySet<string>, valuesByScope: ReadonlyMap<string, readonly SccComplexityValue[]>): void;
|
|
5
|
+
export declare function normalizeSccComplexityBaseline(baseline: SccComplexityBaseline, repoRoot: string): SccComplexityBaseline;
|
|
6
|
+
export declare function baselineEntryByScopeAndFile(baseline: SccComplexityBaseline | undefined): ReadonlyMap<string, ReadonlyMap<string, SccComplexityBaselineEntry>>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { assertWorkflowCommandData, canonicalRepoPath, isInScope } from './paths.mjs';
|
|
2
|
+
import { SCC_COMPLEXITY_LIMIT } from './constants.mjs';
|
|
3
|
+
export const SCC_COMPLEXITY_BASELINE_VERSION = 1;
|
|
4
|
+
export function parseSccComplexityBaseline(json) {
|
|
5
|
+
const parsed = JSON.parse(json);
|
|
6
|
+
if (!isRecord(parsed))
|
|
7
|
+
throw new Error('baseline must be an object');
|
|
8
|
+
if (parsed.version !== SCC_COMPLEXITY_BASELINE_VERSION)
|
|
9
|
+
throw new Error(`baseline version must be ${SCC_COMPLEXITY_BASELINE_VERSION}`);
|
|
10
|
+
if (!Array.isArray(parsed.entries))
|
|
11
|
+
throw new Error('baseline entries must be an array');
|
|
12
|
+
const entries = parsed.entries.map(parseEntry);
|
|
13
|
+
const seen = new Map();
|
|
14
|
+
for (const entry of entries) {
|
|
15
|
+
if (seen.get(entry.scope)?.has(entry.file))
|
|
16
|
+
throw new Error(`baseline entry ${entry.scope}:${entry.file} is duplicated`);
|
|
17
|
+
const files = seen.get(entry.scope) ?? new Set();
|
|
18
|
+
files.add(entry.file);
|
|
19
|
+
seen.set(entry.scope, files);
|
|
20
|
+
}
|
|
21
|
+
return { entries, version: SCC_COMPLEXITY_BASELINE_VERSION };
|
|
22
|
+
}
|
|
23
|
+
export function validateSccComplexityBaseline(baseline, scopes, trackedFileSet, valuesByScope) {
|
|
24
|
+
const scopeByName = new Map(scopes.map((scope) => [scope.name, scope]));
|
|
25
|
+
const allFiles = new Set();
|
|
26
|
+
for (const values of valuesByScope.values()) {
|
|
27
|
+
for (const value of values)
|
|
28
|
+
allFiles.add(value.file);
|
|
29
|
+
}
|
|
30
|
+
for (const entry of baseline.entries) {
|
|
31
|
+
const key = `${entry.scope}:${entry.file}`;
|
|
32
|
+
const scope = scopeByName.get(entry.scope);
|
|
33
|
+
if (!scope)
|
|
34
|
+
throw new Error(`baseline entry ${key} is out of scope`);
|
|
35
|
+
if (!trackedFileSet.has(entry.file))
|
|
36
|
+
throw new Error(`baseline entry ${key} is untracked`);
|
|
37
|
+
if (!isInScope(entry.file, scope.includePaths))
|
|
38
|
+
throw new Error(`baseline entry ${key} is out of scope`);
|
|
39
|
+
const value = valuesByScope.get(scope.name)?.find((item) => item.file === entry.file);
|
|
40
|
+
if (!value) {
|
|
41
|
+
if (allFiles.has(entry.file))
|
|
42
|
+
throw new Error(`baseline entry ${key} is out of scope`);
|
|
43
|
+
throw new Error(`baseline entry ${key} is stale`);
|
|
44
|
+
}
|
|
45
|
+
if (value.complexity <= (scope.limit ?? SCC_COMPLEXITY_LIMIT))
|
|
46
|
+
throw new Error(`baseline entry ${key} is stale`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function normalizeSccComplexityBaseline(baseline, repoRoot) {
|
|
50
|
+
const entries = baseline.entries.map((entry) => ({
|
|
51
|
+
...entry,
|
|
52
|
+
file: canonicalRepoPath(repoRoot, entry.file),
|
|
53
|
+
}));
|
|
54
|
+
const seen = new Map();
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
if (seen.get(entry.scope)?.has(entry.file))
|
|
57
|
+
throw new Error(`baseline entry ${entry.scope}:${entry.file} is duplicated`);
|
|
58
|
+
const files = seen.get(entry.scope) ?? new Set();
|
|
59
|
+
files.add(entry.file);
|
|
60
|
+
seen.set(entry.scope, files);
|
|
61
|
+
}
|
|
62
|
+
return { entries, version: baseline.version };
|
|
63
|
+
}
|
|
64
|
+
export function baselineEntryByScopeAndFile(baseline) {
|
|
65
|
+
const scopes = new Map();
|
|
66
|
+
for (const entry of baseline?.entries ?? []) {
|
|
67
|
+
const files = scopes.get(entry.scope) ?? new Map();
|
|
68
|
+
files.set(entry.file, entry);
|
|
69
|
+
scopes.set(entry.scope, files);
|
|
70
|
+
}
|
|
71
|
+
return scopes;
|
|
72
|
+
}
|
|
73
|
+
function parseEntry(value, index) {
|
|
74
|
+
if (!isRecord(value))
|
|
75
|
+
throw new Error(`baseline entry ${index} must be an object`);
|
|
76
|
+
if (typeof value.scope !== 'string' || value.scope.length === 0)
|
|
77
|
+
throw new Error(`baseline entry ${index} has an invalid scope`);
|
|
78
|
+
if (typeof value.file !== 'string' || value.file.length === 0)
|
|
79
|
+
throw new Error(`baseline entry ${index} has an invalid file`);
|
|
80
|
+
assertWorkflowCommandData(value.scope, `baseline entry ${index}`);
|
|
81
|
+
assertWorkflowCommandData(value.file, `baseline entry ${index}`);
|
|
82
|
+
if (typeof value.complexity !== 'number' ||
|
|
83
|
+
!Number.isSafeInteger(value.complexity) ||
|
|
84
|
+
value.complexity < 0)
|
|
85
|
+
throw new Error(`baseline entry ${index} has an invalid complexity`);
|
|
86
|
+
return { complexity: value.complexity, file: value.file, scope: value.scope };
|
|
87
|
+
}
|
|
88
|
+
function isRecord(value) {
|
|
89
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
90
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const SCC_COMPLEXITY_LIMIT = 50;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const SCC_COMPLEXITY_LIMIT = 50;
|
|
@@ -1,19 +1,11 @@
|
|
|
1
1
|
import type { SharedContext } from '../shared-context/index.mts';
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
notMatch?: string;
|
|
8
|
-
tmpdirPrefix?: string;
|
|
9
|
-
command?: string;
|
|
10
|
-
}
|
|
11
|
-
export interface SccComplexityViolation {
|
|
12
|
-
file: string;
|
|
13
|
-
complexity: number;
|
|
14
|
-
}
|
|
2
|
+
import type { SccComplexityOptions, SccComplexityScope, SccComplexityViolation } from './types.mts';
|
|
3
|
+
export { parseSccComplexityBaseline, SCC_COMPLEXITY_BASELINE_VERSION } from './baseline.mts';
|
|
4
|
+
export { SCC_COMPLEXITY_LIMIT } from './constants.mts';
|
|
5
|
+
export type { SccComplexityBaseline, SccComplexityBaselineEntry, SccComplexityOptions, SccComplexityScope, SccComplexityViolation, } from './types.mts';
|
|
6
|
+
type RunScc = (outputPath: string, scope?: SccComplexityScope) => Promise<string>;
|
|
15
7
|
export declare function buildSccArgs(options?: SccComplexityOptions): string[];
|
|
16
|
-
export declare function checkSccComplexity(ctx: SharedContext, options?: SccComplexityOptions, runScc?:
|
|
8
|
+
export declare function checkSccComplexity(ctx: SharedContext, options?: SccComplexityOptions, runScc?: RunScc): Promise<{
|
|
17
9
|
errors: string[];
|
|
18
10
|
}>;
|
|
19
11
|
export declare function parseSccComplexityViolations(json: string, trackedFileSet: ReadonlySet<string>, limit?: number): SccComplexityViolation[];
|