@wix/pathgrade 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/pathgrade.js +2 -0
- package/dist/affected/anchor.d.ts +18 -0
- package/dist/affected/anchor.js +42 -0
- package/dist/affected/config.d.ts +25 -0
- package/dist/affected/config.js +98 -0
- package/dist/affected/format.d.ts +45 -0
- package/dist/affected/format.js +87 -0
- package/dist/affected/git.d.ts +33 -0
- package/dist/affected/git.js +68 -0
- package/dist/affected/glob.d.ts +17 -0
- package/dist/affected/glob.js +25 -0
- package/dist/affected/meta.d.ts +26 -0
- package/dist/affected/meta.js +158 -0
- package/dist/affected/select.d.ts +30 -0
- package/dist/affected/select.js +101 -0
- package/dist/affected/sidecar.d.ts +32 -0
- package/dist/affected/sidecar.js +86 -0
- package/dist/affected/types.d.ts +43 -0
- package/dist/affected/types.js +7 -0
- package/dist/agents/claude/ask-user-answer-store.d.ts +28 -0
- package/dist/agents/claude/ask-user-answer-store.js +29 -0
- package/dist/agents/claude/ask-user-bridge.d.ts +56 -0
- package/dist/agents/claude/ask-user-bridge.js +162 -0
- package/dist/agents/claude/sdk-message-projector.d.ts +43 -0
- package/dist/agents/claude/sdk-message-projector.js +208 -0
- package/dist/agents/claude/sdk-options.d.ts +52 -0
- package/dist/agents/claude/sdk-options.js +79 -0
- package/dist/agents/claude.d.ts +56 -0
- package/dist/agents/claude.js +143 -0
- package/dist/agents/codex-app-server/agent.d.ts +35 -0
- package/dist/agents/codex-app-server/agent.js +388 -0
- package/dist/agents/codex-app-server/fixtures/app-server-client.d.ts +19 -0
- package/dist/agents/codex-app-server/fixtures/app-server-client.js +104 -0
- package/dist/agents/codex-app-server/fixtures/run-gate.d.ts +25 -0
- package/dist/agents/codex-app-server/fixtures/run-gate.js +48 -0
- package/dist/agents/codex-app-server/protocol/ClientRequest.d.ts +27 -0
- package/dist/agents/codex-app-server/protocol/ClientRequest.js +7 -0
- package/dist/agents/codex-app-server/protocol/DynamicToolCallParams.d.ts +9 -0
- package/dist/agents/codex-app-server/protocol/DynamicToolCallParams.js +4 -0
- package/dist/agents/codex-app-server/protocol/GrantedPermissionProfile.d.ts +6 -0
- package/dist/agents/codex-app-server/protocol/GrantedPermissionProfile.js +7 -0
- package/dist/agents/codex-app-server/protocol/McpElicitationRequestParams.d.ts +23 -0
- package/dist/agents/codex-app-server/protocol/McpElicitationRequestParams.js +8 -0
- package/dist/agents/codex-app-server/protocol/Op.d.ts +11 -0
- package/dist/agents/codex-app-server/protocol/Op.js +5 -0
- package/dist/agents/codex-app-server/protocol/PermissionsRequestApprovalParams.d.ts +10 -0
- package/dist/agents/codex-app-server/protocol/PermissionsRequestApprovalParams.js +6 -0
- package/dist/agents/codex-app-server/protocol/PermissionsRequestApprovalResponse.d.ts +15 -0
- package/dist/agents/codex-app-server/protocol/PermissionsRequestApprovalResponse.js +4 -0
- package/dist/agents/codex-app-server/protocol/SandboxMode.d.ts +1 -0
- package/dist/agents/codex-app-server/protocol/SandboxMode.js +4 -0
- package/dist/agents/codex-app-server/protocol/ServerRequest.d.ts +42 -0
- package/dist/agents/codex-app-server/protocol/ServerRequest.js +7 -0
- package/dist/agents/codex-app-server/protocol/ThreadStartParams.d.ts +45 -0
- package/dist/agents/codex-app-server/protocol/ThreadStartParams.js +10 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputAnswer.d.ts +6 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputAnswer.js +4 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputOption.d.ts +7 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputOption.js +4 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputParams.d.ts +10 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputParams.js +4 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputQuestion.d.ts +12 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputQuestion.js +4 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputResponse.d.ts +9 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputResponse.js +4 -0
- package/dist/agents/codex-app-server/protocol/TurnCompletedNotification.d.ts +5 -0
- package/dist/agents/codex-app-server/protocol/TurnCompletedNotification.js +4 -0
- package/dist/agents/codex-app-server/protocol/index.d.ts +16 -0
- package/dist/agents/codex-app-server/protocol/index.js +11 -0
- package/dist/agents/codex-app-server/transport.d.ts +84 -0
- package/dist/agents/codex-app-server/transport.js +246 -0
- package/dist/agents/codex-app-server/wire-translators.d.ts +19 -0
- package/dist/agents/codex-app-server/wire-translators.js +52 -0
- package/dist/agents/codex.d.ts +7 -0
- package/dist/agents/codex.js +164 -0
- package/dist/agents/cursor.d.ts +39 -0
- package/dist/agents/cursor.js +233 -0
- package/dist/agents/registry.d.ts +17 -0
- package/dist/agents/registry.js +27 -0
- package/dist/agents/transcript-agent.d.ts +14 -0
- package/dist/agents/transcript-agent.js +59 -0
- package/dist/analytics/engine.d.ts +18 -0
- package/dist/analytics/engine.js +66 -0
- package/dist/commands/affected.d.ts +31 -0
- package/dist/commands/affected.js +150 -0
- package/dist/commands/analyze.d.ts +21 -0
- package/dist/commands/analyze.js +167 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +242 -0
- package/dist/commands/preview-reactions.d.ts +10 -0
- package/dist/commands/preview-reactions.js +88 -0
- package/dist/commands/preview.d.ts +5 -0
- package/dist/commands/preview.js +17 -0
- package/dist/commands/report.d.ts +39 -0
- package/dist/commands/report.js +108 -0
- package/dist/commands/run-args.d.ts +22 -0
- package/dist/commands/run-args.js +69 -0
- package/dist/commands/run-changed.d.ts +26 -0
- package/dist/commands/run-changed.js +138 -0
- package/dist/commands/validate.d.ts +13 -0
- package/dist/commands/validate.js +233 -0
- package/dist/core/mcp-mock.d.ts +3 -0
- package/dist/core/mcp-mock.js +23 -0
- package/dist/core/mcp-mock.types.d.ts +15 -0
- package/dist/core/mcp-mock.types.js +1 -0
- package/dist/core/skills.d.ts +15 -0
- package/dist/core/skills.js +76 -0
- package/dist/evals/discovery.d.ts +7 -0
- package/dist/evals/discovery.js +69 -0
- package/dist/mcp-mock-server.d.ts +1 -0
- package/dist/mcp-mock-server.js +116 -0
- package/dist/pathgrade.d.ts +10 -0
- package/dist/pathgrade.js +243 -0
- package/dist/plugin/index.d.ts +9 -0
- package/dist/plugin/index.js +81 -0
- package/dist/plugin/lifecycle.d.ts +45 -0
- package/dist/plugin/lifecycle.js +146 -0
- package/dist/plugin/reporter.d.ts +36 -0
- package/dist/plugin/reporter.js +275 -0
- package/dist/plugin/setup.d.ts +1 -0
- package/dist/plugin/setup.js +3 -0
- package/dist/providers/copy-filter.d.ts +18 -0
- package/dist/providers/copy-filter.js +66 -0
- package/dist/providers/credentials.d.ts +36 -0
- package/dist/providers/credentials.js +202 -0
- package/dist/providers/mcp-config.d.ts +34 -0
- package/dist/providers/mcp-config.js +55 -0
- package/dist/providers/sandbox-exec.d.ts +8 -0
- package/dist/providers/sandbox-exec.js +82 -0
- package/dist/providers/sandbox.d.ts +22 -0
- package/dist/providers/sandbox.js +158 -0
- package/dist/providers/sandboxed-claude-spawn.d.ts +24 -0
- package/dist/providers/sandboxed-claude-spawn.js +57 -0
- package/dist/providers/workspace.d.ts +15 -0
- package/dist/providers/workspace.js +73 -0
- package/dist/reporters/browser.d.ts +2 -0
- package/dist/reporters/browser.js +85 -0
- package/dist/reporters/cli.d.ts +2 -0
- package/dist/reporters/cli.js +131 -0
- package/dist/reporters/diagnostics.d.ts +46 -0
- package/dist/reporters/diagnostics.js +147 -0
- package/dist/reporters/github-comment.d.ts +67 -0
- package/dist/reporters/github-comment.js +252 -0
- package/dist/reporters/loader.d.ts +8 -0
- package/dist/reporters/loader.js +36 -0
- package/dist/reporters/results-path.d.ts +2 -0
- package/dist/reporters/results-path.js +10 -0
- package/dist/reporters/verbose-emitter.d.ts +69 -0
- package/dist/reporters/verbose-emitter.js +103 -0
- package/dist/sdk/agent-crash.d.ts +19 -0
- package/dist/sdk/agent-crash.js +20 -0
- package/dist/sdk/agent-resolution.d.ts +12 -0
- package/dist/sdk/agent-resolution.js +21 -0
- package/dist/sdk/agent-result-log.d.ts +19 -0
- package/dist/sdk/agent-result-log.js +45 -0
- package/dist/sdk/agent.d.ts +4 -0
- package/dist/sdk/agent.js +354 -0
- package/dist/sdk/ask-bus/bus.d.ts +21 -0
- package/dist/sdk/ask-bus/bus.js +129 -0
- package/dist/sdk/ask-bus/handler.d.ts +31 -0
- package/dist/sdk/ask-bus/handler.js +244 -0
- package/dist/sdk/ask-bus/parsers.d.ts +9 -0
- package/dist/sdk/ask-bus/parsers.js +30 -0
- package/dist/sdk/ask-bus/projection.d.ts +26 -0
- package/dist/sdk/ask-bus/projection.js +45 -0
- package/dist/sdk/ask-bus/types.d.ts +69 -0
- package/dist/sdk/ask-bus/types.js +1 -0
- package/dist/sdk/chat.d.ts +29 -0
- package/dist/sdk/chat.js +93 -0
- package/dist/sdk/conversation-window.d.ts +14 -0
- package/dist/sdk/conversation-window.js +38 -0
- package/dist/sdk/converse.d.ts +42 -0
- package/dist/sdk/converse.js +329 -0
- package/dist/sdk/eval-runtime.d.ts +10 -0
- package/dist/sdk/eval-runtime.js +17 -0
- package/dist/sdk/evaluate.d.ts +13 -0
- package/dist/sdk/evaluate.js +325 -0
- package/dist/sdk/index.d.ts +31 -0
- package/dist/sdk/index.js +20 -0
- package/dist/sdk/judge-pipeline.d.ts +7 -0
- package/dist/sdk/judge-pipeline.js +178 -0
- package/dist/sdk/judge-prompt-builder.d.ts +7 -0
- package/dist/sdk/judge-prompt-builder.js +76 -0
- package/dist/sdk/judge-tool-runner.d.ts +19 -0
- package/dist/sdk/judge-tool-runner.js +51 -0
- package/dist/sdk/judge-tool-session.d.ts +47 -0
- package/dist/sdk/judge-tool-session.js +213 -0
- package/dist/sdk/judge-tools.d.ts +29 -0
- package/dist/sdk/judge-tools.js +283 -0
- package/dist/sdk/managed-session.d.ts +50 -0
- package/dist/sdk/managed-session.js +110 -0
- package/dist/sdk/persona.d.ts +5 -0
- package/dist/sdk/persona.js +33 -0
- package/dist/sdk/reaction-loader.d.ts +4 -0
- package/dist/sdk/reaction-loader.js +119 -0
- package/dist/sdk/reaction-preview.d.ts +6 -0
- package/dist/sdk/reaction-preview.js +94 -0
- package/dist/sdk/run-scorer.d.ts +10 -0
- package/dist/sdk/run-scorer.js +124 -0
- package/dist/sdk/runtime-policy.d.ts +21 -0
- package/dist/sdk/runtime-policy.js +89 -0
- package/dist/sdk/scorer-utils.d.ts +6 -0
- package/dist/sdk/scorer-utils.js +39 -0
- package/dist/sdk/scorers.d.ts +64 -0
- package/dist/sdk/scorers.js +80 -0
- package/dist/sdk/snapshots.d.ts +41 -0
- package/dist/sdk/snapshots.js +148 -0
- package/dist/sdk/types.d.ts +416 -0
- package/dist/sdk/types.js +17 -0
- package/dist/sdk/visible-turn.d.ts +4 -0
- package/dist/sdk/visible-turn.js +23 -0
- package/dist/tool-events.d.ts +45 -0
- package/dist/tool-events.js +167 -0
- package/dist/types.d.ts +372 -0
- package/dist/types.js +37 -0
- package/dist/utils/cli.d.ts +55 -0
- package/dist/utils/cli.js +131 -0
- package/dist/utils/env.d.ts +8 -0
- package/dist/utils/env.js +26 -0
- package/dist/utils/llm-mocks.d.ts +43 -0
- package/dist/utils/llm-mocks.js +108 -0
- package/dist/utils/llm-providers/anthropic.d.ts +2 -0
- package/dist/utils/llm-providers/anthropic.js +132 -0
- package/dist/utils/llm-providers/cli.d.ts +20 -0
- package/dist/utils/llm-providers/cli.js +162 -0
- package/dist/utils/llm-providers/openai.d.ts +2 -0
- package/dist/utils/llm-providers/openai.js +56 -0
- package/dist/utils/llm-types.d.ts +107 -0
- package/dist/utils/llm-types.js +1 -0
- package/dist/utils/llm.d.ts +36 -0
- package/dist/utils/llm.js +209 -0
- package/dist/utils/shutdown.d.ts +15 -0
- package/dist/utils/shutdown.js +41 -0
- package/dist/utils/timeout.d.ts +8 -0
- package/dist/utils/timeout.js +32 -0
- package/dist/viewer.html +1178 -0
- package/package.json +88 -0
- package/templates/eval.ts.template +28 -0
package/bin/pathgrade.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill-anchor resolution for affected-eval selection.
|
|
3
|
+
*
|
|
4
|
+
* Walks the directory tree upward from an eval file looking for the nearest
|
|
5
|
+
* `SKILL.md` file. The first ancestor directory containing `SKILL.md` is the
|
|
6
|
+
* **skill root**. Returns the skill root path repo-relative (forward slashes),
|
|
7
|
+
* or `null` if no ancestor has `SKILL.md`.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the nearest SKILL.md ancestor directory for an eval file.
|
|
11
|
+
*
|
|
12
|
+
* @param evalFile Absolute or repo-relative path to the `.eval.ts` file.
|
|
13
|
+
* @param repoRoot Absolute path to the repo root. All returned paths are
|
|
14
|
+
* repo-root-relative with forward slashes.
|
|
15
|
+
* @returns the skill root directory path (e.g., `"skills/foo"`), or `null`
|
|
16
|
+
* if no `SKILL.md` ancestor is found up to the repo root.
|
|
17
|
+
*/
|
|
18
|
+
export declare function findSkillRoot(evalFile: string, repoRoot: string): string | null;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill-anchor resolution for affected-eval selection.
|
|
3
|
+
*
|
|
4
|
+
* Walks the directory tree upward from an eval file looking for the nearest
|
|
5
|
+
* `SKILL.md` file. The first ancestor directory containing `SKILL.md` is the
|
|
6
|
+
* **skill root**. Returns the skill root path repo-relative (forward slashes),
|
|
7
|
+
* or `null` if no ancestor has `SKILL.md`.
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
function toPosix(p) {
|
|
12
|
+
return p.split(path.sep).join('/');
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Resolve the nearest SKILL.md ancestor directory for an eval file.
|
|
16
|
+
*
|
|
17
|
+
* @param evalFile Absolute or repo-relative path to the `.eval.ts` file.
|
|
18
|
+
* @param repoRoot Absolute path to the repo root. All returned paths are
|
|
19
|
+
* repo-root-relative with forward slashes.
|
|
20
|
+
* @returns the skill root directory path (e.g., `"skills/foo"`), or `null`
|
|
21
|
+
* if no `SKILL.md` ancestor is found up to the repo root.
|
|
22
|
+
*/
|
|
23
|
+
export function findSkillRoot(evalFile, repoRoot) {
|
|
24
|
+
const absEvalFile = path.isAbsolute(evalFile)
|
|
25
|
+
? evalFile
|
|
26
|
+
: path.resolve(repoRoot, evalFile);
|
|
27
|
+
const absRepoRoot = path.resolve(repoRoot);
|
|
28
|
+
let current = path.dirname(absEvalFile);
|
|
29
|
+
while (true) {
|
|
30
|
+
if (fs.existsSync(path.join(current, 'SKILL.md'))) {
|
|
31
|
+
const rel = path.relative(absRepoRoot, current);
|
|
32
|
+
return rel === '' ? '.' : toPosix(rel);
|
|
33
|
+
}
|
|
34
|
+
if (current === absRepoRoot)
|
|
35
|
+
break;
|
|
36
|
+
const parent = path.dirname(current);
|
|
37
|
+
if (parent === current)
|
|
38
|
+
break; // filesystem root
|
|
39
|
+
current = parent;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load `vitest.config.ts` at CLI time and extract the pathgrade plugin's
|
|
3
|
+
* `affected.global` configuration.
|
|
4
|
+
*
|
|
5
|
+
* Why this file exists: `pathgrade()` runs *inside* the vitest process, but
|
|
6
|
+
* `pathgrade affected` / `pathgrade run --changed` compute selection
|
|
7
|
+
* *before* vitest spawns. So the CLI must read the plugin's options itself.
|
|
8
|
+
* We do that by importing the user's `vitest.config.ts` via `jiti`
|
|
9
|
+
* (already a pathgrade dependency), walking the resulting plugin list, and
|
|
10
|
+
* finding the pathgrade plugin instance — which annotates itself with
|
|
11
|
+
* `__pathgradeOptions` so that pre-vitest tooling can extract its options
|
|
12
|
+
* without re-executing the plugin factory.
|
|
13
|
+
*
|
|
14
|
+
* v1 limitation: `vitest.workspace.ts` is detected and warned about, but
|
|
15
|
+
* not traversed. Affected-selection still works via per-eval deps; only
|
|
16
|
+
* the `affected.global` short-circuit is disabled in workspace mode.
|
|
17
|
+
* Documented in USER_GUIDE for Issue 14.
|
|
18
|
+
*/
|
|
19
|
+
export interface AffectedConfig {
|
|
20
|
+
global: string[];
|
|
21
|
+
}
|
|
22
|
+
export interface LoadOptions {
|
|
23
|
+
onWarning?: (message: string) => void;
|
|
24
|
+
}
|
|
25
|
+
export declare function loadAffectedConfig(repoRoot: string, options?: LoadOptions): Promise<AffectedConfig>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load `vitest.config.ts` at CLI time and extract the pathgrade plugin's
|
|
3
|
+
* `affected.global` configuration.
|
|
4
|
+
*
|
|
5
|
+
* Why this file exists: `pathgrade()` runs *inside* the vitest process, but
|
|
6
|
+
* `pathgrade affected` / `pathgrade run --changed` compute selection
|
|
7
|
+
* *before* vitest spawns. So the CLI must read the plugin's options itself.
|
|
8
|
+
* We do that by importing the user's `vitest.config.ts` via `jiti`
|
|
9
|
+
* (already a pathgrade dependency), walking the resulting plugin list, and
|
|
10
|
+
* finding the pathgrade plugin instance — which annotates itself with
|
|
11
|
+
* `__pathgradeOptions` so that pre-vitest tooling can extract its options
|
|
12
|
+
* without re-executing the plugin factory.
|
|
13
|
+
*
|
|
14
|
+
* v1 limitation: `vitest.workspace.ts` is detected and warned about, but
|
|
15
|
+
* not traversed. Affected-selection still works via per-eval deps; only
|
|
16
|
+
* the `affected.global` short-circuit is disabled in workspace mode.
|
|
17
|
+
* Documented in USER_GUIDE for Issue 14.
|
|
18
|
+
*/
|
|
19
|
+
import * as fs from 'fs';
|
|
20
|
+
import * as path from 'path';
|
|
21
|
+
import { createJiti } from 'jiti';
|
|
22
|
+
const VITEST_CONFIG_CANDIDATES = [
|
|
23
|
+
'vitest.config.ts',
|
|
24
|
+
'vitest.config.mts',
|
|
25
|
+
'vitest.config.js',
|
|
26
|
+
'vitest.config.mjs',
|
|
27
|
+
];
|
|
28
|
+
const WORKSPACE_CANDIDATES = [
|
|
29
|
+
'vitest.workspace.ts',
|
|
30
|
+
'vitest.workspace.js',
|
|
31
|
+
'vitest.workspace.mts',
|
|
32
|
+
'vitest.workspace.mjs',
|
|
33
|
+
];
|
|
34
|
+
export async function loadAffectedConfig(repoRoot, options = {}) {
|
|
35
|
+
const warn = options.onWarning ?? (() => { });
|
|
36
|
+
// v1 limitation: workspace mode skips the short-circuit.
|
|
37
|
+
for (const candidate of WORKSPACE_CANDIDATES) {
|
|
38
|
+
if (fs.existsSync(path.join(repoRoot, candidate))) {
|
|
39
|
+
warn(`pathgrade: ${candidate} detected — v1 does not read affected.global from workspace configs; ` +
|
|
40
|
+
`selection will still work, but the repo-wide short-circuit is disabled.`);
|
|
41
|
+
return { global: [] };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const configPath = VITEST_CONFIG_CANDIDATES
|
|
45
|
+
.map(c => path.join(repoRoot, c))
|
|
46
|
+
.find(p => fs.existsSync(p));
|
|
47
|
+
if (!configPath)
|
|
48
|
+
return { global: [] };
|
|
49
|
+
let loaded;
|
|
50
|
+
try {
|
|
51
|
+
const jiti = createJiti(repoRoot, { interopDefault: true });
|
|
52
|
+
loaded = await jiti.import(configPath, { default: true });
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
warn(`pathgrade: failed to load ${path.relative(repoRoot, configPath)}: ${errMsg(err)}`);
|
|
56
|
+
return { global: [] };
|
|
57
|
+
}
|
|
58
|
+
const plugins = findPluginsList(loaded);
|
|
59
|
+
if (!plugins) {
|
|
60
|
+
warn(`pathgrade: no plugin list found in ${path.relative(repoRoot, configPath)}`);
|
|
61
|
+
return { global: [] };
|
|
62
|
+
}
|
|
63
|
+
const pathgradePlugin = plugins.find(p => isPathgradePlugin(p));
|
|
64
|
+
if (!pathgradePlugin) {
|
|
65
|
+
warn(`pathgrade: no pathgrade plugin found in ${path.relative(repoRoot, configPath)} — ` +
|
|
66
|
+
`affected.global will be empty. Add \`pathgrade()\` to your vitest plugins list.`);
|
|
67
|
+
return { global: [] };
|
|
68
|
+
}
|
|
69
|
+
const opts = pathgradePlugin.__pathgradeOptions ?? {};
|
|
70
|
+
const global = opts.affected?.global;
|
|
71
|
+
return { global: Array.isArray(global) ? global : [] };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Given a loaded vitest config (either the raw export or a `defineConfig()`
|
|
75
|
+
* result), locate the plugin array. Supports both:
|
|
76
|
+
* - `defineConfig({ plugins: [...] })` (top-level)
|
|
77
|
+
* - `defineConfig({ test: { plugins: [...] } })` (vitest convention)
|
|
78
|
+
*/
|
|
79
|
+
function findPluginsList(config) {
|
|
80
|
+
if (!config || typeof config !== 'object')
|
|
81
|
+
return null;
|
|
82
|
+
const c = config;
|
|
83
|
+
if (Array.isArray(c.plugins))
|
|
84
|
+
return c.plugins;
|
|
85
|
+
if (c.test && typeof c.test === 'object' && Array.isArray(c.test.plugins)) {
|
|
86
|
+
return c.test.plugins;
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
function isPathgradePlugin(plugin) {
|
|
91
|
+
if (!plugin || typeof plugin !== 'object')
|
|
92
|
+
return false;
|
|
93
|
+
const p = plugin;
|
|
94
|
+
return p.name === 'pathgrade' || '__pathgradeOptions' in p;
|
|
95
|
+
}
|
|
96
|
+
function errMsg(err) {
|
|
97
|
+
return err instanceof Error ? err.message : String(err);
|
|
98
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatters for `pathgrade affected --explain` and `--json`.
|
|
3
|
+
*
|
|
4
|
+
* JSON wire-format uses snake_case; the TS interface uses camelCase.
|
|
5
|
+
* The enum values (`SelectionReason`) go on the wire as-is.
|
|
6
|
+
*
|
|
7
|
+
* This file stays pure — no fs, no process I/O — so it can be re-used by
|
|
8
|
+
* Issue 11's sidecar writer (which uses the same shape, except for the
|
|
9
|
+
* size-optimized `changed_files_count` summary).
|
|
10
|
+
*/
|
|
11
|
+
import type { SelectionResult } from './types.js';
|
|
12
|
+
interface JsonSelectionEntry {
|
|
13
|
+
file: string;
|
|
14
|
+
reason: string;
|
|
15
|
+
matched_glob?: string;
|
|
16
|
+
}
|
|
17
|
+
interface JsonSelectionSkipped {
|
|
18
|
+
file: string;
|
|
19
|
+
reason: string;
|
|
20
|
+
}
|
|
21
|
+
export interface JsonSelectionResult {
|
|
22
|
+
base_ref: string;
|
|
23
|
+
changed_files: string[];
|
|
24
|
+
global_match?: string;
|
|
25
|
+
selected: JsonSelectionEntry[];
|
|
26
|
+
skipped: JsonSelectionSkipped[];
|
|
27
|
+
warnings: string[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Transform `SelectionResult` (camelCase) into the snake_case wire shape
|
|
31
|
+
* consumed by `--json` and the sidecar-writer (Issue 11).
|
|
32
|
+
*/
|
|
33
|
+
export declare function toJsonShape(result: SelectionResult): JsonSelectionResult;
|
|
34
|
+
/**
|
|
35
|
+
* Stable JSON: keys in a fixed order, arrays pre-sorted by `toJsonShape`.
|
|
36
|
+
* Emits a trailing newline so downstream tools can treat it like any other
|
|
37
|
+
* command output.
|
|
38
|
+
*/
|
|
39
|
+
export declare function formatJson(result: SelectionResult): string;
|
|
40
|
+
/**
|
|
41
|
+
* Human-readable explanation for `--explain`. One section: base ref,
|
|
42
|
+
* changed files, per-eval decision with reason, warnings footer.
|
|
43
|
+
*/
|
|
44
|
+
export declare function formatExplain(result: SelectionResult): string;
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatters for `pathgrade affected --explain` and `--json`.
|
|
3
|
+
*
|
|
4
|
+
* JSON wire-format uses snake_case; the TS interface uses camelCase.
|
|
5
|
+
* The enum values (`SelectionReason`) go on the wire as-is.
|
|
6
|
+
*
|
|
7
|
+
* This file stays pure — no fs, no process I/O — so it can be re-used by
|
|
8
|
+
* Issue 11's sidecar writer (which uses the same shape, except for the
|
|
9
|
+
* size-optimized `changed_files_count` summary).
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Transform `SelectionResult` (camelCase) into the snake_case wire shape
|
|
13
|
+
* consumed by `--json` and the sidecar-writer (Issue 11).
|
|
14
|
+
*/
|
|
15
|
+
export function toJsonShape(result) {
|
|
16
|
+
const out = {
|
|
17
|
+
base_ref: result.baseRef,
|
|
18
|
+
changed_files: [...result.changedFiles].sort(),
|
|
19
|
+
selected: result.selected.map(s => {
|
|
20
|
+
const entry = { file: s.file, reason: s.reason };
|
|
21
|
+
if (s.matchedGlob !== undefined)
|
|
22
|
+
entry.matched_glob = s.matchedGlob;
|
|
23
|
+
return entry;
|
|
24
|
+
}),
|
|
25
|
+
skipped: [...result.skipped].sort((a, b) => a.file.localeCompare(b.file)),
|
|
26
|
+
warnings: [...result.warnings],
|
|
27
|
+
};
|
|
28
|
+
if (result.globalMatch !== undefined)
|
|
29
|
+
out.global_match = result.globalMatch;
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Stable JSON: keys in a fixed order, arrays pre-sorted by `toJsonShape`.
|
|
34
|
+
* Emits a trailing newline so downstream tools can treat it like any other
|
|
35
|
+
* command output.
|
|
36
|
+
*/
|
|
37
|
+
export function formatJson(result) {
|
|
38
|
+
return JSON.stringify(toJsonShape(result), null, 2) + '\n';
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Human-readable explanation for `--explain`. One section: base ref,
|
|
42
|
+
* changed files, per-eval decision with reason, warnings footer.
|
|
43
|
+
*/
|
|
44
|
+
export function formatExplain(result) {
|
|
45
|
+
const lines = [];
|
|
46
|
+
lines.push(`base: ${result.baseRef}`);
|
|
47
|
+
lines.push(`changed files: ${result.changedFiles.length}`);
|
|
48
|
+
for (const f of result.changedFiles)
|
|
49
|
+
lines.push(` - ${f}`);
|
|
50
|
+
if (result.globalMatch) {
|
|
51
|
+
lines.push('');
|
|
52
|
+
lines.push(`global trigger fired: \`${result.globalMatch}\` — all ${result.selected.length} evals selected`);
|
|
53
|
+
}
|
|
54
|
+
lines.push('');
|
|
55
|
+
lines.push(`selected: ${result.selected.length}`);
|
|
56
|
+
for (const s of result.selected) {
|
|
57
|
+
lines.push(` + ${s.file} — ${reasonLabel(s.reason, s.matchedGlob)}`);
|
|
58
|
+
}
|
|
59
|
+
if (result.skipped.length > 0) {
|
|
60
|
+
lines.push('');
|
|
61
|
+
lines.push(`skipped: ${result.skipped.length}`);
|
|
62
|
+
for (const s of result.skipped) {
|
|
63
|
+
lines.push(` - ${s.file} — ${s.reason}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (result.warnings.length > 0) {
|
|
67
|
+
lines.push('');
|
|
68
|
+
lines.push('warnings:');
|
|
69
|
+
for (const w of result.warnings)
|
|
70
|
+
lines.push(` ⚠ ${w}`);
|
|
71
|
+
}
|
|
72
|
+
return lines.join('\n') + '\n';
|
|
73
|
+
}
|
|
74
|
+
function reasonLabel(reason, matchedGlob) {
|
|
75
|
+
switch (reason) {
|
|
76
|
+
case 'deps-match':
|
|
77
|
+
return matchedGlob ? `deps match (${matchedGlob})` : 'deps match';
|
|
78
|
+
case 'always-run':
|
|
79
|
+
return 'selected: alwaysRun';
|
|
80
|
+
case 'global-match':
|
|
81
|
+
return matchedGlob ? `global match (${matchedGlob})` : 'global match';
|
|
82
|
+
case 'on-missing-fail-closed':
|
|
83
|
+
return 'selected: onMissing fail-closed';
|
|
84
|
+
default:
|
|
85
|
+
return reason;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git-based change detection for affected-eval-selection.
|
|
3
|
+
*
|
|
4
|
+
* Resolves the base ref (merge-base preferred) and computes the repo-relative
|
|
5
|
+
* file list that changed between that base and HEAD. See PRD §"Change Detection".
|
|
6
|
+
*
|
|
7
|
+
* Three precedence branches for base-ref resolution, in order:
|
|
8
|
+
* 1. `GITHUB_BASE_REF` (GitHub Actions pull_request event) → merge-base
|
|
9
|
+
* between `refs/remotes/origin/$GITHUB_BASE_REF` and `HEAD`.
|
|
10
|
+
* 2. `GITHUB_EVENT_BEFORE` (push event) if non-zero SHA → diff vs. that SHA.
|
|
11
|
+
* The all-zero SHA (`000…000`) is emitted on branch-first-push and falls
|
|
12
|
+
* through to branch 3.
|
|
13
|
+
* 3. Fallback: `git merge-base HEAD origin/main`.
|
|
14
|
+
*
|
|
15
|
+
* Merge-base (not plain two-dot) is critical for long-lived branches whose
|
|
16
|
+
* base has diverged. Vitest's built-in `--changed` gets this wrong; we must not.
|
|
17
|
+
*/
|
|
18
|
+
export type BaseRefResolution = {
|
|
19
|
+
base: string;
|
|
20
|
+
sha: string;
|
|
21
|
+
} | {
|
|
22
|
+
error: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Resolve the git base ref to diff against. Accepts an env object (defaults
|
|
26
|
+
* to `process.env`) so tests can exercise each precedence branch.
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveBaseRef(env?: NodeJS.ProcessEnv): BaseRefResolution;
|
|
29
|
+
/**
|
|
30
|
+
* Repo-relative file list changed between `<base>...HEAD`. The three-dot
|
|
31
|
+
* form covers adds, modifications, deletions, and both sides of renames.
|
|
32
|
+
*/
|
|
33
|
+
export declare function computeChangedFiles(base: string): string[];
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git-based change detection for affected-eval-selection.
|
|
3
|
+
*
|
|
4
|
+
* Resolves the base ref (merge-base preferred) and computes the repo-relative
|
|
5
|
+
* file list that changed between that base and HEAD. See PRD §"Change Detection".
|
|
6
|
+
*
|
|
7
|
+
* Three precedence branches for base-ref resolution, in order:
|
|
8
|
+
* 1. `GITHUB_BASE_REF` (GitHub Actions pull_request event) → merge-base
|
|
9
|
+
* between `refs/remotes/origin/$GITHUB_BASE_REF` and `HEAD`.
|
|
10
|
+
* 2. `GITHUB_EVENT_BEFORE` (push event) if non-zero SHA → diff vs. that SHA.
|
|
11
|
+
* The all-zero SHA (`000…000`) is emitted on branch-first-push and falls
|
|
12
|
+
* through to branch 3.
|
|
13
|
+
* 3. Fallback: `git merge-base HEAD origin/main`.
|
|
14
|
+
*
|
|
15
|
+
* Merge-base (not plain two-dot) is critical for long-lived branches whose
|
|
16
|
+
* base has diverged. Vitest's built-in `--changed` gets this wrong; we must not.
|
|
17
|
+
*/
|
|
18
|
+
import { execFileSync } from 'child_process';
|
|
19
|
+
const ALL_ZERO_SHA = '0000000000000000000000000000000000000000';
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the git base ref to diff against. Accepts an env object (defaults
|
|
22
|
+
* to `process.env`) so tests can exercise each precedence branch.
|
|
23
|
+
*/
|
|
24
|
+
export function resolveBaseRef(env = process.env) {
|
|
25
|
+
const { GITHUB_BASE_REF, GITHUB_EVENT_BEFORE } = env;
|
|
26
|
+
if (GITHUB_BASE_REF) {
|
|
27
|
+
const ref = `refs/remotes/origin/${GITHUB_BASE_REF}`;
|
|
28
|
+
try {
|
|
29
|
+
const sha = runGit(['merge-base', ref, 'HEAD']);
|
|
30
|
+
return { base: ref, sha };
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
return { error: formatMergeBaseError(err) };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (GITHUB_EVENT_BEFORE && GITHUB_EVENT_BEFORE !== ALL_ZERO_SHA) {
|
|
37
|
+
// Explicit SHA: no merge-base resolution needed.
|
|
38
|
+
return { base: GITHUB_EVENT_BEFORE, sha: GITHUB_EVENT_BEFORE };
|
|
39
|
+
}
|
|
40
|
+
// Fallback: merge-base with origin/main.
|
|
41
|
+
try {
|
|
42
|
+
const sha = runGit(['merge-base', 'HEAD', 'origin/main']);
|
|
43
|
+
return { base: 'origin/main', sha };
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
return { error: formatMergeBaseError(err) };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Repo-relative file list changed between `<base>...HEAD`. The three-dot
|
|
51
|
+
* form covers adds, modifications, deletions, and both sides of renames.
|
|
52
|
+
*/
|
|
53
|
+
export function computeChangedFiles(base) {
|
|
54
|
+
const output = runGit(['diff', '--relative', '--name-only', `${base}...HEAD`]);
|
|
55
|
+
return output
|
|
56
|
+
.split('\n')
|
|
57
|
+
.map(l => l.trim())
|
|
58
|
+
.filter(l => l.length > 0);
|
|
59
|
+
}
|
|
60
|
+
function runGit(args) {
|
|
61
|
+
return execFileSync('git', args, { stdio: ['ignore', 'pipe', 'pipe'] }).toString().trim();
|
|
62
|
+
}
|
|
63
|
+
function formatMergeBaseError(err) {
|
|
64
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
65
|
+
return (`pathgrade: merge-base resolution failed — ensure \`actions/checkout@v4\` ` +
|
|
66
|
+
`has \`fetch-depth: 0\` (shallow clones break merge-base). ` +
|
|
67
|
+
`Underlying error: ${detail}`);
|
|
68
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo-root-relative glob matching helpers for the affected-selection pipeline.
|
|
3
|
+
*
|
|
4
|
+
* All paths — both globs and test paths — must be repo-root-relative with
|
|
5
|
+
* forward slashes. Callers are responsible for normalization.
|
|
6
|
+
*/
|
|
7
|
+
export interface GlobIntersection {
|
|
8
|
+
/** If any file matched any glob, which glob fired (first match wins). */
|
|
9
|
+
matchedGlob?: string;
|
|
10
|
+
matched: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Return `{ matched: true, matchedGlob }` if any path in `files` matches any
|
|
14
|
+
* glob in `globs`. Iteration order: globs outer, files inner — we surface the
|
|
15
|
+
* first glob (in declared order) that has at least one hit.
|
|
16
|
+
*/
|
|
17
|
+
export declare function intersect(globs: string[], files: string[]): GlobIntersection;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo-root-relative glob matching helpers for the affected-selection pipeline.
|
|
3
|
+
*
|
|
4
|
+
* All paths — both globs and test paths — must be repo-root-relative with
|
|
5
|
+
* forward slashes. Callers are responsible for normalization.
|
|
6
|
+
*/
|
|
7
|
+
import picomatch from 'picomatch';
|
|
8
|
+
function toMatcher(glob) {
|
|
9
|
+
return picomatch(glob, { dot: true });
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Return `{ matched: true, matchedGlob }` if any path in `files` matches any
|
|
13
|
+
* glob in `globs`. Iteration order: globs outer, files inner — we surface the
|
|
14
|
+
* first glob (in declared order) that has at least one hit.
|
|
15
|
+
*/
|
|
16
|
+
export function intersect(globs, files) {
|
|
17
|
+
for (const glob of globs) {
|
|
18
|
+
const match = toMatcher(glob);
|
|
19
|
+
for (const f of files) {
|
|
20
|
+
if (match(f))
|
|
21
|
+
return { matched: true, matchedGlob: glob };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return { matched: false };
|
|
25
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static extraction of the `__pathgradeMeta` export from an eval file.
|
|
3
|
+
*
|
|
4
|
+
* The eval module is **never executed** — we parse it with the TypeScript
|
|
5
|
+
* compiler's syntax API and walk the AST to find `export const __pathgradeMeta`.
|
|
6
|
+
* This is critical: evals typically perform real work at module load
|
|
7
|
+
* (creating agents, hitting LLMs) and running them just to read a static
|
|
8
|
+
* declaration would defeat the purpose of affected-selection.
|
|
9
|
+
*
|
|
10
|
+
* Consequence: the value must be a literal expression. Dynamic composition
|
|
11
|
+
* (`deps: [...BASE, 'extra']`, spread, computed identifiers) is not supported.
|
|
12
|
+
* Users needing dedup should codegen the file from a build script.
|
|
13
|
+
*
|
|
14
|
+
* Returns `null` when the export is absent. Issue 7 will extend this to
|
|
15
|
+
* throw on malformed meta (non-array deps, invalid globs, path-escape).
|
|
16
|
+
*/
|
|
17
|
+
export interface ParsedMeta {
|
|
18
|
+
deps?: string[];
|
|
19
|
+
extraDeps?: string[];
|
|
20
|
+
alwaysRun?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Parse `__pathgradeMeta` from an eval file's AST. Returns `null` if the
|
|
24
|
+
* export is not present.
|
|
25
|
+
*/
|
|
26
|
+
export declare function parsePathgradeMeta(evalFile: string): ParsedMeta | null;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static extraction of the `__pathgradeMeta` export from an eval file.
|
|
3
|
+
*
|
|
4
|
+
* The eval module is **never executed** — we parse it with the TypeScript
|
|
5
|
+
* compiler's syntax API and walk the AST to find `export const __pathgradeMeta`.
|
|
6
|
+
* This is critical: evals typically perform real work at module load
|
|
7
|
+
* (creating agents, hitting LLMs) and running them just to read a static
|
|
8
|
+
* declaration would defeat the purpose of affected-selection.
|
|
9
|
+
*
|
|
10
|
+
* Consequence: the value must be a literal expression. Dynamic composition
|
|
11
|
+
* (`deps: [...BASE, 'extra']`, spread, computed identifiers) is not supported.
|
|
12
|
+
* Users needing dedup should codegen the file from a build script.
|
|
13
|
+
*
|
|
14
|
+
* Returns `null` when the export is absent. Issue 7 will extend this to
|
|
15
|
+
* throw on malformed meta (non-array deps, invalid globs, path-escape).
|
|
16
|
+
*/
|
|
17
|
+
import * as fs from 'fs';
|
|
18
|
+
import * as ts from 'typescript';
|
|
19
|
+
import picomatch from 'picomatch';
|
|
20
|
+
/**
|
|
21
|
+
* Parse `__pathgradeMeta` from an eval file's AST. Returns `null` if the
|
|
22
|
+
* export is not present.
|
|
23
|
+
*/
|
|
24
|
+
export function parsePathgradeMeta(evalFile) {
|
|
25
|
+
const source = fs.readFileSync(evalFile, 'utf-8');
|
|
26
|
+
const sourceFile = ts.createSourceFile(evalFile, source, ts.ScriptTarget.Latest,
|
|
27
|
+
/* setParentNodes */ true, ts.ScriptKind.TS);
|
|
28
|
+
for (const stmt of sourceFile.statements) {
|
|
29
|
+
const initializer = findMetaInitializer(stmt);
|
|
30
|
+
if (initializer) {
|
|
31
|
+
return extractMeta(initializer, evalFile);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
function findMetaInitializer(stmt) {
|
|
37
|
+
// Match: `export const __pathgradeMeta [: Type] = <expr>`
|
|
38
|
+
if (!ts.isVariableStatement(stmt))
|
|
39
|
+
return null;
|
|
40
|
+
const hasExport = stmt.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
41
|
+
if (!hasExport)
|
|
42
|
+
return null;
|
|
43
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
44
|
+
if (!ts.isIdentifier(decl.name))
|
|
45
|
+
continue;
|
|
46
|
+
if (decl.name.text !== '__pathgradeMeta')
|
|
47
|
+
continue;
|
|
48
|
+
if (decl.initializer)
|
|
49
|
+
return decl.initializer;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
function extractMeta(expr, evalFile) {
|
|
54
|
+
// Optionally unwrap a type assertion: `{...} as PathgradeMeta` / `<PathgradeMeta>{...}`.
|
|
55
|
+
let node = expr;
|
|
56
|
+
while (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node) || ts.isSatisfiesExpression(node)) {
|
|
57
|
+
node = node.expression;
|
|
58
|
+
}
|
|
59
|
+
if (!ts.isObjectLiteralExpression(node)) {
|
|
60
|
+
throw new Error(`${evalFile}: __pathgradeMeta must be an object literal (got ${ts.SyntaxKind[node.kind]}).`);
|
|
61
|
+
}
|
|
62
|
+
const meta = {};
|
|
63
|
+
for (const prop of node.properties) {
|
|
64
|
+
if (!ts.isPropertyAssignment(prop))
|
|
65
|
+
continue;
|
|
66
|
+
const name = propertyKeyName(prop.name);
|
|
67
|
+
if (name === 'deps' || name === 'extraDeps') {
|
|
68
|
+
const globs = extractStringArray(prop.initializer, evalFile, name);
|
|
69
|
+
for (const g of globs)
|
|
70
|
+
validateGlob(g, evalFile, name);
|
|
71
|
+
meta[name] = globs;
|
|
72
|
+
}
|
|
73
|
+
else if (name === 'alwaysRun') {
|
|
74
|
+
meta.alwaysRun = extractBoolean(prop.initializer, evalFile);
|
|
75
|
+
}
|
|
76
|
+
// Unknown keys are silently ignored — forward-compatible.
|
|
77
|
+
}
|
|
78
|
+
return meta;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Validate a single glob entry. Catches three classes of error:
|
|
82
|
+
* - Path-escape: `../` segments that would match outside the repo root.
|
|
83
|
+
* - Unbalanced bracket / brace groups (unclosed `[`, `{` are
|
|
84
|
+
* parse-level mistakes that picomatch silently tolerates but that the
|
|
85
|
+
* PRD requires us to reject as user-intent errors).
|
|
86
|
+
* - Empty string.
|
|
87
|
+
*/
|
|
88
|
+
function validateGlob(glob, evalFile, field) {
|
|
89
|
+
if (glob.length === 0) {
|
|
90
|
+
throw new Error(`${evalFile}: __pathgradeMeta.${field} contains an empty glob.`);
|
|
91
|
+
}
|
|
92
|
+
if (glob.startsWith('../') || glob.includes('/../') || glob === '..') {
|
|
93
|
+
throw new Error(`${evalFile}: __pathgradeMeta.${field} entry "${glob}" escapes the repo root — ` +
|
|
94
|
+
`globs must be repo-root-relative.`);
|
|
95
|
+
}
|
|
96
|
+
const brackets = countUnbalanced(glob, '[', ']');
|
|
97
|
+
const braces = countUnbalanced(glob, '{', '}');
|
|
98
|
+
if (brackets !== 0 || braces !== 0) {
|
|
99
|
+
throw new Error(`${evalFile}: __pathgradeMeta.${field} entry "${glob}" has invalid glob syntax ` +
|
|
100
|
+
`(unbalanced brackets or braces).`);
|
|
101
|
+
}
|
|
102
|
+
// Ask picomatch to compile — if it throws, surface the reason.
|
|
103
|
+
try {
|
|
104
|
+
picomatch(glob);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
108
|
+
throw new Error(`${evalFile}: __pathgradeMeta.${field} entry "${glob}" has invalid glob syntax: ${msg}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function countUnbalanced(s, open, close) {
|
|
112
|
+
let depth = 0;
|
|
113
|
+
let escaped = false;
|
|
114
|
+
for (const ch of s) {
|
|
115
|
+
if (escaped) {
|
|
116
|
+
escaped = false;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (ch === '\\') {
|
|
120
|
+
escaped = true;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (ch === open)
|
|
124
|
+
depth++;
|
|
125
|
+
else if (ch === close)
|
|
126
|
+
depth--;
|
|
127
|
+
if (depth < 0)
|
|
128
|
+
return depth;
|
|
129
|
+
}
|
|
130
|
+
return depth;
|
|
131
|
+
}
|
|
132
|
+
function propertyKeyName(name) {
|
|
133
|
+
if (ts.isIdentifier(name))
|
|
134
|
+
return name.text;
|
|
135
|
+
if (ts.isStringLiteral(name))
|
|
136
|
+
return name.text;
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
function extractStringArray(expr, evalFile, field) {
|
|
140
|
+
if (!ts.isArrayLiteralExpression(expr)) {
|
|
141
|
+
throw new Error(`${evalFile}: __pathgradeMeta.${field} must be an array of strings.`);
|
|
142
|
+
}
|
|
143
|
+
const out = [];
|
|
144
|
+
for (const el of expr.elements) {
|
|
145
|
+
if (!ts.isStringLiteral(el) && !ts.isNoSubstitutionTemplateLiteral(el)) {
|
|
146
|
+
throw new Error(`${evalFile}: __pathgradeMeta.${field} entries must be string literals.`);
|
|
147
|
+
}
|
|
148
|
+
out.push(el.text);
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
function extractBoolean(expr, evalFile) {
|
|
153
|
+
if (expr.kind === ts.SyntaxKind.TrueKeyword)
|
|
154
|
+
return true;
|
|
155
|
+
if (expr.kind === ts.SyntaxKind.FalseKeyword)
|
|
156
|
+
return false;
|
|
157
|
+
throw new Error(`${evalFile}: __pathgradeMeta.alwaysRun must be a boolean literal.`);
|
|
158
|
+
}
|