jev-agent-tools 0.1.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/CHANGELOG.md +66 -0
- package/LICENSE +21 -0
- package/README.md +119 -0
- package/docs/design.md +187 -0
- package/docs/tools/jev_ask.md +92 -0
- package/docs/tools/jev_ask_files.md +80 -0
- package/docs/tools/jev_check_diff.md +77 -0
- package/docs/tools/jev_find_files.md +59 -0
- package/docs/tools/jev_locate_in_file.md +51 -0
- package/docs/tools/jev_select_tests.md +59 -0
- package/package.json +85 -0
- package/rules/jev-ask.md +4 -0
- package/src/adapters/analysis-context.ts +100 -0
- package/src/adapters/ask-files.ts +236 -0
- package/src/adapters/ask-proof.ts +202 -0
- package/src/adapters/ask-syntax.ts +463 -0
- package/src/adapters/command.ts +240 -0
- package/src/adapters/docs.ts +222 -0
- package/src/adapters/files.ts +411 -0
- package/src/adapters/find.ts +151 -0
- package/src/adapters/git-base.ts +32 -0
- package/src/adapters/git-inventory.ts +94 -0
- package/src/adapters/git.ts +525 -0
- package/src/adapters/locate-file.ts +197 -0
- package/src/adapters/output-lines.ts +50 -0
- package/src/adapters/risk-callers.ts +525 -0
- package/src/adapters/runner-version.ts +102 -0
- package/src/adapters/syntax.ts +229 -0
- package/src/adapters/test-inventory.ts +168 -0
- package/src/adapters/usage.ts +21 -0
- package/src/adapters/utf8.ts +57 -0
- package/src/constants.ts +109 -0
- package/src/core/ask-closure.ts +419 -0
- package/src/core/ask-proof.ts +32 -0
- package/src/core/ask-references.ts +249 -0
- package/src/core/asks.ts +616 -0
- package/src/core/batches.ts +83 -0
- package/src/core/command-output.ts +249 -0
- package/src/core/diff.ts +226 -0
- package/src/core/docs.ts +399 -0
- package/src/core/find.ts +157 -0
- package/src/core/git.ts +5 -0
- package/src/core/import-boundaries.ts +102 -0
- package/src/core/imports.ts +691 -0
- package/src/core/integrity.ts +64 -0
- package/src/core/lexical.ts +130 -0
- package/src/core/locate.ts +213 -0
- package/src/core/output.ts +264 -0
- package/src/core/pointer.ts +51 -0
- package/src/core/risk-callers.ts +1270 -0
- package/src/core/runner-version.ts +66 -0
- package/src/core/sections.ts +269 -0
- package/src/core/state.ts +53 -0
- package/src/core/syntax.ts +8 -0
- package/src/core/test-commands.ts +430 -0
- package/src/core/test-coverage.ts +103 -0
- package/src/core/test-discovery.ts +1695 -0
- package/src/core/test-evidence.ts +649 -0
- package/src/core/test-state.ts +99 -0
- package/src/core/truncate.ts +14 -0
- package/src/core/units.ts +531 -0
- package/src/describe.ts +26 -0
- package/src/guide.ts +42 -0
- package/src/host.ts +22 -0
- package/src/index.ts +40 -0
- package/src/jev/client.ts +505 -0
- package/src/jev/pool.ts +60 -0
- package/src/jev/types.ts +60 -0
- package/src/presets/docs.ts +85 -0
- package/src/presets/risk.ts +263 -0
- package/src/presets/spec.ts +111 -0
- package/src/presets/witnesses.ts +313 -0
- package/src/render.ts +45 -0
- package/src/result.ts +4 -0
- package/src/run-end.ts +157 -0
- package/src/runtime.ts +16 -0
- package/src/session.ts +106 -0
- package/src/texts/ask-files.ts +2 -0
- package/src/texts/ask.ts +4 -0
- package/src/texts/check-diff.ts +24 -0
- package/src/texts/configuration.ts +2 -0
- package/src/texts/find.ts +14 -0
- package/src/texts/guide.ts +16 -0
- package/src/texts/locate.ts +10 -0
- package/src/texts/run-end.ts +13 -0
- package/src/texts/select-tests.ts +3 -0
- package/src/tools/ask-files.ts +263 -0
- package/src/tools/ask-schema.ts +116 -0
- package/src/tools/ask.ts +925 -0
- package/src/tools/check-diff.ts +510 -0
- package/src/tools/docs-check.ts +399 -0
- package/src/tools/find.ts +529 -0
- package/src/tools/locate.ts +369 -0
- package/src/tools/select-tests.ts +746 -0
- package/src/tools/spec-check.ts +210 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# jev_check_diff
|
|
2
|
+
|
|
3
|
+
Review a finished diff for risk, stale existing documentation or specification drift using fixed questions.
|
|
4
|
+
|
|
5
|
+
## Use for
|
|
6
|
+
|
|
7
|
+
Review completed changes before committing or reporting completion, review a branch against a Git ref, or check whether existing documentation remains true after source changes.
|
|
8
|
+
|
|
9
|
+
## Use another tool when
|
|
10
|
+
|
|
11
|
+
Use native `git diff` execution to read the diff, [jev_select_tests](jev_select_tests.md) to choose existing tests, and [jev_ask](jev_ask.md) for your own typed judgment. Do not review a half-written change as if it were complete.
|
|
12
|
+
|
|
13
|
+
## Evidence model
|
|
14
|
+
|
|
15
|
+
Changed declarations, slices, files or hunks become before/after evidence units. Tests are supporting evidence, never units to judge. Preset questions are fixed in code; the caller chooses a check, not raw questions. Jev does not run the code. Verbatim specification text is evidence, not instructions.
|
|
16
|
+
|
|
17
|
+
## Parameters
|
|
18
|
+
|
|
19
|
+
| Field | Type / default | Meaning |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| `check` | Required `"risk"`, `"docs"` or `"spec"` | Select the preset below. |
|
|
22
|
+
| `base` | Optional nonempty Git ref, default `HEAD` | Compare the current tree, including untracked files, with this revision. |
|
|
23
|
+
| `spec_path` | Optional nonempty repository-relative string | Required for `spec`: Markdown specification with `### REQ-…` headings. |
|
|
24
|
+
| `witnesses` | Optional `"off"`, `"auto"` (default), `"on"` | Control decoy/reference evidence in the built-in risk matrix. |
|
|
25
|
+
| `dimensions` | Optional map of names to nonempty statements | Additional risk rules: positive statements true of a risky change. Built-in names are reserved. Marked uncalibrated, without severity or witnesses. |
|
|
26
|
+
| `only` | Optional nonempty array of distinct nonempty names | Restrict risk to named built-in or custom dimensions; custom names must be supplied in `dimensions`. |
|
|
27
|
+
| `max_calls` | Optional non-negative integer | Bound all requests, including local-caller checks, controls and severity; unjudged work remains unchecked. |
|
|
28
|
+
|
|
29
|
+
Custom dimensions and `only` apply to risk and are ignored by docs/spec. Unknown fields, invalid risk dimensions or unresolved refs are rejected with guidance. File paths follow repository confinement.
|
|
30
|
+
|
|
31
|
+
### risk
|
|
32
|
+
|
|
33
|
+
Ask built-in correctness, security, compatibility and reliability questions, then grade flagged risks by severity. Optional witnesses check the setup with decoy and known-positive evidence. Failed witnesses make findings unsure with raw values and a reason.
|
|
34
|
+
|
|
35
|
+
For replaced member accesses, separate local-caller checks inspect statically resolved tracked callers, including import aliases, and their imported providers. They account for coordinated caller/provider edits. Literal-name discovery cannot cover dynamic access that never names the target. Unknown providers, partial units and metaprogramming remain visibly unchecked. Local checks run once per eligible unit, not once per caller; local evidence limits do not invalidate the separate risk matrix.
|
|
36
|
+
|
|
37
|
+
### docs
|
|
38
|
+
|
|
39
|
+
Judge up to 40 existing tracked Markdown sections mentioning changed code or importing source, and identify the existing sentence made false. Collection and traversal limits are visible. This is not an exhaustive detector of missing documentation, arbitrary companion edits or every stale sentence.
|
|
40
|
+
|
|
41
|
+
### spec
|
|
42
|
+
|
|
43
|
+
Judge requirements under `### REQ-…` headings against changed behavior and identify behavior absent from the specification. `spec_path` is required and read inside the repository. Markdown tables produce an explicit interpretation limit; an arbitrary document without requirement headings is not treated as a complete specification.
|
|
44
|
+
|
|
45
|
+
## Example
|
|
46
|
+
|
|
47
|
+
Illustrative call with a fictional repository convention, not a recorded execution:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"check": "risk",
|
|
52
|
+
"base": "HEAD",
|
|
53
|
+
"witnesses": "auto",
|
|
54
|
+
"dimensions": {
|
|
55
|
+
"rounding": "The change rounds a monetary amount before the final conversion to cents"
|
|
56
|
+
},
|
|
57
|
+
"max_calls": 12
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
For another preset, use `{"check":"docs","base":"HEAD"}` or `{"check":"spec","base":"HEAD","spec_path":"docs/requirements.md"}`; these are also illustrative calls, not recorded executions.
|
|
62
|
+
|
|
63
|
+
## Results and next action
|
|
64
|
+
|
|
65
|
+
Findings name the evidence unit, stale sentence or requirement and its probability. Fixed findings require at least 0.7. Docs probabilities from 0.2 up to 0.7 are unsure without an additional judgment; read the indicated wording. Local-caller probabilities between 0.2 and 0.7 are unsure; `cannot_tell` at least 0.3 is abstain naming the missing provider or binding. Failed witness batches remain unsure. Read flagged source, callers or documentation before editing and preserve unresolved uncertainty in reports. See [shared result reading](../../README.md#read-the-results).
|
|
66
|
+
|
|
67
|
+
## Limits and failure behavior
|
|
68
|
+
|
|
69
|
+
No findings is not proof of safety, unseen-caller coverage or complete documentation. Unjudged units, callers, sections, requirements and severity are named when request or session budgets stop work. Parser gaps and unresolved dynamic providers remain limits, not invented answers. Missing configuration refuses judgment without a chat-model fallback. Static collection is bounded and does not evaluate third-party code.
|
|
70
|
+
|
|
71
|
+
## Host differences
|
|
72
|
+
|
|
73
|
+
Explicit checks share the same read-only contract in pi and omp. The extension also runs only the docs preset automatically at eligible run end; see [automatic documentation checking](../../README.md#automatic-documentation-check). Risk and spec never run automatically.
|
|
74
|
+
|
|
75
|
+
## Related tools
|
|
76
|
+
|
|
77
|
+
[Typed situation judgment](jev_ask.md), [existing test selection](jev_select_tests.md), [design policy](../design.md).
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# jev_find_files
|
|
2
|
+
|
|
3
|
+
Rank repository files for a behavioral goal and suggest the entry point to read first.
|
|
4
|
+
|
|
5
|
+
## Use for
|
|
6
|
+
|
|
7
|
+
Start a task in unfamiliar code when you can describe the behavior but do not know its filename or exact identifier.
|
|
8
|
+
|
|
9
|
+
## Use another tool when
|
|
10
|
+
|
|
11
|
+
Use native filename search for known names and text search for known strings, regular expressions or symbols. Use [jev_ask_files](jev_ask_files.md) to ask questions about candidates you already have, or [jev_locate_in_file](jev_locate_in_file.md) to locate a range inside one known large file.
|
|
12
|
+
|
|
13
|
+
## Evidence model
|
|
14
|
+
|
|
15
|
+
Deterministic lexical pre-ranking and name judgments produce candidates; the tool then reads bounded keyword-selected passages and narrows a shortlist for the entry-point choice. Jev sees excerpts, not every complete file. Names help ranking but do not establish behavior. A behavioral sentence is more useful than a guessed identifier; known identifiers belong in `keywords`. The ranked list estimates whether each file helps with the goal, not whether it implements the complete behavior.
|
|
16
|
+
|
|
17
|
+
## Parameters
|
|
18
|
+
|
|
19
|
+
| Field | Type / default | Meaning |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| `goal` | Required nonempty string | Behavioral sentence with at least five content words for an unqualified entry verdict. Shorter goals are accepted but capped at unsure. |
|
|
22
|
+
| `keywords` | Optional array of nonempty strings, default empty | Known identifiers or terms steering lexical ranking and excerpt selection. |
|
|
23
|
+
| `scope` | Optional directory string or array of directory strings, default repository | Restrict search; an incorrect scope may yield none. |
|
|
24
|
+
| `exclude` | Optional array of glob strings | Skip matches; excluding tests also removes their context from results. |
|
|
25
|
+
| `effort` | Optional `"quick"`, `"default"` (default), or `"thorough"` | Bound breadth: quick ranks up to 32 name candidates and reads 8; default 128 and 20; thorough 256 and 40. Wider search is not a guarantee of a better entry. |
|
|
26
|
+
| `max_calls` | Optional non-negative integer | Bound requests including controls; return the best-ranked work completed so far and state omissions. |
|
|
27
|
+
|
|
28
|
+
Unknown fields or invalid values are rejected. All paths are repository-relative; absolute paths, parent traversal, escaping symlinks, Git metadata and internal URLs are not allowed file inputs.
|
|
29
|
+
|
|
30
|
+
## Example
|
|
31
|
+
|
|
32
|
+
Illustrative call with fictional repository paths, not a recorded execution:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"goal": "Find where invoice amounts are rounded before storage",
|
|
37
|
+
"keywords": ["invoice", "round"],
|
|
38
|
+
"scope": ["src"],
|
|
39
|
+
"exclude": ["**/generated/**"],
|
|
40
|
+
"effort": "quick",
|
|
41
|
+
"max_calls": 6
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Results and next action
|
|
46
|
+
|
|
47
|
+
`entry:` names the file to open first with its probability, followed by ranked related paths and a suggested read. `entry: unsure` can mean two plausible entries, order sensitivity, inconclusive excerpts or a short goal: read the leading two candidates. `entry: none` means no candidate fit the supplied evidence: rephrase the behavioral goal or widen scope. Paths and probabilities are not source text; read the files before editing. See [shared result reading](../../README.md#read-the-results).
|
|
48
|
+
|
|
49
|
+
## Limits and failure behavior
|
|
50
|
+
|
|
51
|
+
Candidate counts and excerpts are bounded; excluded, unreadable or oversized evidence and exhausted budgets leave explicit limits. Keyword pre-ranking and name guards do not prove that omitted files are irrelevant. Missing configuration refuses judgment without a chat-model fallback. Optional native search acceleration may fall back to the available collection path; this does not broaden the evidence guarantee. Session budgets remain separate from `max_calls`.
|
|
52
|
+
|
|
53
|
+
## Host differences
|
|
54
|
+
|
|
55
|
+
pi exposes this semantic tool alongside its native filename `find`. In omp it starts inactive when native semantic `find` is active; when native `find` is absent, session startup activates `jev_find_files`. omp filename search is `glob`. Judgment behavior is otherwise shared and read-only.
|
|
56
|
+
|
|
57
|
+
## Related tools
|
|
58
|
+
|
|
59
|
+
[Per-file triage](jev_ask_files.md), [large-file location](jev_locate_in_file.md), [design policy](../design.md).
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# jev_locate_in_file
|
|
2
|
+
|
|
3
|
+
Choose the line range in one large file that serves a behavioral goal, then read that range directly.
|
|
4
|
+
|
|
5
|
+
## Use for
|
|
6
|
+
|
|
7
|
+
A known file is too large to read whole and you need the part serving a particular goal, such as retry scheduling or flag parsing. Prefer goal-based location to opening arbitrary successive chunks.
|
|
8
|
+
|
|
9
|
+
## Use another tool when
|
|
10
|
+
|
|
11
|
+
Use native text search for known symbols or strings, direct reading for files smaller than 19 KB, [jev_find_files](jev_find_files.md) for unknown filenames, and [jev_ask_files](jev_ask_files.md) for independent questions about multiple files.
|
|
12
|
+
|
|
13
|
+
## Evidence model
|
|
14
|
+
|
|
15
|
+
Code splits the file into declarations or sections and constructs a choice over relevant ranges. Larger files use streamed window outlines and refinement, with explicit limitations. Jev reads supplied source, not runtime behavior. Several sections can legitimately share a goal, dividing probability between them; two plausible ranges do not imply that either is wrong. Optional parser failures use heuristic sections and report the gap.
|
|
16
|
+
|
|
17
|
+
## Parameters
|
|
18
|
+
|
|
19
|
+
| Field | Type / default | Meaning |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| `path` | Required nonempty string | One repository-relative file, at least 19,000 bytes. |
|
|
22
|
+
| `goal` | Required nonempty string | Sentence describing the behavior you need to find. |
|
|
23
|
+
|
|
24
|
+
There is no per-invocation `max_calls` parameter; session budgets still apply. Unknown fields are rejected. Absolute paths, parent traversal, escaping symlinks, Git metadata and internal URLs are not file inputs.
|
|
25
|
+
|
|
26
|
+
## Example
|
|
27
|
+
|
|
28
|
+
Illustrative call with a fictional repository path, not a recorded execution:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"path": "src/scheduler.ts",
|
|
33
|
+
"goal": "Find the section that reschedules tasks after a transient failure"
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Results and next action
|
|
38
|
+
|
|
39
|
+
A result names `path:start-end`, a label, probability and the next read. Above 0.7 an unmarked range is a verdict: read it. From 0.4 through 0.7, unsure lists the two best ranges by probability: read both, not an arbitrary next section. Below 0.4 the tool narrows to three leading candidates plus none and judges again, retaining the original unsure band. Option-order sensitivity can also leave the result unsure. `none` means no supplied section fits: search elsewhere or clarify the goal. See [shared result reading](../../README.md#read-the-results).
|
|
40
|
+
|
|
41
|
+
## Limits and failure behavior
|
|
42
|
+
|
|
43
|
+
Small files are refused with direct-read guidance. Whole-file admission is bounded by 320,000 characters and 1,280,000 bytes; larger files take the streamed-window path, not silent whole-file truncation. Window serialization, fallback section sizes, parsing support and choice cardinality are separately bounded. Unreadable or invalid evidence, missing configuration and exhausted session budgets are reported rather than producing a range verdict. Missing optional Python grammar is named with installation guidance. A selected window does not prove every relevant declaration was inspected.
|
|
44
|
+
|
|
45
|
+
## Host differences
|
|
46
|
+
|
|
47
|
+
omp `read` already outlines code files: use this tool when that outline does not tell you which range serves your goal. Both hosts use the same judgment protocol; omp marks the tool read-only.
|
|
48
|
+
|
|
49
|
+
## Related tools
|
|
50
|
+
|
|
51
|
+
[Semantic file finding](jev_find_files.md), [per-file triage](jev_ask_files.md), [design policy](../design.md).
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# jev_select_tests
|
|
2
|
+
|
|
3
|
+
Select affected existing tests from a diff and return runner commands without running or collecting tests.
|
|
4
|
+
|
|
5
|
+
## Use for
|
|
6
|
+
|
|
7
|
+
After editing code, decide which existing scenarios to execute while preserving conservative fallbacks and runner context.
|
|
8
|
+
|
|
9
|
+
## Use another tool when
|
|
10
|
+
|
|
11
|
+
Use native reading/search to inspect exact test names and shell execution to run them. Use [jev_check_diff](jev_check_diff.md) for risk or documentation review, and [jev_ask](jev_ask.md) for one test-versus-implementation judgment. This tool does not decide whether a new test scenario must be written.
|
|
12
|
+
|
|
13
|
+
## Evidence model
|
|
14
|
+
|
|
15
|
+
Static discovery reads tracked tests, literal project configuration and imports without evaluating third-party configuration or invoking collection. Import closure includes unchanged intermediates and applicable pytest `conftest` fixtures. Touched tests are selected without judgment. Remaining scenarios receive changed-unit pointers, using test and import evidence. Runtime and type tests remain separate; commands preserve working directory, config, project, framework and project options.
|
|
16
|
+
|
|
17
|
+
Residual exported-unit checks concern changed units not exercised by any discovered test, only within the discovered inventory. They are not global coverage reports or obligations to add a scenario. Unsupported or unresolved runners keep that conclusion unsure and name a manual next action.
|
|
18
|
+
|
|
19
|
+
## Parameters
|
|
20
|
+
|
|
21
|
+
| Field | Type / default | Meaning |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| `base` | Optional nonempty Git ref, default `HEAD` | Compare the current tree with this revision. |
|
|
24
|
+
| `paths` | Optional array of nonempty strings, default discovered inventory | Candidate test files or globs, not changed source paths. |
|
|
25
|
+
| `witnesses` | Optional `"off"`, `"auto"` (default), `"on"` | Controls residual coverage checks; test pointers have no witnesses. |
|
|
26
|
+
| `max_calls` | Optional non-negative integer | Bound requests; unjudged scenarios remain selected. Separate from session budgets. |
|
|
27
|
+
|
|
28
|
+
Unknown fields and unresolved refs are rejected with guidance. Repository-relative evidence paths are confined to the repository; internal URLs are not file inputs. Narrowing candidate paths also narrows the inventory to which coverage observations apply.
|
|
29
|
+
|
|
30
|
+
## Example
|
|
31
|
+
|
|
32
|
+
Illustrative call with fictional repository paths, not a recorded execution:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"base": "HEAD",
|
|
37
|
+
"paths": ["test/**/*.test.ts"],
|
|
38
|
+
"witnesses": "auto",
|
|
39
|
+
"max_calls": 8
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Results and next action
|
|
44
|
+
|
|
45
|
+
Inspect selected scenarios and the returned runner commands, then execute them yourself. A scenario is selected when `1 - p(none) >= 0.5`; missing answers are selected too. If at least 80% of a file's scenarios are selected, names are uncertain or counts unknown, the command runs the whole file instead of a fragile name filter. Commands remain separate when runner context differs.
|
|
46
|
+
|
|
47
|
+
`fallback: all` means conservative selection, not that Jev confirmed every scenario is affected. `changed, run by no discovered test` is limited to discovery evidence; do not call it repository-wide uncovered behavior. Read unsure or unsupported-runner limits and follow the named manual check. See [shared result reading](../../README.md#read-the-results).
|
|
48
|
+
|
|
49
|
+
## Limits and failure behavior
|
|
50
|
+
|
|
51
|
+
No tests are executed or collected. Discovery is static and runner/version support is bounded; calculated config, unresolved runners, dynamic names and missing optional parsers can prevent precise filtering. Unavailable Jev or exhausted budgets preserve tests rather than silently dropping them. Whole-file fallbacks avoid unsafe name filtering but do not prove the discovery inventory complete. Residual coverage witnesses check setup, not execution. This tool cannot infer required new scenarios or replace actual verification.
|
|
52
|
+
|
|
53
|
+
## Host differences
|
|
54
|
+
|
|
55
|
+
Both hosts share static discovery and command planning; omp marks this tool read-only. Host-native tools perform execution separately. Optional parser availability changes evidence precision and is reported, not concealed behind a host-specific judgment implementation.
|
|
56
|
+
|
|
57
|
+
## Related tools
|
|
58
|
+
|
|
59
|
+
[Diff review](jev_check_diff.md), [combined judgment](jev_ask.md), [design policy](../design.md).
|
package/package.json
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jev-agent-tools",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"description": "Six evidence-oriented tools for pi and omp coding agents, compatible with the Jev API format.",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/NomenAK/jev-tools.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/NomenAK/jev-tools#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/NomenAK/jev-tools/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"typescript",
|
|
17
|
+
"coding-agents",
|
|
18
|
+
"code-review",
|
|
19
|
+
"test-selection",
|
|
20
|
+
"pi",
|
|
21
|
+
"omp",
|
|
22
|
+
"jev"
|
|
23
|
+
],
|
|
24
|
+
"files": [
|
|
25
|
+
"src/",
|
|
26
|
+
"rules/",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE",
|
|
29
|
+
"CHANGELOG.md",
|
|
30
|
+
"docs/design.md",
|
|
31
|
+
"docs/tools/jev_ask.md",
|
|
32
|
+
"docs/tools/jev_ask_files.md",
|
|
33
|
+
"docs/tools/jev_find_files.md",
|
|
34
|
+
"docs/tools/jev_locate_in_file.md",
|
|
35
|
+
"docs/tools/jev_check_diff.md",
|
|
36
|
+
"docs/tools/jev_select_tests.md"
|
|
37
|
+
],
|
|
38
|
+
"pi": {
|
|
39
|
+
"extensions": [
|
|
40
|
+
"./src/index.ts"
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
"omp": {
|
|
44
|
+
"extensions": [
|
|
45
|
+
"./src/index.ts"
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=24"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"typecheck": "tsc --noEmit",
|
|
53
|
+
"check:imports": "node scripts/check-imports.ts",
|
|
54
|
+
"lint": "biome check src test scripts",
|
|
55
|
+
"test": "node --test test/*.test.ts"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"@sinclair/typebox": "^0.34.0"
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"@earendil-works/pi-coding-agent": ">=0.87.1"
|
|
62
|
+
},
|
|
63
|
+
"peerDependenciesMeta": {
|
|
64
|
+
"@earendil-works/pi-coding-agent": {
|
|
65
|
+
"optional": true
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@biomejs/biome": "^2.0.0",
|
|
70
|
+
"@earendil-works/pi-coding-agent": "0.87.1",
|
|
71
|
+
"@types/node": "^24.0.0",
|
|
72
|
+
"jest": "30.5.2",
|
|
73
|
+
"typescript": "^5.9.0",
|
|
74
|
+
"vitest": "~3.2.7"
|
|
75
|
+
},
|
|
76
|
+
"optionalDependencies": {
|
|
77
|
+
"@ast-grep/lang-python": "^0.0.6",
|
|
78
|
+
"@ast-grep/napi": "^0.45.3",
|
|
79
|
+
"@ff-labs/fff-node": "^0.11.0"
|
|
80
|
+
},
|
|
81
|
+
"publishConfig": {
|
|
82
|
+
"access": "public",
|
|
83
|
+
"provenance": true
|
|
84
|
+
}
|
|
85
|
+
}
|
package/rules/jev-ask.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { SgNode } from "@ast-grep/napi";
|
|
3
|
+
import {
|
|
4
|
+
type LexicalSource,
|
|
5
|
+
type LexicalToken,
|
|
6
|
+
tokenize,
|
|
7
|
+
} from "../core/lexical.ts";
|
|
8
|
+
import type { SyntaxRootParser } from "../core/syntax.ts";
|
|
9
|
+
import type { Result } from "../result.ts";
|
|
10
|
+
import { loadSyntaxRootParser } from "./syntax.ts";
|
|
11
|
+
|
|
12
|
+
export interface AnalysisCounter {
|
|
13
|
+
parsed(path: string, fingerprint: string): void;
|
|
14
|
+
tokenized(path: string, fingerprint: string): void;
|
|
15
|
+
resolved(path: string, fingerprint: string): void;
|
|
16
|
+
}
|
|
17
|
+
export interface AnalysisContext extends LexicalSource {
|
|
18
|
+
parser: SyntaxRootParser | undefined;
|
|
19
|
+
fingerprint(text: string): string;
|
|
20
|
+
counter: AnalysisCounter | undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Owns one invocation's source versions; never retains repository data across calls. */
|
|
24
|
+
export async function createAnalysisContext(
|
|
25
|
+
counter?: AnalysisCounter,
|
|
26
|
+
sourceParser?: SyntaxRootParser,
|
|
27
|
+
): Promise<AnalysisContext> {
|
|
28
|
+
const fingerprints = new Map<string, string>();
|
|
29
|
+
const fingerprint = (text: string): string => {
|
|
30
|
+
let value = fingerprints.get(text);
|
|
31
|
+
if (value === undefined) {
|
|
32
|
+
value = createHash("sha256").update(text).digest("hex");
|
|
33
|
+
fingerprints.set(text, value);
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
};
|
|
37
|
+
const lexical = new Map<string, Map<string, LexicalToken[]>>();
|
|
38
|
+
const tokenizeSource: LexicalSource["tokenize"] = (
|
|
39
|
+
path,
|
|
40
|
+
text,
|
|
41
|
+
python = false,
|
|
42
|
+
) => {
|
|
43
|
+
const version = fingerprint(text);
|
|
44
|
+
let versions = lexical.get(path);
|
|
45
|
+
if (!versions) {
|
|
46
|
+
versions = new Map();
|
|
47
|
+
lexical.set(path, versions);
|
|
48
|
+
}
|
|
49
|
+
const key = `${version}:${python}`;
|
|
50
|
+
let tokens = versions.get(key);
|
|
51
|
+
if (!tokens) {
|
|
52
|
+
counter?.tokenized(path, version);
|
|
53
|
+
tokens = tokenize(text, python);
|
|
54
|
+
versions.set(key, tokens);
|
|
55
|
+
}
|
|
56
|
+
return tokens;
|
|
57
|
+
};
|
|
58
|
+
const source = sourceParser ?? (await loadSyntaxRootParser());
|
|
59
|
+
if (!source)
|
|
60
|
+
return {
|
|
61
|
+
parser: undefined,
|
|
62
|
+
fingerprint,
|
|
63
|
+
counter,
|
|
64
|
+
tokenize: tokenizeSource,
|
|
65
|
+
};
|
|
66
|
+
const roots = new Map<string, Map<string, Result<{ root: SgNode }>>>();
|
|
67
|
+
const parser: SyntaxRootParser = {
|
|
68
|
+
supports: source.supports,
|
|
69
|
+
parseRaw(path, text) {
|
|
70
|
+
const version = fingerprint(text);
|
|
71
|
+
let versions = roots.get(path);
|
|
72
|
+
if (!versions) {
|
|
73
|
+
versions = new Map();
|
|
74
|
+
roots.set(path, versions);
|
|
75
|
+
}
|
|
76
|
+
let parsed = versions.get(version);
|
|
77
|
+
if (!parsed) {
|
|
78
|
+
counter?.parsed(path, version);
|
|
79
|
+
parsed = source.parseRaw(path, text);
|
|
80
|
+
versions.set(version, parsed);
|
|
81
|
+
}
|
|
82
|
+
return parsed;
|
|
83
|
+
},
|
|
84
|
+
parse(path, text) {
|
|
85
|
+
const parsed = this.parseRaw(path, text);
|
|
86
|
+
if (!parsed.ok) return parsed;
|
|
87
|
+
return parsed.root.find({ rule: { kind: "ERROR" } })
|
|
88
|
+
? { ok: false, error: "Source syntax incomplete" }
|
|
89
|
+
: parsed;
|
|
90
|
+
},
|
|
91
|
+
declarations: source.declarations,
|
|
92
|
+
};
|
|
93
|
+
return {
|
|
94
|
+
parser,
|
|
95
|
+
fingerprint,
|
|
96
|
+
counter,
|
|
97
|
+
tokenize: tokenizeSource,
|
|
98
|
+
resolved: (path, text) => counter?.resolved(path, fingerprint(text)),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { dirname, isAbsolute, matchesGlob, relative, resolve } from "node:path";
|
|
3
|
+
import { CONCURRENCY, MAX_FILES, TIMEOUT_MS } from "../constants.ts";
|
|
4
|
+
import type { GitExec } from "../core/git.ts";
|
|
5
|
+
import type { FileIdentity } from "../core/integrity.ts";
|
|
6
|
+
import type { Result } from "../result.ts";
|
|
7
|
+
import {
|
|
8
|
+
checkFileAdmission,
|
|
9
|
+
collectFiles,
|
|
10
|
+
resolveInsideRepo,
|
|
11
|
+
} from "./files.ts";
|
|
12
|
+
|
|
13
|
+
const excludedDirectories = new Set(["node_modules", ".git", "dist", "build"]);
|
|
14
|
+
const lockfile =
|
|
15
|
+
/(?:^|\/)(?:package-lock\.json|npm-shrinkwrap\.json|yarn\.lock|pnpm-lock\.yaml|bun\.lockb?|Cargo\.lock|Gemfile\.lock|poetry\.lock|uv\.lock|composer\.lock|Pipfile\.lock|go\.sum)$/;
|
|
16
|
+
export interface AskFile {
|
|
17
|
+
path: string;
|
|
18
|
+
content: string;
|
|
19
|
+
identity: FileIdentity;
|
|
20
|
+
}
|
|
21
|
+
export async function collectAskFiles(
|
|
22
|
+
cwd: string,
|
|
23
|
+
paths: readonly string[],
|
|
24
|
+
signal?: AbortSignal,
|
|
25
|
+
exec?: GitExec,
|
|
26
|
+
): Promise<Result<{ files: AskFile[]; skipped: string[] }>> {
|
|
27
|
+
if (!paths.length)
|
|
28
|
+
return {
|
|
29
|
+
ok: false,
|
|
30
|
+
error: "Provide at least one path, directory or glob.",
|
|
31
|
+
};
|
|
32
|
+
for (const path of paths) {
|
|
33
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(path))
|
|
34
|
+
return {
|
|
35
|
+
ok: false,
|
|
36
|
+
error: `Internal URLs are not files; use read for ${path}.`,
|
|
37
|
+
};
|
|
38
|
+
if (isAbsolute(path) || path.split(/[\\/]/).includes(".."))
|
|
39
|
+
return {
|
|
40
|
+
ok: false,
|
|
41
|
+
error: `Path must remain inside the repository: ${path}.`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const skipped = new Set<string>();
|
|
45
|
+
const candidates = new Set<string>();
|
|
46
|
+
try {
|
|
47
|
+
let inventory: Set<string> | undefined;
|
|
48
|
+
if (exec) {
|
|
49
|
+
const result = await exec(
|
|
50
|
+
"git",
|
|
51
|
+
["ls-files", "--cached", "--others", "--exclude-standard", "-z"],
|
|
52
|
+
{ cwd, timeout: TIMEOUT_MS, signal },
|
|
53
|
+
);
|
|
54
|
+
if (result.code === 0)
|
|
55
|
+
inventory = new Set(result.stdout.split("\0").filter(Boolean));
|
|
56
|
+
}
|
|
57
|
+
const add = async (path: string) => {
|
|
58
|
+
const admission = await checkFileAdmission(
|
|
59
|
+
cwd,
|
|
60
|
+
path,
|
|
61
|
+
exec,
|
|
62
|
+
signal,
|
|
63
|
+
inventory,
|
|
64
|
+
);
|
|
65
|
+
if (!admission.ok) {
|
|
66
|
+
skipped.add(`${path} (${admission.error})`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (path.split("/").some((part) => excludedDirectories.has(part)))
|
|
70
|
+
skipped.add(`${path} (build output or dependencies)`);
|
|
71
|
+
else if (lockfile.test(path)) skipped.add(`${path} (lockfile)`);
|
|
72
|
+
else candidates.add(path);
|
|
73
|
+
};
|
|
74
|
+
const walk = async (directory: string, pattern?: string): Promise<void> => {
|
|
75
|
+
signal?.throwIfAborted();
|
|
76
|
+
const safe = await resolveInsideRepo(cwd, directory);
|
|
77
|
+
if (!safe.ok) {
|
|
78
|
+
skipped.add(`${directory} (${safe.error})`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const entries = await readdir(safe.abs, { withFileTypes: true });
|
|
82
|
+
await Promise.all(
|
|
83
|
+
entries.map(async (entry) => {
|
|
84
|
+
const path = relative(
|
|
85
|
+
cwd,
|
|
86
|
+
resolve(cwd, directory, entry.name),
|
|
87
|
+
).replaceAll("\\", "/");
|
|
88
|
+
if (entry.isSymbolicLink()) {
|
|
89
|
+
skipped.add(`${path} (symbolic link)`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (entry.isDirectory()) {
|
|
93
|
+
if (excludedDirectories.has(entry.name)) {
|
|
94
|
+
skipped.add(`${path}/ (build output or dependencies)`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
await walk(path, pattern);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (
|
|
101
|
+
entry.isFile() &&
|
|
102
|
+
(!pattern ||
|
|
103
|
+
matchesGlob(
|
|
104
|
+
path
|
|
105
|
+
.split("/")
|
|
106
|
+
.map((part) =>
|
|
107
|
+
part.startsWith(".") ? `__dot__${part.slice(1)}` : part,
|
|
108
|
+
)
|
|
109
|
+
.join("/"),
|
|
110
|
+
pattern
|
|
111
|
+
.split("/")
|
|
112
|
+
.map((part) =>
|
|
113
|
+
part.startsWith(".") ? `__dot__${part.slice(1)}` : part,
|
|
114
|
+
)
|
|
115
|
+
.join("/"),
|
|
116
|
+
))
|
|
117
|
+
)
|
|
118
|
+
await add(path);
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
for (const input of paths) {
|
|
123
|
+
signal?.throwIfAborted();
|
|
124
|
+
const safe = await resolveInsideRepo(cwd, input);
|
|
125
|
+
const info = safe.ok
|
|
126
|
+
? await stat(safe.abs).catch(() => undefined)
|
|
127
|
+
: undefined;
|
|
128
|
+
if (info && safe.ok) {
|
|
129
|
+
if (info.isFile()) {
|
|
130
|
+
const admission = await checkFileAdmission(
|
|
131
|
+
cwd,
|
|
132
|
+
safe.rel,
|
|
133
|
+
exec,
|
|
134
|
+
signal,
|
|
135
|
+
inventory,
|
|
136
|
+
);
|
|
137
|
+
if (!admission.ok) return admission;
|
|
138
|
+
}
|
|
139
|
+
if (input.split("/").some((part) => excludedDirectories.has(part))) {
|
|
140
|
+
skipped.add(`${input} (build output or dependencies)`);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (info.isDirectory()) await walk(safe.rel || ".");
|
|
144
|
+
else if (info.isFile()) {
|
|
145
|
+
const admission = await checkFileAdmission(
|
|
146
|
+
cwd,
|
|
147
|
+
safe.rel,
|
|
148
|
+
exec,
|
|
149
|
+
signal,
|
|
150
|
+
inventory,
|
|
151
|
+
);
|
|
152
|
+
if (!admission.ok) return admission;
|
|
153
|
+
await add(safe.rel);
|
|
154
|
+
} else skipped.add(`${input} (not a regular file)`);
|
|
155
|
+
} else {
|
|
156
|
+
const parts = input.split("/");
|
|
157
|
+
const wildcard = parts.findIndex((part) => /[?*[\]{}]/.test(part));
|
|
158
|
+
if (wildcard < 0) {
|
|
159
|
+
if (!safe.ok && !safe.error.includes("ENOENT")) return safe;
|
|
160
|
+
skipped.add(`${input} (missing)`);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const prefix = parts.slice(0, wildcard).join("/") || ".";
|
|
164
|
+
const prefixLocation = await resolveInsideRepo(cwd, prefix);
|
|
165
|
+
if (!prefixLocation.ok) return prefixLocation;
|
|
166
|
+
const pattern = input.replace(/^\.\//, "");
|
|
167
|
+
if (inventory) {
|
|
168
|
+
for (const path of inventory) {
|
|
169
|
+
const dotPath = path
|
|
170
|
+
.split("/")
|
|
171
|
+
.map((part) =>
|
|
172
|
+
part.startsWith(".") ? `__dot__${part.slice(1)}` : part,
|
|
173
|
+
)
|
|
174
|
+
.join("/");
|
|
175
|
+
const dotPattern = pattern
|
|
176
|
+
.split("/")
|
|
177
|
+
.map((part) =>
|
|
178
|
+
part.startsWith(".") ? `__dot__${part.slice(1)}` : part,
|
|
179
|
+
)
|
|
180
|
+
.join("/");
|
|
181
|
+
if (matchesGlob(dotPath, dotPattern)) await add(path);
|
|
182
|
+
}
|
|
183
|
+
} else await walk(prefix, pattern);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const files: AskFile[] = [];
|
|
187
|
+
const sorted = [...candidates].sort();
|
|
188
|
+
const contributions = new Map<string, number>();
|
|
189
|
+
for (const path of sorted) {
|
|
190
|
+
const directory = dirname(path);
|
|
191
|
+
contributions.set(directory, (contributions.get(directory) ?? 0) + 1);
|
|
192
|
+
}
|
|
193
|
+
let accepted = 0;
|
|
194
|
+
for (let i = 0; i < sorted.length; i += CONCURRENCY) {
|
|
195
|
+
const rows = await Promise.all(
|
|
196
|
+
sorted.slice(i, i + CONCURRENCY).map(async (path) => {
|
|
197
|
+
const result = await collectFiles(cwd, [path], signal, {
|
|
198
|
+
exec,
|
|
199
|
+
inventory,
|
|
200
|
+
});
|
|
201
|
+
if (!result.ok) {
|
|
202
|
+
skipped.add(`${path} (${result.error})`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const identity = result.identities[0];
|
|
206
|
+
if (!identity) return;
|
|
207
|
+
if (
|
|
208
|
+
identity.content.includes("\0") ||
|
|
209
|
+
identity.readSha256 !== identity.insertedSha256
|
|
210
|
+
) {
|
|
211
|
+
skipped.add(`${path} (binary or invalid UTF-8)`);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
return { path, content: identity.content, identity };
|
|
215
|
+
}),
|
|
216
|
+
);
|
|
217
|
+
for (const file of rows)
|
|
218
|
+
if (file) {
|
|
219
|
+
accepted++;
|
|
220
|
+
if (files.length < MAX_FILES) files.push(file);
|
|
221
|
+
}
|
|
222
|
+
if (accepted > MAX_FILES) {
|
|
223
|
+
const largest = [...contributions]
|
|
224
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
225
|
+
.slice(0, 5);
|
|
226
|
+
return {
|
|
227
|
+
ok: false,
|
|
228
|
+
error: `At least ${accepted} files exceeds MAX_FILES=${MAX_FILES}; narrow paths. Largest expanded directories: ${largest.map(([path, count]) => `${path}: ${count}`).join(", ")}.`,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return { ok: true, files, skipped: [...skipped].sort() };
|
|
233
|
+
} catch (error) {
|
|
234
|
+
return { ok: false, error: `Cannot expand paths: ${String(error)}` };
|
|
235
|
+
}
|
|
236
|
+
}
|