jev-affected 0.1.0 → 0.3.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/CHANGELOG.md +16 -0
- package/EVALUATION.md +6 -6
- package/README.md +30 -15
- package/SECURITY.md +1 -1
- package/dist/cache.d.ts +8 -0
- package/dist/cache.js +35 -0
- package/dist/cli.js +42 -7
- package/dist/config.d.ts +2 -0
- package/dist/config.js +13 -0
- package/dist/git.d.ts +6 -2
- package/dist/git.js +89 -13
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/planner.d.ts +2 -5
- package/dist/planner.js +5 -3
- package/dist/provider.d.ts +1 -0
- package/dist/provider.js +1 -0
- package/package.json +1 -3
- package/skills/jev-affected/SKILL.md +14 -10
- package/.env.example +0 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
- Add a repository-owned `jev-affected.yml` for dogfooding this project.
|
|
6
|
+
- Analyze committed, working-tree, or staged changes with explicit input modes.
|
|
7
|
+
- Detect pull-request base branches across GitHub Actions, GitLab CI, Buildkite,
|
|
8
|
+
and mapped CircleCI pipeline values.
|
|
9
|
+
- Inspect and safely clear the Git-local semantic decision cache.
|
|
10
|
+
- Avoid treating source text that mentions Git submodule markers as an actual
|
|
11
|
+
submodule diff.
|
|
12
|
+
|
|
13
|
+
## 0.2.0
|
|
14
|
+
|
|
15
|
+
- Read API credentials from the process environment only.
|
|
16
|
+
- Reject literal API keys in YAML configuration.
|
|
17
|
+
- Accept `TYPESAFEAI_API_KEY` as a compatibility fallback.
|
|
18
|
+
|
|
3
19
|
## 0.1.0
|
|
4
20
|
|
|
5
21
|
- YAML semantic task conditions, deterministic globs and protected tasks.
|
package/EVALUATION.md
CHANGED
|
@@ -34,10 +34,10 @@ Run on 2026-09-21 with `jev-1.13.0` and `skipBelow: 0.10`:
|
|
|
34
34
|
| False skips | 0 |
|
|
35
35
|
| Invalid cases | 0 |
|
|
36
36
|
| Cases with reduction | 6 |
|
|
37
|
-
| Task reduction |
|
|
38
|
-
| Unnecessary-run rate |
|
|
37
|
+
| Task reduction | 42.86% |
|
|
38
|
+
| Unnecessary-run rate | 50.00% |
|
|
39
39
|
| API calls | 7 |
|
|
40
|
-
| Total latency | 2,
|
|
40
|
+
| Total latency | 2,126 ms |
|
|
41
41
|
| Input tokens | 2,882 |
|
|
42
42
|
| Output tokens | 476 |
|
|
43
43
|
|
|
@@ -47,7 +47,7 @@ Selected tasks by fixture:
|
|
|
47
47
|
| --- | ---: |
|
|
48
48
|
| API field | 3 / 4 |
|
|
49
49
|
| Authentication | 4 / 4 |
|
|
50
|
-
| Comment |
|
|
50
|
+
| Comment | 2 / 4 |
|
|
51
51
|
| Database schema | 3 / 4 |
|
|
52
52
|
| Documentation | 0 / 4 |
|
|
53
53
|
| Logging | 1 / 4 |
|
|
@@ -65,8 +65,8 @@ pnpm eval:live
|
|
|
65
65
|
|
|
66
66
|
`pnpm eval` uses deterministic synthetic answers and verifies evaluation and
|
|
67
67
|
decision plumbing without making a network request. `pnpm eval:live` reads
|
|
68
|
-
`TYPESAFE_API_KEY` from the process environment
|
|
69
|
-
|
|
68
|
+
`TYPESAFE_API_KEY` from the process environment and sends the fixture inputs to
|
|
69
|
+
Jev. Dotenv files are not loaded.
|
|
70
70
|
|
|
71
71
|
## Limits
|
|
72
72
|
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
<p align="center"><strong>Semantic task routing for software development.</strong></p>
|
|
4
4
|
<p align="center">Run tasks based on what changed, not where it changed.</p>
|
|
5
|
-
<p align="center">v0.
|
|
5
|
+
<p align="center">v0.3.0 Public Beta · Node.js 20+ · TypeScript · MIT · Jev-powered</p>
|
|
6
6
|
|
|
7
7
|

|
|
8
8
|
|
|
@@ -30,13 +30,7 @@ npm install -D jev-affected
|
|
|
30
30
|
npx jev-affected init
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
```dotenv
|
|
36
|
-
TYPESAFE_API_KEY=your_api_key_here
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
`.env` is ignored by this repository's generated `.gitignore`. Never put the key in `jev-affected.yml` or commit it. An existing process environment value takes precedence over `.env`. You can also set it for the current shell:
|
|
33
|
+
Set the API key in the process environment. The CLI does not read dotenv files or accept API keys in `jev-affected.yml`.
|
|
40
34
|
|
|
41
35
|
```powershell
|
|
42
36
|
$env:TYPESAFE_API_KEY = "your_api_key_here"
|
|
@@ -46,6 +40,8 @@ $env:TYPESAFE_API_KEY = "your_api_key_here"
|
|
|
46
40
|
export TYPESAFE_API_KEY="your_api_key_here"
|
|
47
41
|
```
|
|
48
42
|
|
|
43
|
+
For persistent local use, configure the variable in your shell profile or operating-system credential environment. Open a new terminal after changing persistent environment variables. `TYPESAFEAI_API_KEY` is accepted as a compatibility fallback.
|
|
44
|
+
|
|
49
45
|
Configure your own trusted commands, commit your changes on a feature branch, then:
|
|
50
46
|
|
|
51
47
|
```sh
|
|
@@ -54,7 +50,14 @@ npx jev-affected plan --base main
|
|
|
54
50
|
npx jev-affected run --base main
|
|
55
51
|
```
|
|
56
52
|
|
|
57
|
-
`plan` performs analysis but never executes task commands. `run` executes configured commands.
|
|
53
|
+
`plan` performs analysis but never executes task commands. `run` executes configured commands. The default mode compares committed changes. Use `--working-tree` to include branch commits, staged changes, unstaged changes and untracked files, or `--staged` to analyze only the Git index.
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
npx jev-affected plan --working-tree --base main
|
|
57
|
+
npx jev-affected plan --staged
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`--working-tree` accepts `--base` but not `--head`. `--staged` compares the index with `HEAD` and cannot be combined with `--base` or `--head`.
|
|
58
61
|
|
|
59
62
|
## How it works
|
|
60
63
|
|
|
@@ -125,12 +128,13 @@ Low model probabilities are estimates, not a guarantee that a task is unnecessar
|
|
|
125
128
|
| `inspect` | Preview sanitized analysis inputs locally; no request |
|
|
126
129
|
| `doctor` | Check Node, config, Git base, API key and models endpoint |
|
|
127
130
|
| `eval [--live]` | Evaluate fixture decisions and false-skip rate |
|
|
131
|
+
| `cache [status\|clear]` | Inspect or remove the Git-local decision cache |
|
|
128
132
|
|
|
129
|
-
Common flags: `--base`, `--head`, `--config`, `--no-cache`, `--json`, `--help`, `--version`. JSON task output includes a stable `version: 1`. During `run --json`, child output goes to stderr, keeping stdout parseable.
|
|
133
|
+
Common flags: `--base`, `--head`, `--working-tree`, `--staged`, `--config`, `--no-cache`, `--json`, `--help`, `--version`. JSON task output includes a stable `version: 1`. During `run --json`, child output goes to stderr, keeping stdout parseable.
|
|
130
134
|
|
|
131
135
|
Exit codes: `0` success; `1` failed executed task or failed evaluation gate; `2` config/argument/base or doctor check error; `3` internal/I/O failure. API failures do not fail a normal plan or run by themselves.
|
|
132
136
|
|
|
133
|
-
Base priority: CLI → config →
|
|
137
|
+
Base priority: CLI → config → detected CI target → `origin/HEAD` → `origin/main` → `main` → `master`. The comparison starts at the merge-base with head. Explicit invalid bases fail instead of silently choosing another branch. Fetch full history in CI.
|
|
134
138
|
|
|
135
139
|
## CI
|
|
136
140
|
|
|
@@ -145,9 +149,18 @@ Base priority: CLI → config → GitHub PR base branch → `origin/main` → `m
|
|
|
145
149
|
|
|
146
150
|
Install dependencies first. Do not expose credentials to untrusted PR code. Nx, Turbo and other runners can be invoked as configured commands; no plugins are required.
|
|
147
151
|
|
|
152
|
+
Target branches are detected from GitHub Actions, GitLab CI and Buildkite variables. CircleCI exposes the PR base as a pipeline value rather than a legacy job environment variable; map it to `CIRCLE_PR_BASE_BRANCH` in the job when using automatic detection. `--base` and `base` in configuration remain the explicit overrides.
|
|
153
|
+
|
|
154
|
+
| CI | Base input |
|
|
155
|
+
| --- | --- |
|
|
156
|
+
| GitHub Actions | `GITHUB_BASE_REF` |
|
|
157
|
+
| GitLab CI | merge-request diff base SHA, target branch, then default branch |
|
|
158
|
+
| Buildkite | `BUILDKITE_PULL_REQUEST_BASE_BRANCH` |
|
|
159
|
+
| CircleCI | `CIRCLE_PR_BASE_BRANCH` supplied from the PR base pipeline value |
|
|
160
|
+
|
|
148
161
|
## Agents
|
|
149
162
|
|
|
150
|
-
|
|
163
|
+
Run `jev-affected plan --json --working-tree` while editing, or use the default committed mode after committing. Execute all tasks whose `decision` is `run`, or use `jev-affected run` with the same mode. `inspect` lets you review what could leave the repository first.
|
|
151
164
|
|
|
152
165
|
The package includes a reusable agent skill at [`skills/jev-affected`](skills/jev-affected). Point a compatible coding agent at that directory, or copy it into the agent's skills directory, to give it the safe planning and execution workflow.
|
|
153
166
|
|
|
@@ -163,6 +176,8 @@ const plan = await createPlan({ config, base: 'main' });
|
|
|
163
176
|
|
|
164
177
|
Automatic caches store probabilities and model metadata in `.git/jev-affected/cache` (including Git worktree support). Keys include state, questions, configuration and pinned actual model version. Corrupt cache entries fall back to RUN. **Moving aliases such as `jev-latest` bypass caching**, because an alias cannot safely establish its current actual version without another request. Pin an actual `jev-x.y.z` version to enable reuse. Neither patches nor API keys are stored in cache payloads. Cache writes are the only local side effect of `plan`; `--no-cache` disables them.
|
|
165
178
|
|
|
179
|
+
Use `jev-affected cache status` to inspect entry count and size, and `jev-affected cache clear` to remove only the cache directory resolved inside Git metadata. Both commands support `--json` and do not require a project configuration file.
|
|
180
|
+
|
|
166
181
|
## Evaluation
|
|
167
182
|
|
|
168
183
|
```sh
|
|
@@ -176,7 +191,7 @@ See [EVALUATION.md](EVALUATION.md) for the methodology, release gates, latest li
|
|
|
176
191
|
|
|
177
192
|
## Privacy
|
|
178
193
|
|
|
179
|
-
No backend and no telemetry. Data is read locally; semantic inputs go directly to TypeSafe through its SDK. By default, `.env*`, PEM/key files, and names containing `credentials`, `secret` or `token` are withheld. Custom exclusions add to these defaults. Filename exclusions cannot find every secret embedded in arbitrary source files: inspect inputs and use appropriate repository practices. SDK logging is explicitly disabled.
|
|
194
|
+
No backend and no telemetry. Data is read locally; semantic inputs go directly to TypeSafe through its SDK. API keys are read from the process environment only. By default, `.env*`, PEM/key files, and names containing `credentials`, `secret` or `token` are withheld. Custom exclusions add to these defaults. Filename exclusions cannot find every secret embedded in arbitrary source files: inspect inputs and use appropriate repository practices. SDK logging is explicitly disabled.
|
|
180
195
|
|
|
181
196
|
`TYPESAFE_BASE_URL` is honored by the official SDK; inspect this environment setting before use. Review [TypeSafe's data-processing terms](https://typesafe.ai/legal/mca) for your organization.
|
|
182
197
|
|
|
@@ -188,11 +203,11 @@ No backend and no telemetry. Data is read locally; semantic inputs go directly t
|
|
|
188
203
|
|
|
189
204
|
**What if Jev is down?** Candidate tasks run. Deterministic rules and protected tasks still apply.
|
|
190
205
|
|
|
191
|
-
**Are uncommitted files included?**
|
|
206
|
+
**Are uncommitted files included?** Only with `--working-tree` or `--staged`. Default mode compares commits so explicit refs remain reproducible.
|
|
192
207
|
|
|
193
208
|
## Roadmap
|
|
194
209
|
|
|
195
|
-
|
|
210
|
+
The current release focuses on inspectable semantic dependencies and safe plans for committed, working-tree and staged changes. Later candidates include historical evaluations, watch mode, task groups and Nx/Turbo adapters. No GUI, SaaS, MCP server or autonomous command generation is included.
|
|
196
211
|
|
|
197
212
|
## Contributing and license
|
|
198
213
|
|
package/SECURITY.md
CHANGED
|
@@ -10,7 +10,7 @@ Do not post secrets or exploit details in a public issue. Once the public reposi
|
|
|
10
10
|
|
|
11
11
|
## Secret handling
|
|
12
12
|
|
|
13
|
-
Set TYPESAFE_API_KEY through
|
|
13
|
+
Set TYPESAFE_API_KEY through the process environment or CI secret store. The CLI does not load dotenv files and rejects literal API keys in its YAML configuration. The application disables SDK logging and does not emit provider error bodies. Exclusion globs are additive; they cannot detect every embedded secret. Inspect your inputs before enabling remote analysis.
|
|
14
14
|
|
|
15
15
|
## External API calls
|
|
16
16
|
|
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface CacheInfo {
|
|
2
|
+
path: string;
|
|
3
|
+
entries: number;
|
|
4
|
+
bytes: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function cacheDirectory(cwd?: string): Promise<string>;
|
|
7
|
+
export declare function inspectCache(cwd?: string): Promise<CacheInfo>;
|
|
8
|
+
export declare function clearCache(cwd?: string): Promise<CacheInfo>;
|
package/dist/cache.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { readdir, rm, stat } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
3
|
+
import { ConfigError } from "./config.js";
|
|
4
|
+
import { git } from "./git.js";
|
|
5
|
+
export async function cacheDirectory(cwd = process.cwd()) {
|
|
6
|
+
const common = resolve(cwd, (await git(cwd, "rev-parse", "--git-common-dir")).trim());
|
|
7
|
+
const directory = resolve(cwd, (await git(cwd, "rev-parse", "--git-path", "jev-affected/cache")).trim());
|
|
8
|
+
const fromCommon = relative(common, directory);
|
|
9
|
+
if (!fromCommon || fromCommon.startsWith("..") || isAbsolute(fromCommon))
|
|
10
|
+
throw new ConfigError("Cannot resolve a safe Git cache directory.");
|
|
11
|
+
return directory;
|
|
12
|
+
}
|
|
13
|
+
export async function inspectCache(cwd = process.cwd()) {
|
|
14
|
+
const path = await cacheDirectory(cwd);
|
|
15
|
+
let names;
|
|
16
|
+
try {
|
|
17
|
+
names = (await readdir(path)).filter((name) => name.endsWith(".json"));
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
if (error.code === "ENOENT")
|
|
21
|
+
return { path, entries: 0, bytes: 0 };
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
const sizes = await Promise.all(names.map(async (name) => (await stat(resolve(path, name))).size));
|
|
25
|
+
return {
|
|
26
|
+
path,
|
|
27
|
+
entries: names.length,
|
|
28
|
+
bytes: sizes.reduce((total, size) => total + size, 0),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export async function clearCache(cwd = process.cwd()) {
|
|
32
|
+
const info = await inspectCache(cwd);
|
|
33
|
+
await rm(info.path, { recursive: true, force: true });
|
|
34
|
+
return info;
|
|
35
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -4,13 +4,12 @@ import { resolve } from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
6
6
|
import { TypeSafeClient } from "@typesafe-ai/sdk";
|
|
7
|
-
import {
|
|
8
|
-
import { ConfigError, loadConfig, template } from "./config.js";
|
|
7
|
+
import { clearCache, inspectCache } from "./cache.js";
|
|
8
|
+
import { ConfigError, loadConfig, resolveApiKey, template } from "./config.js";
|
|
9
9
|
import { evaluate } from "./eval.js";
|
|
10
10
|
import { executePlan } from "./executor.js";
|
|
11
11
|
import { collectChanges, matches } from "./git.js";
|
|
12
12
|
import { createPlan } from "./planner.js";
|
|
13
|
-
loadEnvFile({ quiet: true });
|
|
14
13
|
function render(plan) {
|
|
15
14
|
return [
|
|
16
15
|
`jev-affected\n\nBase ${plan.base}\nHead ${plan.head}\nModel ${plan.model ?? "not analyzed"}`,
|
|
@@ -36,6 +35,8 @@ async function main() {
|
|
|
36
35
|
help: { type: "boolean", short: "h" },
|
|
37
36
|
version: { type: "boolean", short: "v" },
|
|
38
37
|
"no-cache": { type: "boolean" },
|
|
38
|
+
"working-tree": { type: "boolean" },
|
|
39
|
+
staged: { type: "boolean" },
|
|
39
40
|
fixtures: { type: "string" },
|
|
40
41
|
live: { type: "boolean" },
|
|
41
42
|
},
|
|
@@ -54,13 +55,15 @@ async function main() {
|
|
|
54
55
|
console.log(`jev-affected — Semantic task routing, powered by Jev.
|
|
55
56
|
|
|
56
57
|
Usage: jev-affected <command> [options]
|
|
57
|
-
Commands: init, plan, run, why <task>, inspect, doctor, eval
|
|
58
|
+
Commands: init, plan, run, why <task>, inspect, doctor, eval, cache [status|clear]
|
|
58
59
|
Options:
|
|
59
60
|
--base <ref> Compare from merge-base(ref, head)
|
|
60
61
|
--head <ref> Head commit (default HEAD; committed changes only)
|
|
61
62
|
--config <file> YAML configuration
|
|
62
63
|
--json Machine-readable output
|
|
63
64
|
--no-cache Bypass the local decision cache
|
|
65
|
+
--working-tree Include committed, staged, unstaged and untracked changes
|
|
66
|
+
--staged Analyze only changes staged against HEAD
|
|
64
67
|
--parallel Execute selected commands concurrently
|
|
65
68
|
--concurrency <n> Maximum parallel commands
|
|
66
69
|
--fixtures <dir> Evaluation fixtures (default evals/fixtures)
|
|
@@ -70,11 +73,39 @@ Options:
|
|
|
70
73
|
inspect never sends requests. plan never executes task commands.`);
|
|
71
74
|
return;
|
|
72
75
|
}
|
|
73
|
-
if (![
|
|
76
|
+
if (![
|
|
77
|
+
"init",
|
|
78
|
+
"plan",
|
|
79
|
+
"run",
|
|
80
|
+
"why",
|
|
81
|
+
"inspect",
|
|
82
|
+
"doctor",
|
|
83
|
+
"eval",
|
|
84
|
+
"cache",
|
|
85
|
+
].includes(command))
|
|
74
86
|
throw new ConfigError("Unknown command. See --help.");
|
|
75
|
-
if (positionals.length > (
|
|
87
|
+
if (positionals.length > (["why", "cache"].includes(command) ? 2 : 1))
|
|
76
88
|
throw new ConfigError("Unexpected positional arguments.");
|
|
89
|
+
if (v["working-tree"] && v.staged)
|
|
90
|
+
throw new ConfigError("Use --working-tree or --staged, not both.");
|
|
91
|
+
if (v["working-tree"] && v.head)
|
|
92
|
+
throw new ConfigError("--working-tree cannot be combined with --head.");
|
|
93
|
+
if (v.staged && (v.base || v.head))
|
|
94
|
+
throw new ConfigError("--staged cannot be combined with --base or --head.");
|
|
77
95
|
const emit = (x) => console.log(JSON.stringify(x, null, 2));
|
|
96
|
+
if (command === "cache") {
|
|
97
|
+
const action = positionals[1] ?? "status";
|
|
98
|
+
if (!["status", "clear"].includes(action))
|
|
99
|
+
throw new ConfigError("Use cache status or cache clear.");
|
|
100
|
+
const info = action === "clear" ? await clearCache() : await inspectCache();
|
|
101
|
+
if (v.json)
|
|
102
|
+
emit({ action, ...info });
|
|
103
|
+
else if (action === "clear")
|
|
104
|
+
console.log(`Cleared ${info.entries} cache entries (${info.bytes} bytes).`);
|
|
105
|
+
else
|
|
106
|
+
console.log(`Cache: ${info.entries} entries, ${info.bytes} bytes\n${info.path}`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
78
109
|
if (command === "init") {
|
|
79
110
|
try {
|
|
80
111
|
await writeFile(String(v.config ?? "jev-affected.yml"), template, {
|
|
@@ -105,6 +136,8 @@ inspect never sends requests. plan never executes task commands.`);
|
|
|
105
136
|
base: typeof v.base === "string" ? v.base : undefined,
|
|
106
137
|
head: typeof v.head === "string" ? v.head : undefined,
|
|
107
138
|
cache: !v["no-cache"],
|
|
139
|
+
workingTree: !!v["working-tree"],
|
|
140
|
+
staged: !!v.staged,
|
|
108
141
|
};
|
|
109
142
|
if (command === "why" &&
|
|
110
143
|
(!positionals[1] || !Object.hasOwn(config.tasks, positionals[1])))
|
|
@@ -140,11 +173,12 @@ inspect never sends requests. plan never executes task commands.`);
|
|
|
140
173
|
return;
|
|
141
174
|
}
|
|
142
175
|
if (command === "doctor") {
|
|
176
|
+
const apiKey = resolveApiKey();
|
|
143
177
|
const checks = {
|
|
144
178
|
node: Number(process.versions.node.split(".")[0]) >= 20,
|
|
145
179
|
config: true,
|
|
146
180
|
tasks: Object.keys(config.tasks).length.toString(),
|
|
147
|
-
apiKey: !!
|
|
181
|
+
apiKey: !!apiKey,
|
|
148
182
|
};
|
|
149
183
|
try {
|
|
150
184
|
await collectChanges(config, opts);
|
|
@@ -156,6 +190,7 @@ inspect never sends requests. plan never executes task commands.`);
|
|
|
156
190
|
if (checks.apiKey) {
|
|
157
191
|
try {
|
|
158
192
|
await new TypeSafeClient({
|
|
193
|
+
apiKey,
|
|
159
194
|
logLevel: "off",
|
|
160
195
|
timeout: config.analysis.timeoutMs,
|
|
161
196
|
retry: { maxRetries: 0 },
|
package/dist/config.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export declare class ConfigError extends Error {
|
|
3
3
|
}
|
|
4
|
+
export declare const API_KEY_VARS: readonly ["TYPESAFE_API_KEY", "TYPESAFEAI_API_KEY"];
|
|
4
5
|
export declare const secretPatterns: string[];
|
|
5
6
|
export declare const configSchema: z.ZodObject<{
|
|
6
7
|
version: z.ZodLiteral<1>;
|
|
@@ -37,5 +38,6 @@ export declare const configSchema: z.ZodObject<{
|
|
|
37
38
|
}, z.core.$strict>;
|
|
38
39
|
export type Config = z.infer<typeof configSchema>;
|
|
39
40
|
export declare function parseConfig(value: unknown): Config;
|
|
41
|
+
export declare function resolveApiKey(env?: NodeJS.ProcessEnv): string | undefined;
|
|
40
42
|
export declare function loadConfig(path?: string): Promise<Config>;
|
|
41
43
|
export declare const template = "version: 1\nmodel: jev-latest\ndefaults:\n skipBelow: 0.10\ntasks:\n unit:\n command: npm test\n when: Could this change alter runtime application behavior?\n typecheck:\n command: npm run typecheck\n always: true\n";
|
package/dist/config.js
CHANGED
|
@@ -3,6 +3,7 @@ import { parse } from "yaml";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
export class ConfigError extends Error {
|
|
5
5
|
}
|
|
6
|
+
export const API_KEY_VARS = ["TYPESAFE_API_KEY", "TYPESAFEAI_API_KEY"];
|
|
6
7
|
const probability = z.number().min(0).max(1);
|
|
7
8
|
const limits = z
|
|
8
9
|
.object({
|
|
@@ -68,11 +69,23 @@ export const configSchema = z
|
|
|
68
69
|
})
|
|
69
70
|
.strict();
|
|
70
71
|
export function parseConfig(value) {
|
|
72
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
73
|
+
const source = value;
|
|
74
|
+
if (Object.hasOwn(source, "apiKey") || Object.hasOwn(source, "api_key"))
|
|
75
|
+
throw new ConfigError("API keys are not supported in jev-affected.yml. Set TYPESAFE_API_KEY in the process environment or CI secret store.");
|
|
76
|
+
}
|
|
71
77
|
const r = configSchema.safeParse(value);
|
|
72
78
|
if (!r.success)
|
|
73
79
|
throw new ConfigError(r.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("\n"));
|
|
74
80
|
return r.data;
|
|
75
81
|
}
|
|
82
|
+
export function resolveApiKey(env = process.env) {
|
|
83
|
+
for (const name of API_KEY_VARS) {
|
|
84
|
+
const value = env[name]?.trim();
|
|
85
|
+
if (value)
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
76
89
|
export async function loadConfig(path = "jev-affected.yml") {
|
|
77
90
|
try {
|
|
78
91
|
return parseConfig(parse(await readFile(path, "utf8")));
|
package/dist/git.d.ts
CHANGED
|
@@ -14,8 +14,12 @@ export interface ChangeState {
|
|
|
14
14
|
incomplete: boolean;
|
|
15
15
|
warnings: string[];
|
|
16
16
|
}
|
|
17
|
-
export
|
|
17
|
+
export interface ChangeOptions {
|
|
18
18
|
cwd?: string;
|
|
19
19
|
base?: string;
|
|
20
20
|
head?: string;
|
|
21
|
-
|
|
21
|
+
workingTree?: boolean;
|
|
22
|
+
staged?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare function detectCiBaseCandidates(env?: NodeJS.ProcessEnv): string[];
|
|
25
|
+
export declare function collectChanges(config: Config, options?: ChangeOptions): Promise<ChangeState>;
|
package/dist/git.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
3
|
+
import { resolve as resolvePath } from "node:path";
|
|
2
4
|
import { promisify } from "node:util";
|
|
3
5
|
import picomatch from "picomatch";
|
|
4
6
|
import { ConfigError, secretPatterns } from "./config.js";
|
|
@@ -12,38 +14,88 @@ export async function git(cwd, ...args) {
|
|
|
12
14
|
})).stdout;
|
|
13
15
|
}
|
|
14
16
|
export const matches = (path, patterns) => patterns.length > 0 && picomatch(patterns, { dot: true })(path);
|
|
17
|
+
const branchRefs = (value) => {
|
|
18
|
+
const branch = value?.trim().replace(/^refs\/heads\//, "");
|
|
19
|
+
if (!branch || branch === "false")
|
|
20
|
+
return [];
|
|
21
|
+
if (branch.startsWith("origin/") || /^[0-9a-f]{7,40}$/.test(branch))
|
|
22
|
+
return [branch];
|
|
23
|
+
return [`origin/${branch}`, branch];
|
|
24
|
+
};
|
|
25
|
+
export function detectCiBaseCandidates(env = process.env) {
|
|
26
|
+
const refs = [
|
|
27
|
+
...branchRefs(env.GITHUB_BASE_REF),
|
|
28
|
+
...(env.GITLAB_CI === "true"
|
|
29
|
+
? [
|
|
30
|
+
...branchRefs(env.CI_MERGE_REQUEST_DIFF_BASE_SHA),
|
|
31
|
+
...branchRefs(env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME),
|
|
32
|
+
...branchRefs(env.CI_DEFAULT_BRANCH),
|
|
33
|
+
]
|
|
34
|
+
: []),
|
|
35
|
+
...(env.BUILDKITE === "true"
|
|
36
|
+
? branchRefs(env.BUILDKITE_PULL_REQUEST_BASE_BRANCH)
|
|
37
|
+
: []),
|
|
38
|
+
...(env.CIRCLECI === "true" ? branchRefs(env.CIRCLE_PR_BASE_BRANCH) : []),
|
|
39
|
+
];
|
|
40
|
+
return [...new Set(refs)];
|
|
41
|
+
}
|
|
42
|
+
const hasUnsupportedPatch = (patch) => /^Binary files .+ differ$/m.test(patch) ||
|
|
43
|
+
/^[+-]Subproject commit [0-9a-f]{7,}(?:-dirty)?$/m.test(patch);
|
|
44
|
+
async function untrackedPatch(cwd, path) {
|
|
45
|
+
try {
|
|
46
|
+
return await git(cwd, "diff", "--no-index", "--no-ext-diff", "--no-textconv", "--unified=3", "--", "/dev/null", path);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
const result = error;
|
|
50
|
+
if (Number(result.code) === 1 && typeof result.stdout === "string")
|
|
51
|
+
return result.stdout;
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
15
55
|
export async function collectChanges(config, options = {}) {
|
|
16
56
|
const cwd = options.cwd ?? process.cwd();
|
|
57
|
+
if (options.workingTree && options.staged)
|
|
58
|
+
throw new ConfigError("Use --working-tree or --staged, not both.");
|
|
59
|
+
if (options.workingTree && options.head)
|
|
60
|
+
throw new ConfigError("--working-tree cannot be combined with --head.");
|
|
61
|
+
if (options.staged && (options.base || options.head))
|
|
62
|
+
throw new ConfigError("--staged cannot be combined with --base or --head.");
|
|
17
63
|
const resolve = async (ref) => (await git(cwd, "rev-parse", "--verify", "--end-of-options", `${ref}^{commit}`)).trim();
|
|
18
|
-
let
|
|
64
|
+
let headCommit;
|
|
19
65
|
try {
|
|
20
|
-
|
|
66
|
+
headCommit = await resolve(options.head ?? "HEAD");
|
|
21
67
|
}
|
|
22
68
|
catch {
|
|
23
69
|
throw new ConfigError("Cannot resolve head commit.");
|
|
24
70
|
}
|
|
25
|
-
const explicit = options.base ?? config.base;
|
|
71
|
+
const explicit = options.staged ? "HEAD" : (options.base ?? config.base);
|
|
26
72
|
const candidates = explicit
|
|
27
73
|
? [explicit]
|
|
28
74
|
: [
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
: undefined,
|
|
75
|
+
...detectCiBaseCandidates(),
|
|
76
|
+
"origin/HEAD",
|
|
32
77
|
"origin/main",
|
|
33
78
|
"main",
|
|
34
79
|
"master",
|
|
35
|
-
]
|
|
80
|
+
];
|
|
36
81
|
let base;
|
|
37
82
|
for (const ref of candidates) {
|
|
38
83
|
try {
|
|
39
|
-
base =
|
|
84
|
+
base = options.staged
|
|
85
|
+
? await resolve("HEAD")
|
|
86
|
+
: (await git(cwd, "merge-base", await resolve(ref), headCommit)).trim();
|
|
40
87
|
break;
|
|
41
88
|
}
|
|
42
89
|
catch { }
|
|
43
90
|
}
|
|
44
91
|
if (!base)
|
|
45
92
|
throw new ConfigError("Cannot resolve Git base. Fetch the base branch or pass --base.");
|
|
46
|
-
const
|
|
93
|
+
const comparison = options.staged
|
|
94
|
+
? ["--cached", base]
|
|
95
|
+
: options.workingTree
|
|
96
|
+
? [base]
|
|
97
|
+
: [base, headCommit];
|
|
98
|
+
const parts = (await git(cwd, "diff", "--name-status", "-z", "--find-renames", ...comparison, "--")).split("\0");
|
|
47
99
|
const files = [];
|
|
48
100
|
for (let i = 0; i < parts.length && parts[i];) {
|
|
49
101
|
const status = parts[i++];
|
|
@@ -59,9 +111,23 @@ export async function collectChanges(config, options = {}) {
|
|
|
59
111
|
else
|
|
60
112
|
files.push({ status, path });
|
|
61
113
|
}
|
|
114
|
+
if (options.workingTree) {
|
|
115
|
+
const tracked = new Set(files.flatMap((file) => [
|
|
116
|
+
file.path,
|
|
117
|
+
...(file.oldPath ? [file.oldPath] : []),
|
|
118
|
+
]));
|
|
119
|
+
for (const path of (await git(cwd, "ls-files", "--others", "--exclude-standard", "-z", "--")).split("\0")) {
|
|
120
|
+
if (path && !tracked.has(path))
|
|
121
|
+
files.push({ status: "A", path });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
62
124
|
const state = {
|
|
63
125
|
base,
|
|
64
|
-
head
|
|
126
|
+
head: options.staged
|
|
127
|
+
? "INDEX"
|
|
128
|
+
: options.workingTree
|
|
129
|
+
? "WORKTREE"
|
|
130
|
+
: headCommit,
|
|
65
131
|
files: [],
|
|
66
132
|
diff: "",
|
|
67
133
|
incomplete: false,
|
|
@@ -78,9 +144,19 @@ export async function collectChanges(config, options = {}) {
|
|
|
78
144
|
continue;
|
|
79
145
|
state.files.push(file);
|
|
80
146
|
try {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
147
|
+
if (options.workingTree && file.status === "A") {
|
|
148
|
+
const size = (await stat(resolvePath(cwd, file.path))).size;
|
|
149
|
+
if (Buffer.byteLength(state.diff) + size >
|
|
150
|
+
config.analysis.maxDiffBytes) {
|
|
151
|
+
state.incomplete = true;
|
|
152
|
+
state.warnings.push("Diff exceeds analysis limit; running all tasks.");
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const patch = options.workingTree && file.status === "A"
|
|
157
|
+
? await untrackedPatch(cwd, file.path)
|
|
158
|
+
: await git(cwd, "diff", "--no-ext-diff", "--no-textconv", "--find-renames", "--unified=3", ...(options.staged ? ["--cached", base] : comparison), "--", ...paths);
|
|
159
|
+
if (hasUnsupportedPatch(patch)) {
|
|
84
160
|
state.incomplete = true;
|
|
85
161
|
state.warnings.push("Binary or submodule change; running all tasks.");
|
|
86
162
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export { type
|
|
1
|
+
export { type CacheInfo, cacheDirectory, clearCache, inspectCache, } from "./cache.js";
|
|
2
|
+
export { API_KEY_VARS, type Config, ConfigError, loadConfig, parseConfig, resolveApiKey, } from "./config.js";
|
|
2
3
|
export { executePlan } from "./executor.js";
|
|
3
|
-
export { type ChangeState, collectChanges } from "./git.js";
|
|
4
|
+
export { type ChangedFile, type ChangeOptions, type ChangeState, collectChanges, detectCiBaseCandidates, } from "./git.js";
|
|
4
5
|
export { createPlan, type Plan, type TaskDecision } from "./planner.js";
|
|
5
6
|
export { type AnalysisInput, type AnalysisResult, type DecisionProvider, JevProvider, } from "./provider.js";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { cacheDirectory, clearCache, inspectCache, } from "./cache.js";
|
|
2
|
+
export { API_KEY_VARS, ConfigError, loadConfig, parseConfig, resolveApiKey, } from "./config.js";
|
|
2
3
|
export { executePlan } from "./executor.js";
|
|
3
|
-
export { collectChanges } from "./git.js";
|
|
4
|
+
export { collectChanges, detectCiBaseCandidates, } from "./git.js";
|
|
4
5
|
export { createPlan } from "./planner.js";
|
|
5
6
|
export { JevProvider, } from "./provider.js";
|
package/dist/planner.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Config } from "./config.js";
|
|
2
|
-
import { type ChangeState } from "./git.js";
|
|
2
|
+
import { type ChangeOptions, type ChangeState } from "./git.js";
|
|
3
3
|
import { type AnalysisResult, type DecisionProvider } from "./provider.js";
|
|
4
4
|
export interface TaskDecision {
|
|
5
5
|
id: string;
|
|
@@ -28,10 +28,7 @@ export interface Plan {
|
|
|
28
28
|
}
|
|
29
29
|
export declare function createPlan(input: {
|
|
30
30
|
config: Config;
|
|
31
|
-
cwd?: string;
|
|
32
|
-
base?: string;
|
|
33
|
-
head?: string;
|
|
34
31
|
state?: ChangeState;
|
|
35
32
|
provider?: DecisionProvider;
|
|
36
33
|
cache?: boolean;
|
|
37
|
-
}): Promise<Plan>;
|
|
34
|
+
} & ChangeOptions): Promise<Plan>;
|
package/dist/planner.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { cacheDirectory } from "./cache.js";
|
|
5
|
+
import { parseConfig, resolveApiKey, secretPatterns, } from "./config.js";
|
|
6
|
+
import { collectChanges, matches, } from "./git.js";
|
|
6
7
|
import { JevProvider, } from "./provider.js";
|
|
7
8
|
export async function createPlan(input) {
|
|
8
9
|
const start = Date.now(), config = parseConfig(input.config), cwd = input.cwd ?? process.cwd();
|
|
@@ -60,7 +61,7 @@ export async function createPlan(input) {
|
|
|
60
61
|
const pinned = /^jev-\d+\.\d+\.\d+$/.test(config.model);
|
|
61
62
|
if (input.cache !== false && !input.provider && pinned) {
|
|
62
63
|
try {
|
|
63
|
-
const dir =
|
|
64
|
+
const dir = await cacheDirectory(cwd);
|
|
64
65
|
const key = createHash("sha256")
|
|
65
66
|
.update(JSON.stringify({
|
|
66
67
|
version: 1,
|
|
@@ -87,6 +88,7 @@ export async function createPlan(input) {
|
|
|
87
88
|
questions,
|
|
88
89
|
model: config.model,
|
|
89
90
|
timeoutMs: config.analysis.timeoutMs,
|
|
91
|
+
apiKey: resolveApiKey(),
|
|
90
92
|
});
|
|
91
93
|
}
|
|
92
94
|
if (!result.model || !/^jev-\d+\.\d+\.\d+$/.test(result.model))
|
package/dist/provider.d.ts
CHANGED
package/dist/provider.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jev-affected",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Semantic task routing for software development, powered by Jev.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -46,7 +46,6 @@
|
|
|
46
46
|
"SECURITY.md",
|
|
47
47
|
"CODE_OF_CONDUCT.md",
|
|
48
48
|
"EVALUATION.md",
|
|
49
|
-
".env.example",
|
|
50
49
|
"evals",
|
|
51
50
|
"skills"
|
|
52
51
|
],
|
|
@@ -65,7 +64,6 @@
|
|
|
65
64
|
},
|
|
66
65
|
"dependencies": {
|
|
67
66
|
"@typesafe-ai/sdk": "^0.6.0",
|
|
68
|
-
"dotenv": "^17.2.2",
|
|
69
67
|
"picomatch": "^4.0.7",
|
|
70
68
|
"yaml": "^2.9.1",
|
|
71
69
|
"zod": "^4.6.5"
|
|
@@ -13,12 +13,13 @@ projects and `npx jev-affected` elsewhere.
|
|
|
13
13
|
1. Confirm the current directory is a Git repository and locate
|
|
14
14
|
`jev-affected.yml`. Run `jev-affected init` only when setup is requested and no
|
|
15
15
|
configuration exists.
|
|
16
|
-
2.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
2. Choose the input mode. Use the default mode for committed branch changes,
|
|
17
|
+
`--working-tree --base <ref>` while editing, or `--staged` for the exact Git
|
|
18
|
+
index before a commit.
|
|
19
|
+
3. On first use, or when privacy matters, run `jev-affected inspect` with the
|
|
20
|
+
same mode and base flags intended for planning. This previews the sanitized
|
|
21
|
+
input locally without sending a request.
|
|
22
|
+
4. Run `jev-affected plan --json` with those same flags and use its task decisions as the
|
|
22
23
|
record of what should run. Do not invent task reasons or commands.
|
|
23
24
|
5. Run `jev-affected run --base <ref>` only when execution is requested. Commands
|
|
24
25
|
come from the repository configuration and should be treated as repository
|
|
@@ -26,15 +27,18 @@ projects and `npx jev-affected` elsewhere.
|
|
|
26
27
|
6. Report the selected and skipped task counts, actual model version, warnings,
|
|
27
28
|
fallback decisions, and any failed task commands.
|
|
28
29
|
|
|
30
|
+
Use `jev-affected cache status` to inspect the Git-local decision cache. Use
|
|
31
|
+
`jev-affected cache clear` when a fresh semantic decision is needed; it removes
|
|
32
|
+
only the cache directory resolved inside Git metadata.
|
|
33
|
+
|
|
29
34
|
Use an explicit base ref in CI and shallow clones. Fetch enough history for Git to
|
|
30
35
|
compute the merge base.
|
|
31
36
|
|
|
32
37
|
## Credentials and data
|
|
33
38
|
|
|
34
|
-
Set `TYPESAFE_API_KEY` in the process environment or
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
`.env`.
|
|
39
|
+
Set `TYPESAFE_API_KEY` in the process environment or CI secret store. Never
|
|
40
|
+
print the value or store the key in `jev-affected.yml`. The CLI does not load
|
|
41
|
+
dotenv files. `TYPESAFEAI_API_KEY` is accepted as a compatibility fallback.
|
|
38
42
|
|
|
39
43
|
`inspect` stays local. `plan`, `why`, and `run` can send the sanitized committed
|
|
40
44
|
diff and configured semantic questions to the provider. Review `inspect` output
|
package/.env.example
DELETED